>>長路漫漫立志行高遠,寒夜漆漆心勇敢獨行。 --致青春
前言:
朋友的一個奇思妙想,促使我接觸了 AccessibilityService ,這是個什麼東西呢,我們看看申請權限時系統怎麼提醒的
意思吧,很清楚,就是這項服務開啓之後,您的屏幕操作動作,甚至是操作的屏幕上的視圖的內容,這貨都能獲取到,發揮一下想象力,可以搞事情!(微信自動搶紅包等自動化軟件就是用的這貨)。
正文:
STEP 1 Android studio 新建項目
首先,鑑於此功能一般有開發經驗的人員纔會想到或者用到,所以,android studio 那一堆新建項目之類的就直接跳過了。
STEP 2 項目配置
1、新建服務。比如:
public class AccessibilityServiceHelper extends AccessibilityService {
private static final String TAG = "AccessibilityServiceHelper";
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
try {
if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
} else if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) {
} else if (event.getEventType() == AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED) {
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void onInterrupt() {
}
}
根據本人測試的經驗解釋一下以上監聽的三種狀態(可監聽的狀態不止這三種,我這裏只用到了這三種,具體可看AccessibilityEvent 類下的各個狀態,基本上見名知意):
- event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:這種狀態是頁面切換纔會觸發,我測試出來的是,必須是Activity切換纔會觸發,如果Activity中包含ViewPager,ViewPager中是Fragment,那麼切換Fragment是不會觸發此狀態的。
- event.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED:這種狀態是監聽窗口內容變化,看名字意思是窗口內容變化纔會觸發,但是我這裏實測的是即使沒看到變化也會觸發,不知道是不是有肉眼看不到的變化。
- event.getEventType() == AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED:通知狀態變化,實測是當有彈窗和有通知欄消息時都會觸發。
2、新建xml配置。app--res 下新建 xml 文件夾,xml文件夾下新建 accessibility_service_config.xml,這個文件名隨意起。內容示例如下:
<?xml version="1.0" encoding="utf-8"?>
<accessibility-service
xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeAllMask"
android:accessibilityFeedbackType="feedbackGeneric"
android:canRetrieveWindowContent="true"
android:description="@string/accessibility_service_description"
android:notificationTimeout="100"
android:packageNames="com.xxx.xxx"/>
<!--accessibilityEventTypes:表示該服務對界面中的哪些變化感興趣,即哪些事件通知,比如窗口打開,
滑動,焦點變化,長按等.具體的值可以在AccessibilityEvent類中查到,如typeAllMask表示接受所有的事
件通知.-->
<!--accessibilityFeedbackType:表示反饋方式,比如是語音播放,還是震動-->
<!--canRetrieveWindowContent:表示該服務能否訪問活動窗口中的內容.也就是如果你希望在服務中獲取
窗體內容的化,則需要設置其值爲true.-->
<!--notificationTimeout:接受事件的時間間隔,通常將其設置爲100即可.-->
<!--packageNames:表示對該服務是用來監聽哪個包的產生的事件-->
各個屬性註釋都有做解釋。(此文件的配置也可在 AccessibilityServiceHelper 的 onServiceConnected 進行配置)
3、註冊服務、配置服務。(AndroidManifest.xml 中註冊服務就不用我說了吧) 示例內容如下:
<service
android:name="com.xxx.xxx.AccessibilityServiceHelper"
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/accessibility_service_config"/>
</service>
CSDN這個代碼編輯器太不智能了,都不會縮進,還得我一行一行調,心累。
注意:meta-data部分,android:resource 指向的就是上面 第 2 小步中的xml文件。
STEP 3 正主登場
1、判斷當前服務開啓狀態。
/**
* 檢測輔助功能是否開啓<br>
*/
public static boolean isAccessibilitySettingsOn(Context mContext) {
int accessibilityEnabled = 0;
final String service = mContext.getPackageName() + "/" + 替換爲自定義的服務類名.class.getCanonicalName();
try {
accessibilityEnabled = Settings.Secure.getInt(mContext.getApplicationContext().getContentResolver(),
android.provider.Settings.Secure.ACCESSIBILITY_ENABLED);
} catch (Settings.SettingNotFoundException e) {
}
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;
}
}
}
} else {
Log.v(TAG, "***ACCESSIBILITY IS DISABLED***");
}
return false;
}
2、跳轉至設置也開啓服務。
Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);
startActivity(intent);
3、獲取屏幕根節點。
在 AccessibilityServiceHelper 中的
@Override
public void onAccessibilityEvent(AccessibilityEvent event){
}
可以通過如下代碼獲取當前屏幕根節點:
AccessibilityNodeInfo accessibilityNodeInfo = accessibilityService.getRootInActiveWindow();
TIP 1: AccessibilityNodeInfo 有以下幾個方法,用於組件的查找:
//通過屏幕顯示的內容,比如:下一步、完成等字眼,獲取包含該文本的所有節點
List<AccessibilityNodeInfo> nodeInfoList = rootNode.findAccessibilityNodeInfosByText(text);
//通過屏幕顯示的view組件的id查找節點,不過前提是要知道該view的節點id,可以通過android SDK -- tools -- monitor.bat 啓動 Android Device Monitor 通過捕獲屏幕組件獲取各個組件的信息,不過這傢伙太TM不好用了,點100次也不一定能成功一次,所以建議在 AccessiblityServicerHelper 中遍歷根節點,找到你要找的組件的節點,然後查看其信息,進行下一步操作
List<AccessibilityNodeInfo> nodeInfoList = accessibilityNodeInfo.findAccessibilityNodeInfosByViewId(id);
有以下幾個方法,用於組件的操作:
//對當前nodeInfo執行單擊操作
nodeInfo.performAction(AccessibilityNodeInfo.ACTION_CLICK);
//對當前nodeInfo執行長按操作
nodeInfo.performAction(AccessibilityNodeInfo.ACTION_LONG_CLICK);
//對當前nodeInfo執行復制操作
nodeInfo.performAction(AccessibilityNodeInfo.ACTION_COPY);
//對當前nodeInfo執行剪切操作
nodeInfo.performAction(AccessibilityNodeInfo.ACTION_CUT);
//對當前nodeInfo執行粘貼操作
nodeInfo.performAction(AccessibilityNodeInfo.ACTION_PASTE);
僅列出常用的幾個,在 AccessibilityNodeInfo 中也可以找到更多操作指令。
TIP 2: AccessibilityService 也有一個本人比較常用的方法,用於執行全局返回操作:
//執行全局返回操作
service.performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK);
在 AccessibilityService 中也可以找到更多操作指令。
4、通知內容的捕獲,就是 event.getEventType() == AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED 時獲取的內容 。
//捕獲的信息都在集合裏了,斷點看一下就知道都有啥了,後續內容處理不再贅述
List<CharSequence> text = event.getText();
5、獲取 WebView 內容。
@Override
protected void onServiceConnected() {
super.onServiceConnected();
AccessibilityServiceInfo serviceInfo = new AccessibilityServiceInfo();
serviceInfo.eventTypes = AccessibilityEvent.TYPES_ALL_MASK;
serviceInfo.feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC;
serviceInfo.packageNames = new String[]{"com.unionpay"};// 監控的app
serviceInfo.notificationTimeout = 100;
serviceInfo.flags = serviceInfo.flags | AccessibilityServiceInfo.FLAG_REQUEST_ENHANCED_WEB_ACCESSIBILITY;
setServiceInfo(serviceInfo);
}
重點:serviceInfo.flags = serviceInfo.flags | AccessibilityServiceInfo.FLAG_REQUEST_ENHANCED_WEB_ACCESSIBILITY;
這個設置可以讓 AccessibilityService 能捕獲到WebView,只有捕獲到了才能拿裏面的內容。
其他設置可以保持和 accessibility_service_config.xml 中的各項設置一致,不設置好像會讓 accessibility_service_config.xml 對應屬性失效,建議設置一遍。
下面是對獲取到的WebView節點的實際操作:
//這裏我是根據頁面標題定位獲取的,因爲我不知道WebView的相關信息,但是我遍歷根節點時拿到了頁面標題的信息,然後通過節點的 getParent()方法往上找其父節點,一直找到頁面根節點,然後再往下找各個子節點,直到找到需要的那個WebView節點
AccessibilityNodeInfo text = AccessibilityUtils.findViewId(this, "com.xxx.xxx:id/tv_title").get(0);
//找到父節點爲一個FrameLayout
AccessibilityNodeInfo framelayout = text.getParent();
//發現FrameLayout下的第三個字節點爲WebView,但此WebView節點的子節點並不是我要的內容,繼續往下找
AccessibilityNodeInfo webview = framelayout.getChild(2);
//又是一個WebView,仍不是目標view
AccessibilityNodeInfo textwebview = webview.getChild(0);
//繼續深入找
AccessibilityNodeInfo textwebview1 = textwebview.getChild(0);
//終於不是WebView了,而是一個View結合,其下有很多子view,而子view的信息正是我要找的
AccessibilityNodeInfo views = textwebview1.getChild(0);
//獲取子view的內容
String content = views.getChild(0).getContentDescription().toString();
註釋已很詳細,每個要監聽的app頁面內容都是不一樣的,這裏只是一個示例,不要死板的複製,這裏只是一個查找思路和方法。
至此,AccessibilityService 的相關操作就結束了,可能還有沒有說到的地方,但,我相信上面所講的已經能滿足大部分需求了。
也算是嘔心瀝血了,認真看完必有收穫。