最近在使用心悅俱樂部這個APP,裏面有個代幣叫G分,可以換遊戲道具,但需要每天領取,比較繁瑣。於是索性做一個自動領取G分的輔助,姑且叫它G分助手吧。
這個輔助主要是通過Accessibility Service(輔助功能)實現的,總體思路就是通過AccessibilityService模擬點擊來實現自動化。項目地址是https://github.com/LittleFogCat/gpointhelper。
1. 查看包名和當前Activity
首先使用adb shell連接上手機。在啓動應用之後,輸入dumpsys activity activities
命令查看當前的Activity。
可以看到,包名是com.tencent.tgclub
,歡迎頁是WelcomeActivity
,主頁面是MainActivity
。
2. 查看當前應用佈局,View的id等
在Android sdk目錄下,有一個tools文件夾。這之中有一個monitor工具,也就是之前的DDMS。連接手機到電腦之後,通過monitor即可看到當前應用界面的佈局了。
-
點擊dump view hierarchy
-
當前應用佈局
通過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及以前的手機暫時苟一下)
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 需求分析
擬定功能有三點:
- 簽到
- 領取月卡積分
- 喂貓
那麼,我們的輔助功能應該是這樣的:
每日定時打開心悅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...
}
}
-
首先檢查一下今日任務是否已經執行完畢了,如果是的話就什麼都不做。因爲不能讓輔助妨礙了用戶的正常操作,所以在完成了任務之後就不需要再進行操作了。
-
按照4.2.1中的流程依次執行任務。
-
執行完畢之後,將結果保存在本地。
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基類出來。這樣做的好處不僅在於可以減少重複代碼,而且如果以後有新的任務,直接繼承這個父類即可,利於擴展:
最後,創建一個TaskManager
的單例類,統一管理任務,將職責從AccessibilityService中分割出來。
這裏任務的設計暫告一段落了。
2. 任務執行
在任務執行之後,還需要保存任務的完成狀態。對於每日更新的任務來說,每天的任務完成之後,需要將任務標記爲已完成,並且在第二天的0點(或其他時候)重新變成未完成。
這個花了我很多時間,主要是比較各種方式的優劣。對於定/延時任務,一般來說有這幾種方法:
- Timer
- Handler
- AlarmManager
鑑於任務間隔時間很長,所以這裏採用了AlarmManager作爲定時任務的方法。
另外,考慮到不同的任務可能會有不同的執行時間和間隔,那麼就沒法統一執行時間了。這個問題其實還是很棘手的,雖然有不同的解決方案,但是我最後也沒有找到一個比較完美的。
最終方案
採用過期時間mExpireTime
。對於每個Task來說,執行完畢任務之後,設定一個任務過期時間。任務過期之前爲保護期,在保護期內,任務不會再次運行。超過過期時間的,或者沒有設置過期時間的,視爲過期任務,則執行。同時將isTaskDone()
方法修改爲shouldRunTask()
方法,使其更符合實際邏輯。
每次AccessibilityService的onAccessibilityEvent
回調均會調用TaskManager.checkAndRunTasks()
方法來檢查所有任務是否過期,對於未過期的任務則跳過,只執行已過期的任務。在checkAndRunTasks
方法中,會調用每個Task的shouldRunTask
方法,檢查是否應該運行。
對於TaskManager
,簡化了外部接口,使得任務的執行更加便捷且清晰;同時當一個任務執行時,其他任務禁止執行,避免互相干擾。
另外對Task
類也進行了優化,刪去了過度設計的部分。
4.3 最終代碼
大體框架已經完成,剩下的內容就是往裏面寫各個任務的業務邏輯了,甚至根據需求可以添加其他任務。
Github地址是https://github.com/LittleFogCat/gpointhelper,有興趣的可以自己改着玩。(不過我猜沒有人會堅持看到這裏。)
事實上,寫到這裏,我已經不想領G分了。