去年寫過微信搶紅包插件的實現,可是今年春節的時候發現微信更新以後本身寫的插件居然會停在開紅包的頁面沒法繼續向下執行,debug以後發現問題是微信團隊把開紅包按鈕的文本內容如今改爲了一張圖片,致使我使用findAccessibilityNodeInfosByText()找不到有效的子節點,也就沒法實現模擬點擊去打開紅包。
因而乎我開始嘗試經過獲取指定控件的ID去實現,迅速打開IDE,使用Android Device Monitor查看開紅包按鈕的控件id,後面會附上使用方法,而後使用findAccessibilityNodeInfosByViewId()來獲取開紅包按鈕的節點,結果固然是能夠的。
可是這就帶來了另外一個問題:若是微信每次發版都修改這個按鈕的控件id,那個人插件也就只能每次都跟隨着修改代碼才能正常使用,事實也證實微信的確是每次發版都會修改此控件的id值。
針對這個問題我目前的作法是開一個ArrayList記錄微信開紅包button所使用過的id值,而後去遍歷id值經過findAccessibilityNodeInfosByViewId()獲取節點,固然用map存儲id值及其對應微信版本號用來作版本兼容會更好些,誰讓我懶呢,懶得去獲取微信版本號。固然,還有種暴力的方法,就是遍歷開紅包頁面的節點樹並模擬點擊其下的每個能點擊的button,由於其實界面裏能點擊的就只有關閉按鈕和開紅包按鈕,但關閉按鈕實際上是個imageView而不是button。
坑老是一個接一個,在最近的微信版本更新後,我發現不只僅是控件id會發生改變,就連某些activity的名稱都被修改了,以及聊天頁面對消息推送的處理方式也變了,適配的代碼我已經更新到github。
之後微信紅包若是還有其餘修改,我會把適配後的代碼直接更新到github的demo,因此下方的代碼片不必定是最新的,感興趣的同窗能夠上github去star一下,demo地址。固然適配過程當中遇到的問題我仍是會記錄在這裏。node
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便可查看android
在manifest中的配置:git
<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資源文件:github
<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:接受事件的時間間隔api
固然,除了以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的自動安裝/批量安裝,去學習吧,騷年!ide