Android輔助工具,G分助手的實現 - 心悅俱樂部app自動簽到、領G分

最近在使用心悅俱樂部這個APP,裏面有個代幣叫G分,可以換遊戲道具,但需要每天領取,比較繁瑣。於是索性做一個自動領取G分的輔助,姑且叫它G分助手吧。

這個輔助主要是通過Accessibility Service(輔助功能)實現的,總體思路就是通過AccessibilityService模擬點擊來實現自動化。項目地址是https://github.com/LittleFogCat/gpointhelper

1. 查看包名和當前Activity

首先使用adb shell連接上手機。在啓動應用之後,輸入dumpsys activity activities命令查看當前的Activity。

image.png
image.png

可以看到,包名是com.tencent.tgclub,歡迎頁是WelcomeActivity,主頁面是MainActivity

2. 查看當前應用佈局,View的id等

在Android sdk目錄下,有一個tools文件夾。這之中有一個monitor工具,也就是之前的DDMS。連接手機到電腦之後,通過monitor即可看到當前應用界面的佈局了。

  • 點擊dump view hierarchy
    點擊dump

  • 當前應用佈局
    當前應用佈局.png

通過monitor工具,我們就可以獲取到想要點擊View的id,從而爲實現模擬點擊做好準備。

3. AccessibilityService的配置

Accessibility Service的教程網上一搜一大把,很簡單,這裏就不贅述了。

AccessibilityService的xml配置文件如下:

<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:accessibilityEventTypes="typeWindowStateChanged|typeWindowContentChanged|typeViewClicked"
    android:accessibilityFeedbackType="feedbackGeneric"
    android:accessibilityFlags="flagReportViewIds|flagRequestEnhancedWebAccessibility|flagRetrieveInteractiveWindows"
    android:canRequestEnhancedWebAccessibility="true"
    android:canRetrieveWindowContent="true"
    android:canPerformGestures="true"
    android:description="@string/app_name"
    android:notificationTimeout="10"
    android:packageNames="com.tencent.tgclub" />

其中特別要指出的是,flagRequestEnhancedWebAccessibility這一項,是爲了操作WebView中的內容的。最坑的地方在於,在api 26中這個flag就被廢棄了,而且我並沒有找到替代方法。也就是說,在Android O以後的手機很可能就不能用這個方式了,而且竟然沒有可以替代的方式!(只能用Android 7及以前的手機暫時苟一下)

image.png

4. 實現代碼

4.1 判斷是否開啓AccessibilityService的權限

/**
 * 檢測是否本應用輔助功能開啓
 */
fun isAccessibilitySettingsOn(mContext: Context, clazz: Class<out AccessibilityService>): Boolean {
    var accessibilityEnabled = 0
    val service = mContext.packageName + "/" + clazz.canonicalName
    try {
        accessibilityEnabled = Settings.Secure.getInt(mContext.applicationContext.contentResolver,
                Settings.Secure.ACCESSIBILITY_ENABLED)
    } catch (e: Exception) {
        e.printStackTrace()
    }

    val mStringColonSplitter = TextUtils.SimpleStringSplitter(':')
    if (accessibilityEnabled == 1) {
        val settingValue = Settings.Secure.getString(mContext.applicationContext.contentResolver,
                Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES)
        if (settingValue != null) {
            mStringColonSplitter.setString(settingValue)
            while (mStringColonSplitter.hasNext()) {
                val accessibilityService = mStringColonSplitter.next()
                if (accessibilityService.equals(service, ignoreCase = true)) {
                    return true
                }
            }
        }
    }
    return false
}
    /**
     * 檢查是否開啓輔助功能,沒有開啓就跳轉到設置頁面
     */
    private fun checkAccessibilityOn() {
        if (!isAccessibilitySettingsOn(this, GPointService::class.java)) {
            mDialog = makeDialog(this,
                    "需要打開輔助功能",
                    "點擊確定,在設置中找到\"G分助手\",打開輔助功能",
                    "確定",
                    DialogInterface.OnClickListener { _, _ -> startActivity(Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)) },
                    false)
            mDialog!!.show()
        }
    }

4.2 AccessibilityService的實現

4.2.1 需求分析

擬定功能有三點:

  1. 簽到
  2. 領取月卡積分
  3. 喂貓

那麼,我們的輔助功能應該是這樣的:
每日定時打開心悅app -> 檢查今日是否完成以上任務 -(如果沒有)-> 執行任務 -> 檢查是否成功 [ -> 上報]

4.2.2 思路

在AccessibilityService的onAccessibilityEvent回調方法中,可以接收到在xml中指定app的事件。

    /**
     * 檢測到事件。
     */
    override fun onAccessibilityEvent(event: AccessibilityEvent) {
        Log.v(TAG, "onAccessibilityEvent: " + AccessibilityEvent.eventTypeToString(event.eventType))
        if (event.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
            // window狀態改變(切換窗口、顯示隱藏、對話框等)
            // doSth...
        } else if(event.eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) {
            // 當前window內容改變
            // doSth...
        }
    }
  1. 首先檢查一下今日任務是否已經執行完畢了,如果是的話就什麼都不做。因爲不能讓輔助妨礙了用戶的正常操作,所以在完成了任務之後就不需要再進行操作了。

  2. 按照4.2.1中的流程依次執行任務。

  3. 執行完畢之後,將結果保存在本地。

4.2.3 設計

思路上捋清楚了,接下來就是具體是設計了。

1. 任務設計

任務的設計主要分爲兩個方面。第一,數據結構;第二,存儲方式。
如何得知當日是否已經執行完畢?執行任務之後如何存儲?
最簡單可行的想法就是使用SharedPreferences來進行記錄,讀取之後保存在AccessibilityService中。

    private lateinit var mSharedPreferences: SharedPreferences

    override fun onServiceConnected() {
        Log.d(TAG, "onServiceConnected: GPointService")
        mSharedPreferences = getSharedPreferences(SP_NAME, Context.MODE_PRIVATE)
    }

另一方面,由於任務狀態包括了簽到、領取月卡積分、喂貓三項,任務重複性高,如果全都寫在AccessibilityService中,冗餘度很高,所以單獨抽象一個Task基類出來。這樣做的好處不僅在於可以減少重複代碼,而且如果以後有新的任務,直接繼承這個父類即可,利於擴展:
uml1

最後,創建一個TaskManager的單例類,統一管理任務,將職責從AccessibilityService中分割出來。
uml3

這裏任務的設計暫告一段落了。

2. 任務執行

在任務執行之後,還需要保存任務的完成狀態。對於每日更新的任務來說,每天的任務完成之後,需要將任務標記爲已完成,並且在第二天的0點(或其他時候)重新變成未完成。

這個花了我很多時間,主要是比較各種方式的優劣。對於定/延時任務,一般來說有這幾種方法:

  • Timer
  • Handler
  • AlarmManager

鑑於任務間隔時間很長,所以這裏採用了AlarmManager作爲定時任務的方法。
另外,考慮到不同的任務可能會有不同的執行時間和間隔,那麼就沒法統一執行時間了。這個問題其實還是很棘手的,雖然有不同的解決方案,但是我最後也沒有找到一個比較完美的。

最終方案

採用過期時間mExpireTime。對於每個Task來說,執行完畢任務之後,設定一個任務過期時間。任務過期之前爲保護期,在保護期內,任務不會再次運行。超過過期時間的,或者沒有設置過期時間的,視爲過期任務,則執行。同時將isTaskDone()方法修改爲shouldRunTask()方法,使其更符合實際邏輯。

每次AccessibilityService的onAccessibilityEvent回調均會調用TaskManager.checkAndRunTasks()方法來檢查所有任務是否過期,對於未過期的任務則跳過,只執行已過期的任務。在checkAndRunTasks方法中,會調用每個Task的shouldRunTask方法,檢查是否應該運行。

對於TaskManager,簡化了外部接口,使得任務的執行更加便捷且清晰;同時當一個任務執行時,其他任務禁止執行,避免互相干擾。
另外對Task類也進行了優化,刪去了過度設計的部分。
umllast

4.3 最終代碼

大體框架已經完成,剩下的內容就是往裏面寫各個任務的業務邏輯了,甚至根據需求可以添加其他任務。
Github地址是https://github.com/LittleFogCat/gpointhelper,有興趣的可以自己改着玩。(不過我猜沒有人會堅持看到這裏。)
事實上,寫到這裏,我已經不想領G分了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章