從APP跳轉到微信指定聯繫人聊天頁面功能的實現與採坑之旅

来源:https://www.cnblogs.com/lanxingren/archive/2019/01/21/10299481.html
-Advertisement-
Play Games

起因: 最近做的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 

 

 

大家如果有什麼疑問或者建議可以通過評論或者郵件的方式聯繫我,歡迎大家的評論~


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 問題一般格式為: 1366 Incorrect string value: '' for column 1300 Invalid utf8 character string: '' 向mysql插入中文(東亞字元)出現問題一般是有欄位不是utf8的問題。 向mysql插入腦殘文/火星文/特殊字元出現 ...
  • 資料庫連表查詢中的nvarchar類型欄位,tb_Users.Certificates is not null條件,is not null 會導致查詢速度慢很多(因為和“=”號條件遍歷方式不一樣)。 替換為 “LEN(tb_Users.Certificates) >0”,利用 Users.Certi ...
  • 1.1.1 hive是什麼? Hive是基於 Hadoop 的一個數據倉庫工具: 1.1.2 hive和Hadoop關係 Hive利用HDFS存儲數據,利用MapReduce查詢數據,聚合函數需要經過MapReduce,非聚合函數直接讀取hdfs塊信息,不通過MapReduce。 1.1.3 hiv ...
  • HBase是建立在Hadoop文件系統之上的分散式面向列的資料庫。它是一個開源項目,是橫向擴展的。 HBase是一個數據模型,類似於谷歌的大表設計,可以提供快速隨機訪問海量結構化數據。它利用了Hadoop的文件系統(HDFS)提供的容錯能力。 它是Hadoop的生態系統,提供對數據的隨機實時讀/寫訪 ...
  • Appache hadoop 版本:2.77 jdk:1.8 系統:centos7 註意不要在root下解壓,要單獨建一個用戶安裝hadoop及其組件。 一、先查看系統是否有自帶j #dk: rpm -qa|grep java 通常是如下4個包: rpm -e --nodeps java-1.8.0 ...
  • oracle提供了for迴圈語句,讓我們可以遍歷select搜索的結果。用法也很簡單,代碼如下: for迴圈語句還可以傳入參數: ...
  • [TOC] 1. RecyclerView 1.1. Add support library 1.2. 將RecyclerView添加到佈局 此文件命名為: 1.3. 主actiivty中如何調用recycleview對象 2. 實例 本節中的所有代碼已上傳到: ...
  • ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...