Android自動化測試中AccessibilityService獲取控件信息
原著出處(這里順便對先驅(qū)表示感謝,省得我多走彎路,所以這里直接轉載過來,我們沒有必要做重新造輪子的事情,既然先驅(qū)們已經(jīng)幫我們把輪子造好了):http://blog.csdn.net/itfootball/article/details/21953763
AccessibilityService為一個響應用戶發(fā)送AccessibilityEvent事件的服務類,主要用作對于一些輔助功能的實現(xiàn)中。對于某些方面有缺陷的人群,可以通過輔助功能反饋給用戶。
AccessibilityService的介紹,網(wǎng)絡上有很多,我就不多做介紹了。我把怎么實現(xiàn)它跟大家分享一下,以及怎么把它跟Android自動化測試扯上關系的學習過程給大家介紹一下。
第一步 編寫輔助程序
創(chuàng)建一個Android項目,創(chuàng)建一個服務類,繼承AccessibilityService。
- import android.accessibilityservice.AccessibilityService;
- import android.accessibilityservice.AccessibilityServiceInfo;
- import android.annotation.SuppressLint;
- import android.util.Log;
- import android.view.accessibility.AccessibilityEvent;
- import android.view.accessibility.AccessibilityNodeInfo;
-
- @SuppressLint("NewApi")
- public class MyAccessibility extends AccessibilityService {
- private static final String TAG = "MyAccessibility";
- String[] PACKAGES = { "com.android.settings" };
-
- @Override
- protected void onServiceConnected() {
- Log.i(TAG, "config success!");
- AccessibilityServiceInfo accessibilityServiceInfo = new AccessibilityServiceInfo();
-
- accessibilityServiceInfo.eventTypes = AccessibilityEvent.TYPES_ALL_MASK;
- accessibilityServiceInfo.feedbackType = AccessibilityServiceInfo.FEEDBACK_SPOKEN;
- accessibilityServiceInfo.notificationTimeout = 1000;
- setServiceInfo(accessibilityServiceInfo);
- }
-
- @SuppressLint("NewApi")
- @Override
- public void onAccessibilityEvent(AccessibilityEvent event) {
-
- int eventType = event.getEventType();
- String eventText = "";
- Log.i(TAG, "==============Start====================");
- switch (eventType) {
- case AccessibilityEvent.TYPE_VIEW_CLICKED:
- eventText = "TYPE_VIEW_CLICKED";
- break;
- case AccessibilityEvent.TYPE_VIEW_FOCUSED:
- eventText = "TYPE_VIEW_FOCUSED";
- break;
- case AccessibilityEvent.TYPE_VIEW_LONG_CLICKED:
- eventText = "TYPE_VIEW_LONG_CLICKED";
- break;
- case AccessibilityEvent.TYPE_VIEW_SELECTED:
- eventText = "TYPE_VIEW_SELECTED";
- break;
- case AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED:
- eventText = "TYPE_VIEW_TEXT_CHANGED";
- break;
- case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:
- eventText = "TYPE_WINDOW_STATE_CHANGED";
- break;
- case AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED:
- eventText = "TYPE_NOTIFICATION_STATE_CHANGED";
- break;
- case AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_END:
- eventText = "TYPE_TOUCH_EXPLORATION_GESTURE_END";
- break;
- case AccessibilityEvent.TYPE_ANNOUNCEMENT:
- eventText = "TYPE_ANNOUNCEMENT";
- break;
- case AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_START:
- eventText = "TYPE_TOUCH_EXPLORATION_GESTURE_START";
- break;
- case AccessibilityEvent.TYPE_VIEW_HOVER_ENTER:
- eventText = "TYPE_VIEW_HOVER_ENTER";
- break;
- case AccessibilityEvent.TYPE_VIEW_HOVER_EXIT:
- eventText = "TYPE_VIEW_HOVER_EXIT";
- break;
- case AccessibilityEvent.TYPE_VIEW_SCROLLED:
- eventText = "TYPE_VIEW_SCROLLED";
- break;
- case AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED:
- eventText = "TYPE_VIEW_TEXT_SELECTION_CHANGED";
- break;
- case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED:
- eventText = "TYPE_WINDOW_CONTENT_CHANGED";
- break;
- }
- eventText = eventText + ":" + eventType;
- Log.i(TAG, eventText);
- Log.i(TAG, "=============END=====================");
- }
-
- @Override
- public void onInterrupt() {
-
-
- }
-
- }
該服務類繼承了3個方法:
1.onServiceConnected():綁定服務所用方法,一些初始化的操作放在這里面。
2.onAccessibilityEvent(AccessibilityEvent event):響應AccessibilityEvent的事件,在用戶操作的過程中,系統(tǒng)不斷的發(fā)送sendAccessibiltyEvent(AccessibilityEvent event);然后通過onAccessibilityEvent()可以捕捉到該事件,然后分析。
3.public void onInterrupt():打斷獲取事件的過程,本例中暫不適用。
在onServiceConnected()我們做了一些初始化的工作,通過AccessibilityServiceInfo設置了AccessibilityService的一些參數(shù)
//ccessibilityServiceInfo.packageNames = PACKAGES:響應某個應用的時間,包名為應用的包名;可以用String[]對象傳入多包。如果不設置,默認響應所有應用的事件。
ssibilityServiceInfo.eventTypes = AccessibilityEvent.TYPES_ALL_MASK:響應時間的類型,事件分很多種:單擊、長按、滑動等,需要指定,我設置了所有事件都響應:TYPES_ALL_MASK。
cessibilityServiceInfo.feedbackType = AccessibilityServiceInfo.FEEDBACK_SPOKEN:設置回饋給用戶的方式,是語音播出還是振動。這個我們稍后嘗試配置一些TTS引擎,讓它發(fā)音。
cessibilityServiceInfo.notificationTimeout = 1000:看意思就能明白,響應時間的設置。
接下來我們要在配置文件AndroidManifest.xml配置該服務:
- <?xml version="1.0" encoding="utf-8"?>
- <manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="com.spreadtrum.accessibilityservice"
- android:versionCode="1"
- android:versionName="1.0" >
-
- <uses-sdk
- android:minSdkVersion="8"
- android:targetSdkVersion="17" />
-
- <application
- android:allowBackup="true"
- android:icon="@drawable/ic_launcher"
- android:label="@string/app_name"
- android:theme="@style/AppTheme" >
- <service
- android:name=".MyAccessibility"
- android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE" >
- <intent-filter>
- <action android:name="android.accessibilityservice.AccessibilityService" />
- </intent-filter>
- </service>
- </application>
-
- </manifest>
這些設置很重要,因為AccessibilityService服務不是用戶去開啟的。我們無法用startService()方法啟動它,所以我連activity都沒寫。因為它是通過該配置讓系統(tǒng)自動識別的,識別成功后,就會在我們手機里面看到該服務,然后需要手動開啟該服務(所以千萬別粗心寫錯,我就是因為少寫了一個單詞,然后安裝上app,一直沒反應,它也不報錯,害我找錯找了半天,后來發(fā)現(xiàn)是自己的低級錯誤。)。
現(xiàn)在我們就可以將應用部署到手機上,部署成功后我們點:設置->輔助功能。找到我們的服務,如圖:
看到我們的應用會顯示在其中,默認是關閉的;如果你在此沒看見你的服務,那么你應該核對一下你的配置文件,確保配置正確;點進去,打開該服務。
然后我們就可以隨意在你的手機設備上操作,看看log輸出:
我們就會看到我們點擊的事件,被服務在后臺捕捉到了。接下來,我們嘗試著從我們捕捉到的時間中獲得我們有用的時間,AccessibilityEvent API 中有一個getSource()方法。得到的是AccessibilityNodeInfo,試著獲得這個對象。我們看看是什么東西。
第二步 獲得控件信息
在調(diào)用這個getSource()方法時,需要事先配置一下,也就是需要android:canRetrieveWindowContent="true",這個屬性是API14(android4.0)給出的,它的配置方法受限于需要采用XML初始化的方式配置AccessibilityService,也就是我們現(xiàn)在不在onServiceConnected()里配置初始化信息,改在xml里實現(xiàn)。那么我們現(xiàn)在就要將上面的程序修改一下。首先,刪除onServiceConnected()的配置(因為getSource()不是所有的操作都有該方法,所以得在特定的方法中實現(xiàn)該方法,我們選擇在TYPE_VIEW_CLICKED里實現(xiàn)),修改后的服務類如下:
- package com.spreadtrum.accessibilityservice;
-
- import android.accessibilityservice.AccessibilityService;
- import android.accessibilityservice.AccessibilityServiceInfo;
- import android.annotation.SuppressLint;
- import android.util.Log;
- import android.view.accessibility.AccessibilityEvent;
- import android.view.accessibility.AccessibilityNodeInfo;
-
- @SuppressLint("NewApi")
- public class MyAccessibility extends AccessibilityService {
- private static final String TAG = "MyAccessibility";
- String[] PACKAGES = { "com.android.settings" };
-
- @Override
- protected void onServiceConnected() {
- Log.i(TAG, "config success!");
- }
-
- @SuppressLint("NewApi")
- @Override
- public void onAccessibilityEvent(AccessibilityEvent event) {
-
- int eventType = event.getEventType();
- String eventText = "";
- switch (eventType) {
- case AccessibilityEvent.TYPE_VIEW_CLICKED:
- Log.i(TAG, "==============Start====================");
- eventText = "TYPE_VIEW_CLICKED";
- AccessibilityNodeInfo noteInfo = event.getSource();
- Log.i(TAG, noteInfo.toString());
- Log.i(TAG, "=============END=====================");
- break;
- case AccessibilityEvent.TYPE_VIEW_FOCUSED:
- eventText = "TYPE_VIEW_FOCUSED";
- break;
- case AccessibilityEvent.TYPE_VIEW_LONG_CLICKED:
- eventText = "TYPE_VIEW_LONG_CLICKED";
- break;
- case AccessibilityEvent.TYPE_VIEW_SELECTED:
- eventText = "TYPE_VIEW_SELECTED";
- break;
- case AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED:
- eventText = "TYPE_VIEW_TEXT_CHANGED";
- break;
- case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:
- eventText = "TYPE_WINDOW_STATE_CHANGED";
- break;
- case AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED:
- eventText = "TYPE_NOTIFICATION_STATE_CHANGED";
- break;
- case AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_END:
- eventText = "TYPE_TOUCH_EXPLORATION_GESTURE_END";
- break;
- case AccessibilityEvent.TYPE_ANNOUNCEMENT:
- eventText = "TYPE_ANNOUNCEMENT";
- break;
- case AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_START:
- eventText = "TYPE_TOUCH_EXPLORATION_GESTURE_START";
- break;
- case AccessibilityEvent.TYPE_VIEW_HOVER_ENTER:
- eventText = "TYPE_VIEW_HOVER_ENTER";
- break;
- case AccessibilityEvent.TYPE_VIEW_HOVER_EXIT:
- eventText = "TYPE_VIEW_HOVER_EXIT";
- break;
- case AccessibilityEvent.TYPE_VIEW_SCROLLED:
- eventText = "TYPE_VIEW_SCROLLED";
- break;
- case AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED:
- eventText = "TYPE_VIEW_TEXT_SELECTION_CHANGED";
- break;
- case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED:
- eventText = "TYPE_WINDOW_CONTENT_CHANGED";
- break;
- }
- }
-
- @Override
- public void onInterrupt() {
-
-
- }
-
- }
然后我們要在res文件寫創(chuàng)建一個XML文件夾,里面新建accessibility.xml配置文件。
在accessibility.xml配置一些屬性:
- <?xml version="1.0" encoding="UTF-8"?>
- <accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
- android:accessibilityEventTypes="typeViewClicked|typeViewFocused"
- android:accessibilityFeedbackType="feedbackSpoken"
- android:notificationTimeout="100"
- android:canRetrieveWindowContent="true"
- />
重點就是標紅的這一句,這樣我們就能在程序中得到AccessibilityNodeInfo對象。然后在AndroidManifest.xml配置文件里引用該XML文件:
- <?xml version="1.0" encoding="utf-8"?>
- <manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="com.spreadtrum.accessibilityservice"
- android:versionCode="1"
- android:versionName="1.0" >
-
- <uses-sdk
- android:minSdkVersion="8"
- android:targetSdkVersion="17" />
-
- <application
- android:allowBackup="true"
- android:icon="@drawable/ic_launcher"
- android:label="@string/app_name"
- android:theme="@style/AppTheme" >
- <service
- android:name=".MyAccessibility"
- 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/accessibility" />
- </service>
- </application>
-
- </manifest>
然后我們部署應用到手機設備,看輸出情況:
注意:
在你配置成功后,有可能會報空指針錯誤,但有可能你已經(jīng)配置正確,只是你點的對象有問題。因為在API文檔中有一句話:如果發(fā)起的窗口事件仍然是活動的窗口,該調(diào)用將會返回一個對象,否則,會返回null!所以首先確保你剛開始點擊的那個控件所在的窗口要是活動的狀態(tài),不然也會報null。例如:我剛開始點擊HOME界面的所有應用按鈕進入到應用的列表,這時候就會報錯,如果你是點擊通訊錄觸發(fā)服務的,就沒問題;原理我也不太清楚,所以不敢大放厥詞,反正在一個應用中來回點是沒問題的;這個隨著深入的了解后,相信會有答案的。
第三步 獲得所有窗體控件
已經(jīng)做到了獲得控件信息,但是AccessibilityEvent.getSource()得到的是被點擊的單體對象。我們需要獲得是整個窗口的對象,在API16中AccessibilityService新引入的方法getRootInActiveWindow()可以滿足我們的要求,所以我們用這個方法得到整個窗口,然后遍歷得到所有子節(jié)點。
- AccessibilityNodeInfo rowNode = getRootInActiveWindow();
- if (rowNode == null) {
- Log.i(TAG, "noteInfo is null");
- return;
- } else {
- recycle(rowNode);
- }
- Log.i(TAG, "==============================================");
其中循環(huán)的方法recycle():
- public void recycle(AccessibilityNodeInfo info) {
- if (info.getChildCount() == 0) {
- Log.i(TAG, "child widget----------------------------" + info.getClassName());
- Log.i(TAG, "showDialog:" + info.canOpenPopup());
- Log.i(TAG, "Text:" + info.getText());
- Log.i(TAG, "windowId:" + info.getWindowId());
- } else {
- for (int i = 0; i < info.getChildCount(); i++) {
- if(info.getChild(i)!=null){
- recycle(info.getChild(i));
- }
- }
- }
- }
打印輸出的信息有:控件名、是否點擊彈出對話框、窗口ID;你還可以查看API里的方法,獲得你想要的信息。
部署到手機上,測試結果如下:
總結
三種方式學習完了,對比一下各種方式的優(yōu)缺點!