輔助模式最終考驗的是想象力,先來看看怎麼用!| Accessibility

1、序

Hi,你們好,我是承香墨影!java

Android 的輔助模式(Accessibility)功能很是的強大。基本上被獲取到受權以後,能夠監聽手機上的任何事件,例如:屏幕點擊、窗口的變化、以及模擬點擊、模擬系統按鍵等等。node

比較常見的實際使用例子,就是通常應用市場,會推薦開啓輔助模式,以便在安裝 Apk 的時候,自動幫你點擊「下一步」和「安裝」按鈕。還有個例子就是微信搶紅包插件,也是基於它來實現的。android

Accessibility 的權限很是的高,基本上你受權開啓某個別人提供的 AccessibilityService 以後,他就能夠幹不少事情而不讓你知道,而這些是不須要 Root 權限的。因此通常小體量的產品,可能支持它並無什麼用,由於信任度過低了,大部分用戶根本不會打開。比較常見的就是一些工具類的 App,幫用戶節省一些點擊的時間。程序員

雖然不少時候,Accessibility 不會被用在商業產品上,可是這並不妨礙咱們使用 Accessibility 來作一些有意思的功能。小程序

2、輔助模式的使用步驟

輔助模式是能夠支持第三方開發,也就是咱們能夠按照文檔對其進行支持,只要用戶受權開啓此服務,咱們就能夠利用 Accessibility 提供的一些標準 Api 實現不少有意思的功能。微信

若是你想要使用輔助模式,你還須要以下步驟:ide

  1. 實現一個繼承自 AccessibilityService 的服務類。
  2. 設定配置信息,以便系統知道該輔助模式的一些基本信息,例如監聽那些事件。
  3. 在清單文件(AndroidManifest.xml)中,註冊此服務。
  4. 在系統設置中,找到「無障礙」,並開啓此服務。

接下來咱們一步一步講解這裏的步驟和細節。工具

2.1 繼承 AccessibilityService

輔助模式,本質上仍是一個服務,咱們若是想要支持它,首先須要繼承 AccessibilityService 這個類。佈局

AccessibilityService 類提供了不少須要重寫的方法,其中有兩個是強制重寫的:學習

public abstract void onAccessibilityEvent(AccessibilityEvent event);
public abstract void onInterrupt();
複製代碼

當開啓了某個 AccessibilityService 服務以後,系統會在該服務監聽的事件發生的時候,回調它的 onAccessibilityEvent() 方法,並將該事件的信息當參數傳遞過去,若是你監聽的事件足夠多,它就會被頻繁調用。

onInterrupt() 方法會在系統事件被打斷的時候回調,也是會被頻繁調用,通常咱們不須要作額外處理。

一般咱們只須要在 onAccessibilityEvent() 方法中,編寫核心邏輯便可,其餘的方法,只是輔助使用。

2.2 配置輔助模式

當建立一個 AccessibilityService 以後,咱們還須要對其進行一些基本的配置,不然在系統設置的「無障礙」中,是看不到咱們編寫的服務的。

配置 AccessibilityService 有兩種方式,

  • 經過 xml 配置文件
  • 經過 Java 代碼中動態配置。

可是其實有一些屬性是隻能經過 XML 配置文件進行配置的,Java 代碼只是讓某一些配置項更靈活了而已,後面會細說。

一、xml 配置文件

想要使用 XML 配置文件,首先須要建立一個 res/xml 的目錄,並在其內建立一個 xml 文件,文件名隨意無要求,內部定義一個 accessibility-service 標籤,在其中設定 AccessibilityService 的各項配置。例如我這裏建立一個 accessibility_config.xml 的文件,後面會用到這個文件。

XML 配置 AccessibilityService 是咱們一個比較經常使用的配置方法,很是清晰且方便。

<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android" android:accessibilityEventTypes="typeAllMask" android:accessibilityFeedbackType="feedbackAllMask" android:accessibilityFlags="flagReportViewIds" android:canRetrieveWindowContent="true" android:packageNames="com.forwarding.wechat" android:description="@string/accessbility_desc" android:notificationTimeout="100" />
複製代碼

例如上面就是一個常見的配置,若是沒有特殊要求的話,直接複製過去,修改一些個別參數就可使用。

各項屬性的含義:

  • accessibilityEventTypes:監聽的事件類型,例如:typeAllMask 表示所有事件,而 typeViewClicked 表示只監聽點擊事件。
  • accessibilityFeedbackType:監聽事件的反饋模式。
  • canRetrieveWindowContent:是否容許獲取視圖層級的訪問權,若是它被設置爲 false,node.getSource() 方法會調用失敗。
  • accessibilityFlags:指定 Flag,通常用於指定根據 Node 獲取 View ID 的權限。
  • packageNames:開啓監聽的應用包名,能夠指定多個包名,經過逗號「,」分割,不設置此屬性標識全局監聽。
  • description:輔助功能的描述,它會顯示在系統設置的「無障礙」中的描述信息中。
  • notificationTimeout:響應的毫秒數。

這些可配置的參數,系統都提供了可選的配置參數,正常不須要額外定製的時候,使用上面默認的配置便可,若是有定製須要,仍是查閱官方文檔得到最全的介紹。

AccessibilityService:

https://developer.android.com/reference/android/accessibilityservice/AccessibilityService

二、Java 代碼中動態配置

除了 XML 文件配置的方式,咱們還能夠經過重寫 AccessibilityService 的 onServiceConnected() 方法,咱們首先須要構建一個 AccessibilityServiceInfo 對象,經過它的標準 Api 進行配置,再使用 setServiceInfo() 方法將它設置給輔助模式。

onServiceConnected() 會在應用成功鏈接到此輔助服務的時候系統調用,通常在其中作一些初始化的操做便可。

override fun onServiceConnected() {
	super.onServiceConnected()
    var serviceInfo = AccessibilityServiceInfo()
    serviceInfo.eventTypes = AccessibilityEvent.TYPES_ALL_MASK
    serviceInfo.feedbackType = AccessibilityServiceInfo.FEEDBACK_ALL_MASK
    serviceInfo.notificationTimeout = 100
    serviceInfo.packageNames = arrayOf("com.forwarding.wechat")
    serviceInfo.flags =  AccessibilityServiceInfo.FLAG_REPORT_VIEW_IDS
    setServiceInfo(serviceInfo)
}
複製代碼

這裏提供的例子,其實和前面使用 XML 配置的效果一直。推薦使用 XML 的配置方式,會更清晰且靈活,並且像 description 這種屬性,在 AccessibilityServiceInfo 中,並無提供有效的相似 setDescription() 方法,這一點也確實是設計如此,畢竟服務沒有運行,就不存在描述信息,在系統設置的「無障礙」頁面,就讀取不到。

也就是說即使是使用 setServiceInfo() 方法動態設置,也逃不脫使用 XML 配置文件的方式,我仍是強烈建議都使用 XML 配置文件的方式配置輔助服務,主要是爲了省事。

2.3 清單文件中註冊服務

本質上 AccessibilityService 仍是一個 Service,使用它咱們還須要在清單文件中配置它。

<service android:label="承香墨影的輔助工具" android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE" android:name=".WeForwardServer">
    <intent-filter>
        <action android:name="android.accessibilityservice.AccessibilityService"/>
    </intent-filter>
    <meta-data android:name="android.accessibilityservice" android:resource="@xml/accessibility_config"/>
</service>
複製代碼

這就是一個標準的 Service,其中 label 會被解析在系統設置的「輔助模式中顯示」,而 intent-filtermeta-data 按照格式寫就行了,沒什麼緣由。

meta-data 中,經過 android:resource 屬性指定的就是咱們在第二步編輯的配置文件路徑,指定它就行了。

2.4 開啓輔助模式

以上步驟都完成以後,你就能夠在系統的「無障礙」設置裏,看到你編寫的輔助模式的開關了。

默認爲關閉狀態,打開它的時候,你會收到一個警告彈窗,說明當前你正在開啓一個無障礙的服務,它有哪些權限,這個對話框,咱們是控制不了的。

open-accessibility

注意這裏的 Title 就是清單文件裏配置的 android:label ,而描述就是 XML 配置文件裏的 android:description 信息。

當你在系統設置裏,能看到此開關的時候,就說明你的輔助模式的服務,配置的沒問題了,接下來就要思考如何使用它。

3、編寫邏輯代碼

前面提到,在 AccessibilityService 裏,咱們最須要關注的就是 onAccessibilityEvent() 方法,它會在咱們監聽的事件發生的時候,被系統回調,並傳遞過來該事件相關的信息。

接下來咱們看看如何在 onAccessibilityEvent() 回調方法裏,編寫具體的邏輯。

接下來 "程序員思惟" 要上線了,把大象關冰箱,須要幾步。咱們接下來來拆分輔助模式的步驟。

  1. 判斷事件,onAccessibilityEvent() 會被回調屢次,而咱們只須要處理咱們關心的事件,其餘的忽略過濾掉便可。
  2. 找到須要控制的關鍵節點(Node),以便以後進行控制。
  3. 對關鍵節點,發送對於的操做事件,以便完成咱們的步驟。
  4. 回收資源,防止資源泄露。

很簡單對不對,接下來咱們細細的說下,這些步驟相關的方法和屬性。

3.1 判斷事件

onAccessibilityEvent() 被系統回調的時候,同時也會傳遞過來一個 AccessibilityEvent 對象,它其中包含了不少與當前事件相關的信息,有興趣能夠看看源碼,咱們這裏只關注最須要的幾個屬性。

一、eventType 判斷事件類型

經過 eventType 來判斷事件的類型,咱們能夠利用 getEventType() 方法獲取到它。

這些事件都很好辨認,例如:TYPE_NOTIFICATION_STATE_CHANGED 是一個窗口 View 發生了變化,TYPE_VIEW_CLICKED 是某個 View 發生了一次點擊事件等等。

二、packageName 判斷事件發生的 App

經過 getPackageName() 方法,判斷出事件發生在那個 App 裏的。

三、className 判斷當前發生事件的是那個類

經過 getClassName() 判斷當前發生事件的是那個類,例如 頁面的顯示,className 可能指向一個 Activity,一個按鈕的點擊,className 可能指向的是一個 Button,這些都是根據實際場景區分的。

四、text 判斷當前事件觸發源上的 Text

經過 getText() 獲取當前事件源的 text 屬性,多是 TextView 的 Text,也多是 Activity 的 Label 屬性,依然是根據實際狀況區分。

通常咱們能夠經過以上幾種方式,猜想是不是咱們須要監聽的事件,下一步就是咱們找到咱們要操做的源。

3.2 找到待控制的關鍵節點(Node)

一般咱們是使用輔助模式去操做頁面上的某個元素,那這一步,就是爲了找到它。

在輔助模式下,頁面上的每一個元素,其實都是一個個 AccessibilityNodeInfo 節點,它是一個相似樹形的結構,其內和咱們真實 App 內的佈局層級是一致的,可是並不能將它單純的理解成一個 ViewTree。

既然是樹形結構,咱們首先要獲取到根節點的 NodeInfo,能夠經過如下兩個方式獲取:

  • event.getSource()
  • getRootInActiveWindow()

這兩個方法都會返回一個 AccessibilityNodeInfo 對象。getSource() 是AccessibilityEvent 的方法,它可用的前提是前面配置 android:canRetrieveWindowContent 的時候,被設置爲 True。因此我推薦使用 getRootInActiveWindow() 方法來獲取。這兩個方法仍是略微有些差別,有興趣能夠打斷點看看信息,可是大多數狀況下,對咱們使用者來講是一致的。

得到根節點的 AccessibilityNodeInfo 以後,就能夠經過它找到咱們想操做的關鍵節點,在 AccessibilityNodeInfo 中,提供瞭如下兩個方法來找到關鍵節點。

  • findAccessibilityNodeInfosByViewId(String viewId)
  • findAccessibilityNodeInfosByText(String text)

一個是依賴 ViewId,另一個是依賴 Text 信息。

使用 ViewId 查找關鍵節點是穩妥的方案,而使用 Text 去查找,可能會找不到。

不管經過哪一種方式查找 關鍵節點 ,都是存在能找到多個 NodeInfo 的可能的,因此這兩個方法乾脆的都返回了一個 List<AccessibilityNodeInfo> ,因此須要咱們經過其餘條件再過濾一遍,一般就是經過 Text 信息過濾。

var mNodeInfo = rootInActiveWindow
var listItem = mNodeInfo.findAccessibilityNodeInfosByViewId("com.tencent.mm:id/lp")
for (item in listItem) {
    if (item.text.toString().equals("承香墨影")){
		nodeClick(item)
	}
}
複製代碼

若是是使用 findXxxByText() 的方法的話,還須要注意它實際上不是經過相似 == 或者 equals() 的方法來查找子節點的,而是經過相似 contain() 的方式,因此只要節點的 text 屬性包含查找的內容,都會被找到,這個咱們額外還須要增長判斷條件。

若是這些方法都試過,仍是找不到關鍵節點,能夠經過遍歷的方式查找。

AccessibilityNodeInfo 既然是一個樹狀結構,也提供了咱們遍歷樹的方法。

  • getParent():查找父節點。
  • getChild():返回子節點。
  • getChildCount():當前節點的子節點個數。

經過 getChild()getChildCount() 兩個方法,咱們是能夠對整個 ViewNodeTree 進行遍歷,來找到咱們關注的關鍵節點,這是一個最後的方案,並不推薦使用。

3.3 觸發事件

輔助模式通常都是幫助咱們響應一些事件,而這些事件大致上,能夠分爲兩類。

  • 全局系統事件。
  • View 事件。

對於全局系統事件,其實咱們並不須要第二步找到的關鍵節點。AccessibilityService 提供了一個 performGlobalAction() 方法,咱們能夠經過該方法,操做一些全局的系統事件,例如:模擬返回鍵點擊、模擬 HOME 鍵點擊、鎖屏等等。

// 返回鍵
performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK);
// HOME鍵
performGlobalAction(AccessibilityService.GLOBAL_ACTION_HOME);
複製代碼

這些事件被封裝在 AccessibilityService 中,以 GLOBAL_ 爲前綴,看看屬性說明就懂了。

除了全局系統事件以外,一般咱們就是想操做第二步拿到的關鍵節點。

在 AccessibilityNodeInfo 中,提供了一個 performAction() 的方法,能夠經過該方法,對關鍵節點傳遞一個咱們須要的事件。

這些事件都被定義在 AccessibilityNodeInfo 中,以 ACTION_ 爲前綴定義。例如:ACTION_CLICK 是一個點擊事件,ACTION_SET_TEXT 設置一個輸入。

這裏僅介紹一些比較常見的操做,更多的操做也是相似的使用方式。

1. View 的點擊

找到關鍵節點以後,就能夠發送 AccessibilityNodeInfo.ACTION_CLICK 模擬對這個 View 的點擊操做。

可是有時候它是不生效的,主要緣由是由於你找到的這個關鍵節點,它的 isClickable() 爲 false。

例如微信的這個公衆號分享彈窗,若是咱們想要查找「發送給朋友」,其實最好的辦法是找到這個 TextView 控件所表明的關鍵節點(NodeInfo),而後對它進行點擊。而實際上這個 TextView 是不具備點擊效果的,它的 isClickable() 爲 false。

這個時候能夠想一個折中的方案,去找關鍵節點(NodeInfo)的父節點,再去判斷它是否可點擊,可點擊則點擊它,不然繼續向上找。

private fun nodeClick(node : AccessibilityNodeInfo?){
    var clickNode = node;
    while (clickNode!=null){
        if(clickNode.isClickable){
            clickNode.performAction(AccessibilityNodeInfo.ACTION_CLICK)
            break;
        }
        clickNode = node?.parent
    }
}
複製代碼

雖然 AccessibilityNodeInfo 其實也開放了 setClickable() 方法,可是我不建議操做它,有些時候會拋出一個異常,不太穩定。

2. EditText 輸入文字

對 EditText 輸入文字,最少須要兩個參數,關鍵節點和輸入的文字。這就須要用到 performAction() 的另一個重載方法,它容許額外在傳遞一個 Bundle 來指定參數。

private fun nodeSetText(node : AccessibilityNodeInfo?,text:String){
    var argument = Bundle()
    argument.putString(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE,text)
    node?.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT,argument)
}
複製代碼

全部支持定義的額外參數,都被定義在 AccessibilityNodeInfo 中,並以 ACTION_ARGUMENT_ 爲前綴定義。

3. ListView 的滾動

AccessibilityNodeInfo 其實只能操做當前屏幕下可見的 節點,因此碰上 ListView 或者 RecycleView 這種列表,就須要對它進行滾動。

滾動的事件有兩種:

  • ACTION_SCROLL_FORWARD
  • ACTION_SCROLL_BACKWARD
private fun nodeScrollList(node : AccessibilityNodeInfo?){
    node?.performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD)
}
複製代碼

一個前進一個後退,足夠使用了。

3.4 回收資源

在使用完 AccessibilityNodeInfo 以後,別忘了還須要調用 recycle() 方法,釋放資源。

nodeInfo.recycle();
複製代碼

4、小結

輔助模式如何使用,到如今已經講解的很是清楚了,後面基本上就是靠本身的想象力來作小功能了。

利用輔助模式,發揮想象力,你也能夠作出不少有意思的功能。


公衆號後臺回覆成長『成長』,將會獲得我準備的學習資料,也能回覆『加羣』,一塊兒學習進步;你還能回覆『提問』,向我發起提問。

推薦閱讀:

相關文章
相關標籤/搜索