起因: 最近做的APP中有一個新功能:已知用戶微信號,可點擊直接跳轉到當前用戶微信聊天視窗頁面。 當時第一想法是使用無障礙來做,並且覺得應該不難,只是邏輯有點複雜。沒想到最終踩了好多坑,特地把踩過的坑記錄下來。 實現邏輯: 在APP中點擊按鈕→跳轉到微信界面→模擬點擊微信搜索按鈕→在微信搜索頁面輸入 ...
起因:
最近做的APP中有一個新功能:已知用戶微信號,可點擊直接跳轉到當前用戶微信聊天視窗頁面。
當時第一想法是使用無障礙來做,並且覺得應該不難,只是邏輯有點複雜。沒想到最終踩了好多坑,特地把踩過的坑記錄下來。
實現邏輯:
在APP中點擊按鈕→跳轉到微信界面→模擬點擊微信搜索按鈕→在微信搜索頁面輸入獲取的微信號→模擬點擊查詢到的用戶進入用戶聊天界面。
效果圖:
實現過程:
跳轉微信按鈕點擊事件:
1 jumpButton.setOnClickListener(new View.OnClickListener() { 2 @Override 3 public void onClick(View view) { 4 Intent intent = new Intent(Intent.ACTION_MAIN); 5 ComponentName cmp = new ComponentName("com.tencent.mm", "com.tencent.mm.ui.LauncherUI"); 6 intent.addCategory(Intent.CATEGORY_LAUNCHER); 7 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 8 intent.setComponent(cmp); 9 startActivity(intent); 10 } 11 });
無障礙監聽主要方法:
一些必要的參數:
1 /** 2 * 微信主頁面的“搜索”按鈕id 3 */ 4 private final String SEARCH_ID = "com.tencent.mm:id/ij"; 5 6 /** 7 * 微信主頁面bottom的“微信”按鈕id 8 */ 9 private final String WECHAT_ID = "com.tencent.mm:id/d3t"; 10 11 /** 12 * 微信搜索頁面的輸入框id 13 */ 14 private final String EDIT_TEXT_ID = "com.tencent.mm:id/ka"; 15 16 /** 17 * 微信搜索頁面活動id 18 */ 19 private String SEARCH_ACTIVITY_NAME = "com.tencent.mm.plugin.fts.ui.FTSMainUI"; 20 21 private String LIST_VIEW_NAME = "android.widget.ListView";
微信組件的id之前有博客說過如何獲取,所以在此就不重覆說明瞭。
監聽主要方法:
1 @Override 2 public void onAccessibilityEvent(AccessibilityEvent event) { 3 List<AccessibilityNodeInfo> searchNode = event.getSource().findAccessibilityNodeInfosByViewId(SEARCH_ID); 4 List<AccessibilityNodeInfo> wechatNode = event.getSource().findAccessibilityNodeInfosByViewId(WECHAT_ID); 5 6 if (searchNode.size() > 1) { 7 // 點擊“搜索”按鈕 8 if (searchNode.get(0).getParent().isClickable()) { 9 searchNode.get(0).getParent().performAction(AccessibilityNodeInfo.ACTION_CLICK); 10 return; 11 } 12 } else if (searchNode.size() == 1) { 13 // 如果在“我”頁面,則進入“微信”頁面 14 for (AccessibilityNodeInfo info : wechatNode) { 15 if (info.getText().toString().equals("微信") && !info.isChecked()) { 16 17 if (info.getParent().isClickable()) { 18 info.getParent().performAction(AccessibilityNodeInfo.ACTION_CLICK); 19 return; 20 } 21 break; 22 } 23 } 24 } 25 26 // 當前頁面是搜索頁面 27 if (SEARCH_ACTIVITY_NAME.equals(event.getClassName().toString())) { 28 List<AccessibilityNodeInfo> editTextNode = event.getSource().findAccessibilityNodeInfosByViewId(EDIT_TEXT_ID); 29 30 if (editTextNode.size() > 0) { 31 // 輸入框內輸入查詢的微信號 32 Bundle arguments = new Bundle(); 33 arguments.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, Constant.wechatId); 34 editTextNode.get(0).performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, arguments); 35 } 36 } else if (LIST_VIEW_NAME.equals(event.getClassName().toString())) { 37 // 如果監聽到了ListView的內容改變,則找到查詢到的人,並點擊進入 38 List<AccessibilityNodeInfo> textNodeList = event.getSource().findAccessibilityNodeInfosByText("微信號: " + Constant.wechatId); 39 if (textNodeList.size() > 0) { 40 textNodeList.get(0).getParent().performAction(AccessibilityNodeInfo.ACTION_CLICK); 41 } 42 } 43 44 }
這是最原始的版本,具體邏輯已在註釋中說明。
遇到的坑:
1. 搜索內容無法賦值給搜索框
最開始以為是賦值的方法有問題,但是在調試狀態下能夠賦值成功。因此猜測是因為UI載入太慢的緣故。
在搜索框還沒完全載入完全的時候就進行了賦值,因此賦值不成功。
解決辦法:
在賦值之前停頓300ms,在30行賦值前先停頓300ms。
1 try { 2 Thread.sleep(300); 3 } catch (InterruptedException e) { 4 e.printStackTrace(); 5 }
2. 如何停止監聽?
由於監聽是一直會進行的,因此只要進入了微信頁面就會執行無障礙方法。這是不合理的。理論上應該在點擊按鈕進入微信才開始監聽,而查找到好友之後就停止監聽。
解決辦法:
可以設置全局的變數用來控制監聽。需要在點擊按鈕設置變數值為監聽,而查找到微信好友之後設置為不監聽。
全局變數:
1 public class Constant { 2 3 /** 4 * 判斷是否需要監聽 5 */ 6 public static int flag = 0; 7 8 /** 9 * 微信號 10 */ 11 public static String wechatId; 12 }
按鈕點擊修改flag值:
1 jumpButton.setOnClickListener(new View.OnClickListener() { 2 @Override 3 public void onClick(View view) { 4 Intent intent = new Intent(Intent.ACTION_MAIN); 5 ComponentName cmp = new ComponentName("com.tencent.mm", "com.tencent.mm.ui.LauncherUI"); 6 intent.addCategory(Intent.CATEGORY_LAUNCHER); 7 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 8 intent.setComponent(cmp); 9 startActivity(intent); 10 11 Constant.flag = 1; 12 Constant.wechatId = editText.getText().toString(); 13 } 14 });
根據flag判斷是否需要監聽:
在無障礙服務的監聽方法中開始位置判斷,
1 // 只有從app進入微信才進行監聽 2 if (Constant.flag == 0) { 3 return; 4 }
查詢到結果後修改flag值:
1 // 如果監聽到了ListView的內容改變,則找到查詢到的人,並點擊進入 2 List<AccessibilityNodeInfo> textNodeList = event.getSource().findAccessibilityNodeInfosByText("微信號: " + Constant.wechatId); 3 if (textNodeList.size() > 0) { 4 textNodeList.get(0).getParent().performAction(AccessibilityNodeInfo.ACTION_CLICK); 5 6 // 模擬點擊之後將暫存值置空,類似於取消監聽 7 Constant.flag = 0; 8 Constant.wechatId = null; 9 }
3. 沒查詢到結果如何停止監聽?
想必大家都發現了,上面的處理方法還沒有考慮到未查詢到好友的情況。那麼,未查詢到好友如何停止監聽呢?
最開始想的是找到未查詢頁面,只要知道了什麼情況是未查詢的,那就可以停止監聽了。
但是未查詢到好友的頁面查找比較麻煩,因此想了一個取巧的辦法。
解決辦法:
寫一個線程,兩秒後執行,因為用戶一般在未查詢到結果頁面會停留至少兩秒,兩秒誤操作就停止監聽。
線程實現(線程得是類持有的,而不應該是方法持有的):
1 Handler handler = new Handler(); 2 Runnable runnable = new Runnable() { 3 @Override 4 public void run() { 5 Constant.flag = 0; 6 Constant.wechatId = null; 7 } 8 };
監聽方法內進行線程的開啟操作:
1 // 兩秒後如果還沒有任何的事件,則停止監聽 2 handler.removeCallbacks(runnable); 3 handler.postDelayed(runnable, 2000);
由於無障礙的監聽方法會反覆執行,因此為了保證其正確性,需要保證在最後一次事件才開始計時。
4. 如果在微信其他頁面怎麼辦?
最開始被這個問題難住了。後來產品給了我一個思路,其實很簡單,如果判斷當前頁面並不是微信主頁面的話,就執行全局返回按鈕事件就行。
解決辦法:
如果是頁面改變事件,並且當前頁面不是主頁面也不是搜索頁面(搜索頁面就可以直接搜索了)的話,就執行全局返回鍵。
1 if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED && !LAUNCHER_ACTIVITY_NAME.equals(event.getClassName().toString()) && !SEARCH_ACTIVITY_NAME.equals(event.getClassName().toString())) { 2 // 如果當前頁面不是微信主頁面也不是微信搜索頁面,就模擬點擊返回鍵 3 performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK); 4 return; 5 }
5. 頁面改變UI載入太慢
在解決上述問題時,又遇到了之前遇到的問題,UI載入太慢的問題,因此需要在每次頁面改變事件中都得加上300ms的延遲時間。
解決辦法:
1 // 頁面改變時需要延遲一段時間進行佈局載入 2 if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { 3 try { 4 Thread.sleep(300); 5 } catch (InterruptedException e) { 6 e.printStackTrace(); 7 } 8 }
6. 聊天界面和主頁面是同一個活動
解決了上述問題之後,又遇到了一個新的問題,經常性的返回到聊天頁面就不返回了。
經過調試,發現聊天頁面的活動和微信主頁面的活動是同一個。
解決辦法:
對聊天界面單獨做處理,根據聊天界面左上角UI存在不存在來確定是否為聊天界面。
1 if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED && !LAUNCHER_ACTIVITY_NAME.equals(event.getClassName().toString()) && !SEARCH_ACTIVITY_NAME.equals(event.getClassName().toString())) { 2 // 如果當前頁面不是微信主頁面也不是微信搜索頁面,就模擬點擊返回鍵 3 performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK); 4 return; 5 } else if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED && LAUNCHER_ACTIVITY_NAME.equals(event.getClassName().toString())) { 6 List<AccessibilityNodeInfo> list = event.getSource().findAccessibilityNodeInfosByViewId(USERNAME_ID); 7 if (list.size() > 0) { 8 // 如果是微信主頁面,但是是微信聊天頁面,則模擬點擊返回鍵 9 performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK); 10 return; 11 } 12 }
其中USRENAME_ID為左上角備註部分的UIid。
7. 搜索不到結果時,發現他在搜索結果頁面亂跳
經排查,發現搜索結果頁面中的搜索佈局提示佈局id和首頁面的搜索按鈕id一致,因此就執行了點擊搜索按鈕的方法。
解決辦法:
對於搜索按鈕頁面(主頁面)也要進行單獨判斷,由於主頁面一定有ViewPage佈局,因此只要找到ViewPage那就證明是在主頁面。
1 List<AccessibilityNodeInfo> searchNode = event.getSource().findAccessibilityNodeInfosByViewId(SEARCH_ID); 2 List<AccessibilityNodeInfo> wechatNode = event.getSource().findAccessibilityNodeInfosByViewId(WECHAT_ID); 3 List<AccessibilityNodeInfo> viewPageNode = event.getSource().findAccessibilityNodeInfosByViewId(VIEW_PAGE_ID); 4 5 Log.e(TAG, "searchNode:" + searchNode.size()); 6 Log.e(TAG, "viewPageNode:" + viewPageNode.size()); 7 8 // 由於搜索控制項在多個頁面都有,所以還得判斷是否在主頁面 9 if (searchNode.size() > 1 && viewPageNode.size() > 0) { 10 // 點擊“搜索”按鈕 11 if (searchNode.get(0).getParent().isClickable()) { 12 searchNode.get(0).getParent().performAction(AccessibilityNodeInfo.ACTION_CLICK); 13 return; 14 } 15 } else if (searchNode.size() == 1) { 16 // 如果在“我”頁面,則進入“微信”頁面 17 for (AccessibilityNodeInfo info : wechatNode) { 18 if (info.getText().toString().equals("微信") && !info.isChecked()) { 19 20 if (info.getParent().isClickable()) { 21 info.getParent().performAction(AccessibilityNodeInfo.ACTION_CLICK); 22 return; 23 } 24 break; 25 } 26 } 27 }
8. 在主頁面偶爾找不到搜索按鈕
這個問題很奇怪,排查了半天也沒發現為什麼。這個問題主要出現在進入微信比較深的地方一步步返回之後。我發現找不到搜索按鈕主要是通過id找直接就沒找到。
於是就換了一種查找控制項的方式。
解決辦法:
將event.getSource()換成getRootInActiveWindow()。
1 // 用getRootInActiveWindow是為了防止找不到搜索按鈕的問題 2 List<AccessibilityNodeInfo> searchNode = getRootInActiveWindow().findAccessibilityNodeInfosByViewId(SEARCH_ID); 3 List<AccessibilityNodeInfo> wechatNode = getRootInActiveWindow().findAccessibilityNodeInfosByViewId(WECHAT_ID); 4 List<AccessibilityNodeInfo> viewPageNode = getRootInActiveWindow().findAccessibilityNodeInfosByViewId(VIEW_PAGE_ID);
9. 如果通過同一微信號進行查找,會發現在搜索結果頁面就停止了
經排查,發現在搜索結果頁面直接更改輸入框的查詢值,如果值一樣的話,不會觸發任何的事件。出現該問題的原因就在這。
解決辦法:
先清空輸入框,再輸入需要查詢的微信號。
1 if (editTextNode.size() > 0) { 2 try { 3 Thread.sleep(300); 4 } catch (InterruptedException e) { 5 e.printStackTrace(); 6 } 7 8 // 輸入框內清空 9 Bundle clear = new Bundle(); 10 clear.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, ""); 11 editTextNode.get(0).performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, clear); 12 13 // 輸入框內輸入查詢的微信號 14 Bundle arguments = new Bundle(); 15 arguments.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, Constant.wechatId); 16 editTextNode.get(0).performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, arguments); 17 }
反思:
- 任何一門技術都是說說容易,做做難。因為在實現過程中總會出現各種各樣的問題;
- 通過無障礙的方式來實現該功能效率低,並且不穩定,不知是否有更好的方法;
- Android系統真的特別不安全!
GitHub地址:JumpToWeChat
大家如果有什麼疑問或者建議可以通過評論或者郵件的方式聯繫我,歡迎大家的評論~