AccessibilityService使用入門
AccessibilityService設計初衷在於幫助殘障使用者使用android裝置和應用,在後臺執行,可以監聽使用者介面的一些狀態轉換,例如頁面切換、焦點改變、通知、Toast等,並在觸發AccessibilityEvents時由系統接收回調。後來被開發者另闢蹊徑,用於一些外掛開發,比如微信紅包助手,還有一些需要監聽第三方應用的外掛。
最好的資料:ofollow,noindex">官方文件
生命週期
AccessbilityService的生命週期由系統專門管理,並遵循Service的基本生命週期,它只能由使用者自己在設定中手動啟動,系統繫結到服務後,會呼叫它的onServiceConnected()方法。
當用戶手動在設定中關閉服務,或者開發者呼叫disableSelf()方法時,該服務會被關閉銷燬。
基本配置
- 繼承AccessbilityService
class AliAccessibilityService : AccessibilityService() { private val TAG = "AliAccessibilityService" //服務中斷時的回撥 override fun onInterrupt() { Log.d(TAG, "onInterrupt") } //接收到系統傳送AccessibilityEvent時的回撥 override fun onAccessibilityEvent(event: AccessibilityEvent) { Log.d(TAG, "onAccessibilityEvent:event:$event") } }
- AccessbilityService本質上還是一個Service,所以要在AndroidManifest中註冊該服務
<service android:name=".AliAccessibilityService" android:label="支付測試" android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"> <intent-filter> <action android:name="android.accessibilityservice.AccessibilityService" /> </intent-filter> </service>
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"是為了確保只有系統可以繫結該服務。
-
配置你需要監聽的事件型別、要監聽哪個程式,最小監聽間隔等屬性。這裡有兩種方式可以進行配置,一種是在manifest中通過meta-data配置,一種是在程式碼中通過setServiceInfo(AccessibilityServiceInfo)設定。
方式一:通過meta-data設定
<service android:name=".AliAccessibilityService" android:label="支付測試" android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"> <intent-filter> <action android:name="android.accessibilityservice.AccessibilityService" /> </intent-filter> <meta-data android:name="android.accessibilityservice" android:resource="@xml/accessible_service_config_ali" /> </service>
accessible_service_config_ali.xml
<?xml version="1.0" encoding="utf-8"?> <accessibility-service xmlns:android="http://schemas.android.com/apk/res/android" android:accessibilityEventTypes="typeWindowStateChanged|typeWindowContentChanged|typeNotificationStateChanged" android:accessibilityFeedbackType="feedbackGeneric" android:accessibilityFlags="flagReportViewIds" android:canRetrieveWindowContent="true" android:description="@string/pay_test" android:notificationTimeout="10" android:canPerformGestures="true" android:packageNames="com.eg.android.AlipayGphone" />
配置的具體含義我們下面再講。
方式二:在程式碼中通過setServiceInfo設定
override fun onServiceConnected() { val serviceInfo = AccessibilityServiceInfo().apply { eventTypes = AccessibilityEvent.TYPES_ALL_MASK feedbackType = AccessibilityServiceInfo.FEEDBACK_ALL_MASK packageNames = arrayOf("com.eg.android.AlipayGphone")//支付寶包名,可以多個 notificationTimeout = 10 } setServiceInfo(serviceInfo) }
這裡我建議使用meta-data的方式進行配置,因為我實踐過程中發現有的屬性不能通過程式碼配置。
-
指引使用者去手動開啟該服務
首先判斷該服務是否為開啟狀態:
public static boolean isAccessibilitySettingsOn(Context mContext, Class<? extends AccessibilityService> clazz) { int accessibilityEnabled = 0; final String service = mContext.getPackageName() + "/" + clazz.getCanonicalName(); try { accessibilityEnabled = Settings.Secure.getInt(mContext.getApplicationContext().getContentResolver(), Settings.Secure.ACCESSIBILITY_ENABLED); } catch (Settings.SettingNotFoundException e) { e.printStackTrace(); } TextUtils.SimpleStringSplitter mStringColonSplitter = new TextUtils.SimpleStringSplitter(':'); if (accessibilityEnabled == 1) { String settingValue = Settings.Secure.getString(mContext.getApplicationContext().getContentResolver(), Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES); if (settingValue != null) { mStringColonSplitter.setString(settingValue); while (mStringColonSplitter.hasNext()) { String accessibilityService = mStringColonSplitter.next(); if (accessibilityService.equalsIgnoreCase(service)) { return true; } } } } return false; }
沒有開啟的話則跳轉到該服務的開啟頁面,由使用者手動開啟
startActivity(Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS))
到這裡,我們的準備工作就已經完成了,開啟支付寶,我們可以看到這樣一條日誌:
onAccessibilityEvent:event:EventType: TYPE_WINDOW_STATE_CHANGED; EventTime: 639278268; PackageName: com.eg.android.AlipayGphone; MovementGranularity: 0; Action: 0 [ ClassName: com.eg.android.AlipayGphone.AlipayLogin; Text: [支付寶]; ContentDescription: null; ItemCount: -1; CurrentItemIndex: -1; IsEnabled: true; IsPassword: false; IsChecked: false; IsFullScreen: true; Scrollable: false; BeforeText: null; FromIndex: -1; ToIndex: -1; ScrollX: -1; ScrollY: -1; MaxScrollX: -1; MaxScrollY: -1; AddedCount: -1; RemovedCount: -1; ParcelableData: null ]; recordCount: 0
其中包括了這個event的類別,當前頁面的包名、類名、一些文字資訊等,具體有哪些屬性可以檢視AccessibilityEvent的toString方法。
檢索視窗內容
除了監聽介面變化之外,我們還可以對介面中的內容進行一些操作,前提是我們配置了以下屬性,允許服務檢索視窗內容:
android:canRetrieveWindowContent="true"
我們可以通過getWindows獲取螢幕上的可以看見的 視窗,它返回一個列表,按降序的方式,排在第一個的就是最頂層的視窗。
當然了,一般情況下我們關心的只是最新的那個活動視窗,稱為當前活動視窗 。我們可以通過以下方法來檢索其中的控制元件:
val root = rootInActiveWindow //獲取當前活動視窗的根節點
val node = root.findAccessibilityNodeInfosByViewId("com.alipay.mobile.payee:id/payee_NextBtn") //通過控制元件id來獲取某個控制元件
val node = root.findAccessibilityNodeInfosByText("確定") //通過text來獲取某個控制元件
val node = root.findFoucs(int falg) //尋找擁有特殊焦點的控制元件(FOCUS_INPUT 或 FOCUS_ACCESSIBILITY)
至於viewId的獲取,我們可以通過android Device Monitor工具來檢視,對於3.0之後的android studio,可以通過命令列工具進入sdk的tools目錄,執行下面命令:
monitor
互動
事件互動
拿到AccessibilityNodeInfo物件後,我們可以進行一些列的操作,包括getChild()、getParent()、getBoundsInScreen()、isClickable等等一些列獲取屬性的操作,當然也可以進行互動性的操作,比如點選(當然前提是這個控制元件的clickable為true):
node[0].performAction(AccessibilityNodeInfo.ACTION_CLICK)
除了操作介面內控制元件之外,我們還可以通過performGlobalAction(int action)執行一些全域性操作,比如點選back鍵、home鍵等等。
performGlobalAction(GLOBAL_ACTION_BACK)performGlobalAction(GLOBAL_ACTION_HOME)performGlobalAction(GLOBAL_ACTION_NOTIFICATIONS)performGlobalAction(GLOBAL_ACTION_RECENTS)
手勢互動
除了Action互動之外,我們還可以模擬人的手勢進行操作,這是在Android 24 中新加的一個api:
dispatchGesture(gesture, callback, handler)
它接收一個GestureDescription(手勢描述)、一個GestureResultCallback(結果回撥)和一個Handler。簡單封裝一下大概是這樣的:
/** * 通過AccessibilityService在螢幕上模擬手勢 * @param path 手勢路徑 */ @RequiresApi(Build.VERSION_CODES.N) fun AccessibilityService.gestureOnScreen( path: Path, startTime:Long = 0, duration:Long = 100, callback:AccessibilityService.GestureResultCallback, handler: Handler? = null ){ val builder = GestureDescription.Builder() builder.addStroke(GestureDescription.StrokeDescription(path, startTime, duration)) val gesture = builder.build() dispatchGesture(gesture, callback, handler) } /** * 通過AccessibilityService在螢幕上某個位置單擊 */ @RequiresApi(Build.VERSION_CODES.N) fun AccessibilityService.clickOnScreen( x:Float, y:Float, callback:AccessibilityService.GestureResultCallback, handler: Handler? = null ){ val p = Path() p.moveTo(x,y) gestureOnScreen(p,callback = callback,handler = handler) }
到這裡,AccessibilityService的基本使用方法已經介紹的差不多了,接下來就是根據自己的專案需求進行組裝,八仙過海各顯神通了。
附錄(重要屬性介紹)
xml屬性 | 說明 | 類別 |
---|---|---|
accessibilityEventTypes | 指定要監聽的事件型別 |
typeAllMask:接收所有事件。 -------視窗事件相關(常用)--------- typeWindowStateChanged:監聽視窗狀態變化,比如開啟一個popupWindow,dialog,Activity切換等等。 typeWindowContentChanged:監聽視窗內容改變,比如根佈局子view的變化。 typeWindowsChanged:監聽螢幕上顯示的系統視窗中的事件更改。 此事件型別只應由系統分派。 typeNotificationStateChanged:監聽通知變化,比如notifacation和toast。 -----------View事件相關-------------- typeViewClicked:監聽view點選事件。 typeViewLongClicked:監聽view長按事件。 typeViewFocused:監聽view焦點事件。 typeViewSelected:監聽AdapterView中的上下文選擇事件。 typeViewTextChanged:監聽EditText的文字改變事件。 typeViewHoverEnter、typeViewHoverExit:監聽view的檢視懸停進入和退出事件。 typeViewScrolled:監聽view滾動,此類事件通常不直接傳送。 typeViewTextSelectionChanged:監聽EditText選擇改變事件。 typeViewAccessibilityFocused:監聽view獲得可訪問性焦點事件。 typeViewAccessibilityFocusCleared:監聽view清除可訪問性焦點事件。 ------------手勢事件相關--------------- typeGestureDetectionStart、typeGestureDetectionEnd:監聽手勢開始和結束事件。 typeTouchInteractionStart、typeTouchInteractionEnd:監聽使用者觸控式螢幕幕事件的開始和結束。 typeTouchExplorationGestureStart、typeTouchExplorationGestureEnd:監聽觸控探索手勢的開始和結束。 |
accessibilityFeedbackType | 指定反饋方式 | feedbackAllMask、feedbackGeneric、feedbackAudible、feedbackSpoken、feedbackHaptic、feedbackVisual |
canRetrieveWindowContent | 是否希望能夠檢索活動視窗內容。此設定無法在執行時更改。 | true or false |
description | 該服務的簡要說明 | @StringRes |
notificationTimeout | 兩個相同型別的可訪問性事件之間的最短間隔時間(以毫秒為單位) | number |
packageNames | 要監聽的應用的包名 | string |
canPerformGestures | 是否可以執行手勢(api 24新增) | true or false |