zoukankan      html  css  js  c++  java
  • 从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 

    大家如果有什么疑问或者建议可以通过评论或者邮件的方式联系我,欢迎大家的评论~

  • 相关阅读:
    PHP图像处理
    PHP文件上传
    PHP文件编程
    PHP面向对象
    【Codeforces Round #694 (Div. 1) B】Strange Definition
    【Codeforces Round #694 (Div. 2) C】Strange Birthday Party
    【Codeforces Round #693 (Div. 3) F】New Year's Puzzle
    【Codeforces Round #693 (Div. 3) G】Moving to the Capital
    【Codeforces Round #695 (Div. 2) E】Distinctive Roots in a Tree
    【Codeforces Round #695 (Div. 2) D】Sum of Paths
  • 原文地址:https://www.cnblogs.com/lanxingren/p/10299481.html
Copyright © 2011-2022 走看看