去年寫過微信搶紅包插件的實現,但春節的時候發現微信更新以後個人插件居然會停在開紅包的頁面沒法繼續向下執行,debug以後發現問題是微信的開紅包按鈕如今被改爲了圖片,致使我使用findAccessibilityNodeInfosByText()找不到有效的子節點,也就沒法實現模擬點擊去打開紅包。 因而乎我開始嘗試經過獲取控件ID去實現,迅速打開IDE,使用Android Device Monitor查看開紅包按鈕的控件id,後面會附上使用方法,而後使用findAccessibilityNodeInfosByViewId()來獲取開紅包按鈕的節點,結果固然是能夠的。 可是這就帶來了另外一個問題:若是每次微信發版後這個按鈕控件的id發生變化,那插件也就只能跟隨着修改代碼才能正常使用。 針對這個問題我目前的作法是開一個ArrayList記錄微信開紅包button所使用過的id值,而後去遍歷id值經過findAccessibilityNodeInfosByViewId()獲取節點,固然用map存儲id值及其對應微信版本號用來作版本兼容會更好些,誰讓我懶呢,懶得去獲取微信版本號。固然,還有種暴力的方法,就是遍歷開紅包頁面的節點樹並模擬點擊其下的每個能點擊的button,由於其實界面裏能點擊的就只有關閉按鈕和開紅包按鈕,但關閉按鈕實際上是個imageView而不是button。node
坑老是一個接一個,在最近的微信版本更新後,我發現不只僅是控件id會發生改變,插件關注的某些activity類名也開始被引入混淆,以及聊天頁面對消息推送的處理方式也變了,適配的代碼我已經更新到github。 之後微信紅包若是還有其餘修改,我會把適配後的代碼直接更新到github的demo,因此下方的代碼片不必定是最新的,感興趣的同窗能夠上github去star一下,demo地址。固然適配過程當中遇到的問題我仍是會記錄在這裏。android
if ("com.tencent.mm.plugin.luckymoney.ui.LuckyMoneyReceiveUI".equals(event.getClassName())) {
//當前在紅包待開頁面,去拆紅包
getLuckyMoney();
} else if ("com.tencent.mm.plugin.luckymoney.ui.LuckyMoneyDetailUI".equals(event.getClassName())) {
//拆完紅包後看詳細紀錄的界面
openNext("查看個人紅包記錄");
} else if ("com.tencent.mm.ui.LauncherUI".equals(event.getClassName())) {
//在聊天界面,去點中紅包
openLuckyEnvelope();
}
複製代碼
if (eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED && event.getClassName().equals("com.tencent.mm.ui.LauncherUI")) {
//記錄打招呼人數置零
i = 0;
//當前在微信聊天頁就點開發現
openNext("發現");
//而後跳轉到附近的人
openDelay(1000, "附近的人");
} else if (event.getClassName().equals("com.tencent.mm.plugin.nearby.ui.NearbyFriendsUI") && eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
prepos = 0;
//當前在附近的人界面就點選人打招呼
AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();
List<AccessibilityNodeInfo> list = nodeInfo.findAccessibilityNodeInfosByText("米之內");
Log.d("name", "附近的人列表人數: " + list.size());
if (i < (list.size() * page)) {
list.get(i % list.size()).performAction(AccessibilityNodeInfo.ACTION_CLICK);
list.get(i % list.size()).getParent().performAction(AccessibilityNodeInfo.ACTION_CLICK);
} else if (i == list.size() * page) {
//本頁已所有打招呼,因此下滑列表加載下一頁,每次下滑的距離是一屏
for (int i = 0; i < nodeInfo.getChild(0).getChildCount(); i++) {
if (nodeInfo.getChild(0).getChild(i).getClassName().equals("android.widget.ListView")) {
AccessibilityNodeInfo node_lsv = nodeInfo.getChild(0).getChild(i);
node_lsv.performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
page++;
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException mE) {
mE.printStackTrace();
}
AccessibilityNodeInfo nodeInfo_ = getRootInActiveWindow();
List<AccessibilityNodeInfo> list_ = nodeInfo_.findAccessibilityNodeInfosByText("米之內");
Log.d("name", "列表人數: " + list_.size());
//滑動以後,上一頁的最後一個item爲當前的第一個item,因此從第二個開始打招呼
list_.get(1).performAction(AccessibilityNodeInfo.ACTION_CLICK);
list_.get(1).getParent().performAction(AccessibilityNodeInfo.ACTION_CLICK);
}
}).start();
}
}
}
} else if (event.getClassName().equals("com.tencent.mm.plugin.profile.ui.ContactInfoUI") && eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
if (prepos == 1) {
//從打招呼界面跳轉來的,則點擊返回到附近的人頁面
performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK);
i++;
} else if (prepos == 0) {
//從附近的人跳轉來的,則點擊打招呼按鈕
AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();
if (nodeInfo == null) {
Log.w(TAG, "rootWindow爲空");
return;
}
List<AccessibilityNodeInfo> list = nodeInfo.findAccessibilityNodeInfosByText("打招呼");
if (list.size() > 0) {
list.get(list.size() - 1).performAction(AccessibilityNodeInfo.ACTION_CLICK);
list.get(list.size() - 1).getParent().performAction(AccessibilityNodeInfo.ACTION_CLICK);
} else {
//若是遇到已加爲好友的則界面的「打招呼」變爲「發消息",因此直接返回上一個界面並記錄打招呼人數+1 performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK); i++; } } } else if (event.getClassName().equals("com.tencent.mm.ui.contact.SayHiEditUI") && eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { //當前在打招呼頁面 prepos = 1; //輸入打招呼的內容併發送 inputHello(hello); openNext("發送"); } 複製代碼
private void openNotification(AccessibilityEvent event) {
if (event.getParcelableData() == null || !(event.getParcelableData() instanceof Notification)) {
return;
}
//將通知欄消息打開
Notification notification = (Notification) event.getParcelableData();
PendingIntent pendingIntent = notification.contentIntent;
try {
pendingIntent.send();
} catch (PendingIntent.CanceledException e) {
e.printStackTrace();
}
}
複製代碼
private void openNext(String str) {
AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();
if (nodeInfo == null) {
Log.w(TAG, "rootWindow爲空");
Toast.makeText(this, "rootWindow爲空", Toast.LENGTH_SHORT).show();
return;
}
List<AccessibilityNodeInfo> list = nodeInfo.findAccessibilityNodeInfosByText(str);
Log.d("name", "匹配個數: " + list.size());
if (list.size() > 0) {
list.get(list.size() - 1).performAction(AccessibilityNodeInfo.ACTION_CLICK);
list.get(list.size() - 1).getParent().performAction(AccessibilityNodeInfo.ACTION_CLICK);
} else {
Toast.makeText(this, "找不到有效的節點", Toast.LENGTH_SHORT).show();
}
}
複製代碼
private void inputHello(String hello) {
AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();
//找到當前獲取焦點的view
AccessibilityNodeInfo target = nodeInfo.findFocus(AccessibilityNodeInfo.FOCUS_INPUT);
if (target == null) {
Log.d(TAG, "inputHello: null");
return;
}
ClipboardManager clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText("label", hello);
clipboard.setPrimaryClip(clip);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
target.performAction(AccessibilityNodeInfo.ACTION_PASTE);
}
}
複製代碼
在Android Studio中開啓Android Device Monitor,選擇設備後點擊Dump View Hierarchy for UI Automator便可查看git
在manifest中的配置:github
<uses-permission android:name="android.permission.BIND_ACCESSIBILITY_SERVICE" />
複製代碼
<service
android:enabled="true"
android:exported="true"
android:label="@string/app_name"
android:name=".AutoService"
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/envelope_service_config"/>
</service>
複製代碼
meta-data中的xml資源文件:api
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeNotificationStateChanged|typeWindowStateChanged|typeWindowContentChanged"
android:accessibilityFeedbackType="feedbackGeneric"
android:accessibilityFlags=""
android:canRetrieveWindowContent="true"
android:description="@string/app_name"
android:notificationTimeout="100"
android:packageNames="com.tencent.mm,com.huawei.android.launcher" />
複製代碼
其中packageName用於配置你想要監測的包名,若是多個則用逗號隔開 accessibilityEventTypes表示該服務可監測界面中哪些事件類型,如窗口打開,滑動等,具體值可查看api accessibilityFeedbackType:表示反饋方式,好比是語音播放,仍是震動 canRetrieveWindowContent:表示該服務可否訪問活動窗口中的內容 notificationTimeout:接受事件的時間間隔bash
固然,除了以meta-data的方式靜態配置,也可經過在服務啓動時的onServiceConnected()方法中調用setServiceInfo(AccessibilityServiceInfo)進行動態配置。微信
幾種經常使用accessibilityEventType事件類型: TYPE_WINDOW_STATE_CHANGED: 窗口狀態改變事件類型,打開PopupWindow、dialog、menu等 TYPE_NOTIFICATION_STATE_CHANGED: 通知欄事件 TYPE_WINDOW_CONTENT_CHANGED: 窗口中內容改變 TYPE_VIEW_SCROLLED: 控件滑動事件 TYPE_WINDOWS_CHANGED: 顯示窗口改變 TYPE_VIEW_TEXT_CHANGED : editText控件的內容發生改變 TYPE_TOUCH_INTERACTION_START: 用戶開始觸摸屏幕 TYPE_TOUCH_INTERACTION_END: 用戶中止觸摸屏幕併發
其中TYPE_WINDOW_CONTENT_CHANGED 又能夠細分爲4個二級類型: 1.CONTENT_CHANGE_TYPE_SUBTREE: 節點發生增減 2.CONTENT_CHANGE_TYPE_TEXT: 節點文本發生改變 3.CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION: 節點的內容描述發生改變,即控件的contentDescription屬性發生改變 4.CONTENT_CHANGE_TYPE_UNDEFINED: 未定義類型,即除上面三種以外的類型app
接下來,或許你能夠本身嘗試下使用AccessibilityService實現app的自動安裝/批量安裝,去學習吧,騷年! demo地址ide