从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
大家如果有什么疑问或者建议可以通过评论或者的方式联系我,欢迎大家的评论~