60.Android 全面理解AccessibilityService

場景

自己想寫一個搶紅包工具,于是需要了解AccessibilityService,在此記錄一下學習使用的過程。目錄如下:

1.什么是AccessibilityService
2.AccessibilityService的基本使用
  2.1 創建服務類
  2.2 聲明服務
  2.3 服務參數設置
    2.3.1 通過<meta-data>進行配置
    2.3.2 通過setServiceInfo()方法進行配置
  2.4 啟動服務
  2.5 獲取事件信息
3.AccessibilityEvent類詳解

1.什么是AccessibilityService

Google為了讓Android系統更實用,為用戶提供了無障礙輔助服務AccessibilityService

AccessibilityService運行在后臺,并且能夠收到由系統發出的一些事件(即:AccessibilityEvent事件,這些事件表示用戶界面一系列的狀態變化),比如焦點改變、輸入內容變化、按鈕被點擊了等等,該種服務能夠請求獲取當前活動窗口并查找其中的內容。換言之,界面中產生的任何變化都會產生一個AccessibilityEvent事件,并由系統通知給AccessibilityService。這就像監視器監視著界面的一舉一動,一旦界面發生變化,立刻發出通知:大家請注意,界面發生了變化。

2.AccessibilityService的基本使用

2.1 創建服務類
編寫自己的服務類,需要繼承AccessibilityService類.其中要實現onAccessibilityEvent(AccessibilityEvent event)onInterruput()兩個重要的方法:

/**
 *  搶紅包服務
 */
public class LuckyMoneyService extends AccessibilityService {

    /**
     *  當服務連上時的回調
     */
    @Override
    protected void onServiceConnected() {
        super.onServiceConnected();

    }

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
       
    }
}
方法名 含義解析
onServiceConnected() 系統成功綁定該服務時被觸發,也就是當你在設置中開啟相應的服務,系統成功的綁定了該服務時會觸發,通常我們可以在這里做一些初始化操作
onAccessibilityEvent(AccessibilityEvent event) 有關AccessibilityEvent事件的回調函數.系統通過sendAccessibiliyEvent()不斷的發送AccessibilityEvent到此處
disableSelf() 禁用當前服務,也就是在服務可以通過該方法停止運行
findFoucs(int falg) 查找擁有特定焦點類型的控件
getRootInActiveWindow() 如果配置能夠獲取窗口內容,則會返回當前活動窗口的根結點
getSeviceInfo() 獲取當前服務的配置信息
performGlobalAction(int action) 執行全局操作,比如返回,回到主頁,打開最近等操作
setServiceInfo(AccessibilityServiceInfo info) 設置當前服務的配置信息
getSystemService(String name) 獲取系統服務
onKeyEvent(KeyEvent event) 如果允許服務監聽按鍵操作,該方法是按鍵事件的回調,需要注意,這個過程發生了系統處理按鍵事件之前

2.2 聲明服務
像其他Service服務一樣,需要在AndroidManifest.xml中聲明該服務.除此之外,該服務還必須配置以下兩項:

配置<intent-filter>,其name為固定的:
android.accessibilityservice.AccessibilityService
聲明BIND_ACCESSIBILITY_SERVICE權限,以便系統能夠綁定該服務(4.0版本后要求)
注意:任何一點配置錯誤,系統都無反應,因此其固定配置如下:

<service
        android:name=". LuckyMoneyService"
        android:enabled="true"
        android:exported="true"
        android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
    <intent-filter>
        <action android:name="android.accessibilityservice.AccessibilityService"/>
    </intent-filter>
</service>

2.3 服務參數設置
AndroidManifest.xml聲明了該服務之后,接下來就是需要對該服務進行一些參數設置了.該服務能夠被配置用來接受指定類型的事件,監聽指定package,檢索窗口內容,獲取事件類型的時間等等.目前有兩種配置方法:

方法一:4.0之后提供了可以通過<meta-data>標簽進行配置
方法二:通過setServiceInfo()方法進行配置

2.3.1 通過<meta-data>進行配置

AndroidManifest.xml聲明的service中,其中:
accessibility_service_label是服務的名稱,這些會在設置里面的輔助服務中顯示出來,用戶需要在里面開啟服務才能使用。
accessibility_service_description是有關這個服務的功能說明,也可以在meta-data指定的配置文件中聲明,效果同:android:description="搶紅包服務,請開啟"
permission和intent-filter是固定寫法。

提供一個meta-data標簽,meta-data主要用于對服務進行一些配置,配置的具體內容通過android:resource指定相應的配置文件:

<service
            android:name=".new_service.LuckyMoneyService"
            android:enabled="true"
            android:exported="true"
            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/lucky_money_accessibility_service_config" />
</service>

接下來我們來看lucky_money_accessibility_service_config的相關配置。
提示:該配置文件是在res目錄下創建xml文件,并在其中創建配置文件lucky_money_accessibility_service_config.xml,該文件名可自定義。

<?xml version="1.0" encoding="utf-8"?>
<accessibility-service
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:description="搶紅包服務,請開啟"
    android:accessibilityEventTypes="typeNotificationStateChanged|typeWindowStateChanged"
    android:packageNames="com.tencent.mm"
    android:accessibilityFeedbackType="feedbackGeneric"
    android:notificationTimeout="100"
    android:accessibilityFlags="flagDefault"
    android:canRetrieveWindowContent="true"/>

現在我們對配置中的重要屬性進行說明:
description="搶紅包服務,請開啟"是有關這個服務的功能說明。

accessibilityEventTypes:服務要監控的事件類型,如通知、窗口改變、點擊、焦點改變等等,如果有多個可以用 | 連起來,具體的值可以在AccessibilityEvent類中查到,如typeAllMask表示接受所有的事件通知。

android:packageNames服務要監控的應用的包名,如果有多個則用逗號連起來,空著表示監聽所有的應用。

android:accessibilityFeedbackType服務反饋的方式,如語音、震動等等,feedbackAllMask代表所有類型。

notificationTimeout:接受事件的時間間隔,通常將其設置為100即可.

canRetrieveWindowContent:服務能否獲取窗口里面的內容。也就是如果你希望在服務中獲取窗體內容的化,則需要設置其值為true.

這些配置除了在xml里面寫之外,還可以在代碼中建立一個AccessibilityServiceInfo對象,然后通過setServiceInfo()來設置。見下方2.3.2:

2.3.2 通過setServiceInfo()方法進行配置
也可以通過setServiceInfo(AccessibilityServiceInfo)為其配置信息,除此之外,通過該方法可以在運行期間動態修改服務配置.需要注意,該方法只能用來配置動態屬性:eventTypes,feedbackType,flags,notificaionTimeout及packageNames.

通常是在onServiceConnected()進行配置,如下代碼:

@Override
    protected void onServiceConnected() {
        AccessibilityServiceInfo serviceInfo = new AccessibilityServiceInfo();
        serviceInfo.eventTypes = AccessibilityEvent.TYPES_ALL_MASK;
        serviceInfo.feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC;
        serviceInfo.packageNames = new String[]{"com.tencent.mm"}; 
        serviceInfo.notificationTimeout=100;
        setServiceInfo(serviceInfo);
    }

在這里涉及到了AccessibilityServiceInfo類,做個說明:
AccessibilityServiceInfo該類被用于配置AccessibilityService信息,該類中包含了大量用于配置的常量字段及用來xml屬性,比如常見的:
android:accessibilityEventTypes、android:canRequestFilterKeyEventsandroid:packageNames等等。

2.4 啟動服務
當我們做完以上操作,便可將app安裝到手機.安裝成功后,在設置->輔助功能中便可以找到我們的服務.該服務默認處在關閉狀態,需要手動開啟。代碼方式打開如下:

Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);
context.startActivity(intent);

2.5 獲取事件信息
上面我們說到,onAccessibilityEvent(AccessibilityEvent event)是該服務的核心方法,其中參數event封裝來自界面相關事件的信息,比如我們可以獲得該事件的事件類型,進而根據起類型選擇不同的處理方式:

 @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
       
    }

這里我們先對AccessibilityEvent進行簡單的說明:
當用戶發生發生變化時,系統會發送一系列的AccessibilityEvent事件,比如按鈕被點擊時會發送TYPE_VIEW_CLICKED類型的事件.

下面會對AccessibilityEvent類詳細介紹。

3.AccessibilityEvent類詳解

AccessibilityEvent是輔助功能服務中一個非常重要的類,它主要用于提供有關用戶界面交互的信息。當用戶界面中發生了服務需要關注的事件時系統就會發送AccessibilityEvent事件,并傳遞到onAccessibilityEvent方法。通常,用得比較多的是event.getEventType()event.getClassName(),分別用于獲取當前事件的類型和發生該事件的類名,通過這兩個的判斷可以過濾想要處理的事件,并進行操作。例如,當點擊一個按鈕時,會發送一個typeTYPE_VIEW_CLICKED,classNameandroid.widget.Button的事件,如果我們需要在某個按鈕被點擊時做一些操作,就可以在onAccessibilityEvent中對event進行判斷。
AccessibilityEventType包括:

TYPE_VIEW_CLICKED
TYPE_VIEW_LONG_CLICKED
TYPE_VIEW_FOCUSED
TYPE_VIEW_SELECTED
TYPE_VIEW_TEXT_CHANGED
TYPE_WINDOW_STATE_CHANGED
TYPE_NOTIFICATION_STATE_CHANGED
TYPE_TOUCH_EXPLORATION_GESTURE_START
TYPE_TOUCH_EXPLORATION_GESTURE_END
TYPE_VIEW_HOVER_ENTER
TYPE_VIEW_HOVER_EXIT
TYPE_VIEW_SCROLLED
TYPE_VIEW_TEXT_SELECTION_CHANGED
TYPE_WINDOW_CONTENT_CHANGED
TYPE_ANNOUNCEMENT
TYPE_GESTURE_DETECTION_START
TYPE_GESTURE_DETECTION_END
TYPE_TOUCH_INTERACTION_START
TYPE_TOUCH_INTERACTION_END
TYPE_VIEW_ACCESSIBILITY_FOCUSED
TYPE_WINDOWS_CHANGED
TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED
方法 說明
getEventType() 事件類型,如上面列舉
getSource() 獲取事件源對應的結點信息
getClassName() 獲取事件源對應類的類型,比如點擊事件是有某個Button產生的,那么此時獲取的就是Button的完整類名: android.widget.Button
getText() 獲取事件源的文本信息,比如事件是有TextView發出的,此時獲取的就是TextView的text屬性.如果該事件源是樹結構,那么此時獲取的是這個樹上所有具有text屬性的值的集合
isEnabled() 事件源(對應的界面控件)是否處在可用狀態
getItemCount() 如果事件源是樹結構,將返回該樹根節點下子節點的數量

特別注意??:
系統不斷的產生各種事件,有些是界面控件產生的,有些是系統產生的。對于由界面控件的產生的事件,通常我們將該控件稱之為事件源。但并不是所有的事件都能通過getSource()方法獲取到事件源,比如像通知消息類型的事件TYPE_NOTIFICATION_STATE_CHANGED

另外,AccessibilityService中還有一個performGlobalAction()方法,用于執行一些通用的事件,主要包括:

GLOBAL_ACTION_BACK    點擊返回按鈕
GLOBAL_ACTION_HOME    點擊home
GLOBAL_ACTION_NOTIFICATIONS    打開通知
GLOBAL_ACTION_RECENTS    打開最近應用
GLOBAL_ACTION_QUICK_SETTINGS    打開快速設置
GLOBAL_ACTION_POWER_DIALOG    打開長按電源鍵的彈框

4.1 獲取窗口內容
僅僅知道事件的信息是不夠的,我們還希望通過事件來獲取發出該事件(事件源)的信息,比如Button按鈕被點擊時它的text.一個服務可以配置為允許服務檢索窗口內容,即獲取窗口內容。

整個窗口內容本質上是關于AccessibilityWindowInfoAccessibilityNodeInfo的樹結構,我們可以稱之為內容樹.(類似View Tree,但由不完全相同)

需要注意,該服務可能配置了只檢測了部分事件,而不是全部事件,這就意味著,當內容樹發生變化后,該服務可能并不知道。即該服務無法及時的了解當前的內容樹是否發生了變化。比如說,你的服務只檢測了點擊事件,但是此時界面的輸入焦點已經變化,這樣整個結點樹也發生了變化,但是你的服務卻不知道,此時你在結點中拿到的窗口內容可能已經不是最新的了。因此,如果你想及時的獲知當前窗口的內容,那么就在配置的時候,設置監聽全部事件。

正如上面所提到的,要想獲取窗口內容,在配置AccessibilityService時需設置canRetrieveWindowContent為true之后,便可以通過一下方法獲取窗口內容:AccessibilityEvent.getSource()、findFocus(int)、getWindow()或者getRootInActiveWindow()。

4.2 服務的生命周期
要理解該中服務的生命周期只需要記住一下三點即可:

a:該種服務完全由系統管理,并遵循已有的服務周期.
b:開啟一個服務只能由用戶在設置中打開,而關閉則只能由用戶在設置中關閉或者服務本身通過diableSelf()方法關閉(當然,現在有些第三放軟件也可以強制關閉該類型服務)
c:系統綁定該服務之后,會調用onServiceConnected()方法,這個方法可以被重寫,在其中,你可以做一些初始化的操作.

4.3 檢測服務是否開啟
介紹了一些AccessibilityService的基礎知識之后,再補充一點關于檢測某個服務是否開啟的知識.通常來說大體有一下兩種方法來檢測服務是否啟用:

方法一:借助服務管理器AccessibilityManager來判斷,但是該方法不能檢測app本身開啟的服務.

private boolean enabled(String name) {
        AccessibilityManager am = (AccessibilityManager) getSystemService(Context.ACCESSIBILITY_SERVICE);
        List<AccessibilityServiceInfo> serviceInfos = am.getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_GENERIC);
        List<AccessibilityServiceInfo> installedAccessibilityServiceList = am.getInstalledAccessibilityServiceList();
        for (AccessibilityServiceInfo info : installedAccessibilityServiceList) {
            Log.d("MainActivity", "AccessibilityServiceInfo -->" + info.getId());
            if (name.equals(info.getId())) {
                return true;
            }
        }
        return false;
    }

既然談到了AccessibilityManager,那么在這里我們就做個簡單的介紹:
AccessibilityManager是系統級別的服務,用來管理AccessibilityService服務,比如分發事件,查詢系統中服務的狀態等等,更多信息參考官方文檔,常見方法如下:

方法 說明
getAccessibilityServiceList() 獲取服務列表(api 14之后廢棄,用下面的方法代替)
getInstalledAccessibilityServiceList() 獲取已安裝到系統的服務列表
getEnabledAccessibilityServiceList(int feedbackTypeFlags) 獲取已啟用的服務列表
isEnabled() 判斷服務是否啟用
sendAccessibilityEvent(AccessibilityEvent event) 發送事件

方法二:我們知道大部分的系統屬性都在settings中進行設置,比如wifi,藍牙狀態等,而這些信息主要是存儲在settings對應的的數據庫中(system表和serure表),這就意味我們可以通過直接讀取setting設置來判斷相關服務是否開啟:

private boolean checkStealFeature1(String service) {
        int ok = 0;
        try {
            ok = Settings.Secure.getInt(getApplicationContext().getContentResolver(), Settings.Secure.ACCESSIBILITY_ENABLED);
        } catch (Settings.SettingNotFoundException e) {
        }

        TextUtils.SimpleStringSplitter ms = new TextUtils.SimpleStringSplitter(':');
        if (ok == 1) {
            String settingValue = Settings.Secure.getString(getApplicationContext().getContentResolver(), Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
            if (settingValue != null) {
                ms.setString(settingValue);
                while (ms.hasNext()) {
                    String accessibilityService = ms.next();
                    if (accessibilityService.equalsIgnoreCase(service)) {
                        return true;
                    }

                }
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容