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

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

原由

最近作的APP中有一個新功能:已知用戶微信號,可點擊直接跳轉到當前用戶微信聊天窗口頁面。java

當時第一想法是使用無障礙來作,而且以爲應該不難,只是邏輯有點複雜。沒想到最終踩了好多坑,特意把踩過的坑記錄下來。android

實現邏輯

在APP中點擊按鈕→跳轉到微信界面→模擬點擊微信搜索按鈕→在微信搜索頁面輸入獲取的微信號→模擬點擊查詢到的用戶進入用戶聊天界面。git

效果圖

實現過程

跳轉微信按鈕點擊事件

jumpButton.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
              Intent intent = new Intent(Intent.ACTION_MAIN);
              ComponentName cmp = new ComponentName("com.tencent.mm", "com.tencent.mm.ui.LauncherUI");
              intent.addCategory(Intent.CATEGORY_LAUNCHER);
              intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
              intent.setComponent(cmp);
              startActivity(intent);
        }
   });
複製代碼

無障礙監聽主要方法

一些必要的參數

/** * 微信主頁面的「搜索」按鈕id */
private final String SEARCH_ID = "com.tencent.mm:id/ij";

/** * 微信主頁面bottom的「微信」按鈕id */
private final String WECHAT_ID = "com.tencent.mm:id/d3t";

/** * 微信搜索頁面的輸入框id */
private final String EDIT_TEXT_ID = "com.tencent.mm:id/ka";

/** * 微信搜索頁面活動id */
private String SEARCH_ACTIVITY_NAME = "com.tencent.mm.plugin.fts.ui.FTSMainUI";

private String LIST_VIEW_NAME = "android.widget.ListView";
複製代碼

微信組件的id以前有博客說過如何獲取,因此在此就不重複說明了。github


監聽主要方法

@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
    List<AccessibilityNodeInfo> searchNode = event.getSource().findAccessibilityNodeInfosByViewId(SEARCH_ID);
    List<AccessibilityNodeInfo> wechatNode = event.getSource().findAccessibilityNodeInfosByViewId(WECHAT_ID);

    if (searchNode.size() > 1) {
        // 點擊「搜索」按鈕
        if (searchNode.get(0).getParent().isClickable()) {
            searchNode.get(0).getParent().performAction(AccessibilityNodeInfo.ACTION_CLICK);
            return;
        }
    } else if (searchNode.size() == 1) {
        // 若是在「我」頁面,則進入「微信」頁面
        for (AccessibilityNodeInfo info : wechatNode) {
            if (info.getText().toString().equals("微信") && !info.isChecked()) {

                if (info.getParent().isClickable()) {
                    info.getParent().performAction(AccessibilityNodeInfo.ACTION_CLICK);
                    return;
                }
                break;
            }
        }
    }

    // 當前頁面是搜索頁面
    if (SEARCH_ACTIVITY_NAME.equals(event.getClassName().toString())) {
        List<AccessibilityNodeInfo> editTextNode = event.getSource().findAccessibilityNodeInfosByViewId(EDIT_TEXT_ID);

        if (editTextNode.size() > 0) {
            // 輸入框內輸入查詢的微信號
            Bundle arguments = new Bundle();
            arguments.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, Constant.wechatId);
            editTextNode.get(0).performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, arguments);
        }
    } else if (LIST_VIEW_NAME.equals(event.getClassName().toString())) {
        // 若是監聽到了ListView的內容改變,則找到查詢到的人,並點擊進入
        List<AccessibilityNodeInfo> textNodeList = event.getSource().findAccessibilityNodeInfosByText("微信號: " + Constant.wechatId);
        if (textNodeList.size() > 0) {
            textNodeList.get(0).getParent().performAction(AccessibilityNodeInfo.ACTION_CLICK);
        }
    }

}
複製代碼

這是最原始的版本,具體邏輯已在註釋中說明。安全

遇到的坑

1. 搜索內容沒法賦值給搜索框

最開始覺得是賦值的方法有問題,可是在調試狀態下可以賦值成功。所以猜想是由於UI加載太慢的緣故。微信

在搜索框還沒徹底加載徹底的時候就進行了賦值,所以賦值不成功。app

解決辦法:ide

在賦值以前停頓300ms,在30行賦值前先停頓300ms。佈局

try {
    Thread.sleep(300);
} catch (InterruptedException e) {
    e.printStackTrace();
}
複製代碼

2. 如何中止監聽?

因爲監聽是一直會進行的,所以只要進入了微信頁面就會執行無障礙方法。這是不合理的。理論上應該在點擊按鈕進入微信纔開始監聽,而查找到好友以後就中止監聽。post

解決辦法:
能夠設置全局的變量用來控制監聽。須要在點擊按鈕設置變量值爲監聽,而查找到微信好友以後設置爲不監聽。

全局變量:

public class Constant {

    /** * 判斷是否須要監聽 */
    public static int flag = 0;

    /** * 微信號 */
    public static String wechatId;
}
複製代碼

按鈕點擊修改flag值:

jumpButton.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        Intent intent = new Intent(Intent.ACTION_MAIN);
        ComponentName cmp = new ComponentName("com.tencent.mm", "com.tencent.mm.ui.LauncherUI");
        intent.addCategory(Intent.CATEGORY_LAUNCHER);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        intent.setComponent(cmp);
        startActivity(intent);

        Constant.flag = 1;
        Constant.wechatId = editText.getText().toString();
    }
});
複製代碼

根據flag判斷是否須要監聽:

在無障礙服務的監聽方法中開始位置判斷,

// 只有從app進入微信才進行監聽
if (Constant.flag == 0) {
    return;
}
複製代碼

查詢到結果後修改flag值:

// 若是監聽到了ListView的內容改變,則找到查詢到的人,並點擊進入
List<AccessibilityNodeInfo> textNodeList = event.getSource().findAccessibilityNodeInfosByText("微信號: " + Constant.wechatId);
if (textNodeList.size() > 0) {
    textNodeList.get(0).getParent().performAction(AccessibilityNodeInfo.ACTION_CLICK);

    // 模擬點擊以後將暫存值置空,相似於取消監聽
    Constant.flag = 0;
    Constant.wechatId = null;
}
複製代碼

3. 沒查詢到結果如何中止監聽?

想必你們都發現了,上面的處理方法尚未考慮到未查詢到好友的狀況。那麼,未查詢到好友如何中止監聽呢?

最開始想的是找到未查詢頁面,只要知道了什麼狀況是未查詢的,那就能夠中止監聽了。

可是未查詢到好友的頁面查找比較麻煩,所以想了一個取巧的辦法。

解決辦法:
寫一個線程,兩秒後執行,由於用戶通常在未查詢到結果頁面會停留至少兩秒,兩秒誤操做就中止監聽。

線程實現(線程得是類持有的,而不該該是方法持有的):

Handler handler = new Handler();
Runnable runnable = new Runnable() {
    @Override
    public void run() {
        Constant.flag = 0;
        Constant.wechatId = null;
    }
};
複製代碼

監聽方法內進行線程的開啓操做:

// 兩秒後若是尚未任何的事件,則中止監聽
handler.removeCallbacks(runnable);
handler.postDelayed(runnable, 2000);
複製代碼

因爲無障礙的監聽方法會反覆執行,所以爲了保證其正確性,須要保證在最後一次事件纔開始計時。


4. 若是在微信其餘頁面怎麼辦?

最開始被這個問題難住了。後來產品給了我一個思路,其實很簡單,若是判斷當前頁面並非微信主頁面的話,就執行全局返回按鈕事件就行。

解決辦法:
若是是頁面改變事件,而且當前頁面不是主頁面也不是搜索頁面(搜索頁面就能夠直接搜索了)的話,就執行全局返回鍵。

if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED && !LAUNCHER_ACTIVITY_NAME.equals(event.getClassName().toString()) && !SEARCH_ACTIVITY_NAME.equals(event.getClassName().toString())) {
    // 若是當前頁面不是微信主頁面也不是微信搜索頁面,就模擬點擊返回鍵
    performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK);
    return;
}
複製代碼

5. 頁面改變UI加載太慢

在解決上述問題時,又遇到了以前遇到的問題,UI加載太慢的問題,所以須要在每次頁面改變事件中都得加上300ms的延遲時間。

解決辦法:

// 頁面改變時須要延遲一段時間進行佈局加載
if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
    try {
        Thread.sleep(300);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}
複製代碼

6. 聊天界面和主頁面是同一個活動

解決了上述問題以後,又遇到了一個新的問題,常常性的返回到聊天頁面就不返回了。

通過調試,發現聊天頁面的活動和微信主頁面的活動是同一個。

解決辦法:
對聊天界面單獨作處理,根據聊天界面左上角UI存在不存在來肯定是否爲聊天界面。

if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED && !LAUNCHER_ACTIVITY_NAME.equals(event.getClassName().toString()) && !SEARCH_ACTIVITY_NAME.equals(event.getClassName().toString())) {
    // 若是當前頁面不是微信主頁面也不是微信搜索頁面,就模擬點擊返回鍵
    performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK);
    return;
} else if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED && LAUNCHER_ACTIVITY_NAME.equals(event.getClassName().toString())) {
    List<AccessibilityNodeInfo> list = event.getSource().findAccessibilityNodeInfosByViewId(USERNAME_ID);
    if (list.size() > 0) {
        // 若是是微信主頁面,可是是微信聊天頁面,則模擬點擊返回鍵
        performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK);
        return;
    }
}
複製代碼

其中USRENAME_ID爲左上角備註部分的UIid


7. 搜索不到結果時,發現他在搜索結果頁面亂跳

經排查,發現搜索結果頁面中的搜索佈局提示佈局id和首頁面的搜索按鈕id一致,所以就執行了點擊搜索按鈕的方法。

解決辦法:
對於搜索按鈕頁面(主頁面)也要進行單獨判斷,因爲主頁面必定有ViewPage佈局,所以只要找到ViewPage那就證實是在主頁面。

List<AccessibilityNodeInfo> searchNode = event.getSource().findAccessibilityNodeInfosByViewId(SEARCH_ID);
List<AccessibilityNodeInfo> wechatNode = event.getSource().findAccessibilityNodeInfosByViewId(WECHAT_ID);
List<AccessibilityNodeInfo> viewPageNode = event.getSource().findAccessibilityNodeInfosByViewId(VIEW_PAGE_ID);

Log.e(TAG, "searchNode:" + searchNode.size());
Log.e(TAG, "viewPageNode:" + viewPageNode.size());

// 因爲搜索控件在多個頁面都有,因此還得判斷是否在主頁面
if (searchNode.size() > 1 && viewPageNode.size() > 0) {
    // 點擊「搜索」按鈕
    if (searchNode.get(0).getParent().isClickable()) {
        searchNode.get(0).getParent().performAction(AccessibilityNodeInfo.ACTION_CLICK);
        return;
    }
} else if (searchNode.size() == 1) {
    // 若是在「我」頁面,則進入「微信」頁面
    for (AccessibilityNodeInfo info : wechatNode) {
        if (info.getText().toString().equals("微信") && !info.isChecked()) {

            if (info.getParent().isClickable()) {
                info.getParent().performAction(AccessibilityNodeInfo.ACTION_CLICK);
                return;
            }
            break;
        }
    }
}
複製代碼

8. 在主頁面偶爾找不到搜索按鈕

這個問題很奇怪,排查了半天也沒發現爲何。這個問題主要出如今進入微信比較深的地方一步步返回以後。我發現找不到搜索按鈕主要是經過id找直接就沒找到。

因而就換了一種查找控件的方式。

解決辦法:
event.getSource()換成getRootInActiveWindow()

// 用getRootInActiveWindow是爲了防止找不到搜索按鈕的問題
List<AccessibilityNodeInfo> searchNode = getRootInActiveWindow().findAccessibilityNodeInfosByViewId(SEARCH_ID);
List<AccessibilityNodeInfo> wechatNode = getRootInActiveWindow().findAccessibilityNodeInfosByViewId(WECHAT_ID);
List<AccessibilityNodeInfo> viewPageNode = getRootInActiveWindow().findAccessibilityNodeInfosByViewId(VIEW_PAGE_ID);
複製代碼

9. 若是經過同一微信號進行查找,會發如今搜索結果頁面就中止了

經排查,發如今搜索結果頁面直接更改輸入框的查詢值,若是值同樣的話,不會觸發任何的事件。出現該問題的緣由就在這。

解決辦法:
先清空輸入框,再輸入須要查詢的微信號。

if (editTextNode.size() > 0) {
    try {
        Thread.sleep(300);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    // 輸入框內清空
    Bundle clear = new Bundle();
    clear.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, "");
    editTextNode.get(0).performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, clear);

    // 輸入框內輸入查詢的微信號
    Bundle arguments = new Bundle();
    arguments.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, Constant.wechatId);
    editTextNode.get(0).performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, arguments);
}
複製代碼

反思

  • 任何一門技術都是說說容易,作作難。由於在實現過程當中總會出現各類各樣的問題;
  • 經過無障礙的方式來實現該功能效率低,而且不穩定,不知是否有更好的方法;
  • Android系統真的特別不安全!

GitHub地址:JumpToWeChat

相關文章
相關標籤/搜索