原创作品,转载请注明出处,尊重别人的劳动果实。
2017.2.7更新:
*现在适配微信版本更加容易了,只需要替换一个Recourse-ID即可
*可以知道对方发的是小视频还是语音,并获取秒数。
*可以区分聊天信息中的图片或者表情
实现效果:
实时监听当前聊天页面的最新一条消息,效果如图:
实现原理:
同样是利用AccessibilityService辅助服务,关于这个服务类还不了解的同学可以先看下我上一篇关于抢红包的博客,原理都一样:
http://www.cnblogs.com/cxk1995/p/6363574.html
1.首先我们先来看一下微信聊天界面的布局,查看方法:
AndroidStudio--Tools--Android--Android Device Monitor,点击:
2.如图我们可以看到,其实每一条微信聊天记录都是一个RelativeLayout:
3.再往下看,我们又可以发现,其实每一个RelativeLayout下面,又包含了一个TextView,还有一个LinearLayout
TextView就是聊天的时间
LinearLayout下则包含了我们所需要的聊天对象以及聊天信息,除了文字聊天,语音,图片等的聊天信息都会在这个LinearLayout下,看图2
4.这里聊天对象比较容易获得,我们先放在前面讲,如上图我们可以看到有一个ImageView的描述内容里面包含着我们的聊天对象,可能后面还会有很多ImageView的参杂,将它与其他ImageView区分还有很重要两点,一是它是isClickable,二是它存在描述内容,并且描述内容是还包含有“头像”字眼。
综合过滤条件: "android.widget.ImageView"+"isClickable()"+"node.getContentDescription().toString().contains("头像")",代码如下:
/** * 遍历所有控件,找到头像Imagview,里面有对联系人的描述 */ private void GetChatName(AccessibilityNodeInfo node) { for (int i = 0; i < node.getChildCount(); i++) { AccessibilityNodeInfo node1 = node.getChild(i); if ("android.widget.ImageView".equals(node1.getClassName()) && node1.isClickable()) { //获取聊天对象,这里两个if是为了确定找到的这个ImageView是头像的 if (!TextUtils.isEmpty(node1.getContentDescription())) { ChatName = node1.getContentDescription().toString(); if (ChatName.contains("头像")) { ChatName = ChatName.replace("头像", ""); } } } GetChatName(node1); } }
5.这里我们暂时把微信的聊天信息分为5种:
a.单纯的文字聊天信息:
其实从上面的右边图就可以看到,他就是一个TextView而已,如果你有去打印看看的话,你会发现他的parent父布局其实是一个RelativeLayout,后面同样可能会有其他的TeView干扰,例如我推荐了个名片或者发了个红包等,所以文字聊天TeView区分其他TextView的还有一个很重的过滤条件,就是它是可以长按的(isLongClickable()),这些属性都可以在Android Device Monitor中查看到。
综合过滤条件:"android.widget.TextView"+“android.widget.RelativeLayout”+“isLongClickable()”
b.发了一段语音聊天信息:
我们这里并没有办法获取到语音内容,只能获取语音的秒数,获取方法跟上面的一模一样,只不过这里它不能长按。我们知道语音的秒数格式都是:数字+双引号("),如60",所以我们只要判断取得的TextView内容是不是符合这种格式就行了,就能判断它是语音秒数。
综合过滤条件:"android.widget.TextView"+“android.widget.RelativeLayout”+“符合秒数格式”
c.发了一个表情:
这里的表情指的是收藏的或者自己下载的那些表情,不是指Emoji那些,他其实就是一个ImagView,父布局是一个LinearLayout。
综合过滤条件:"android.widget.ImageView"+"android.widget.LinearLayout"
d.发了一张图片:
这里目前只能做到监听到安装服务的这一端发过去的图片,不能监听到对面发过来的啧啧,其实也是一个ImageView,父布局是FrameLayout,而且还有很重要一点,该节点存在描述内容字眼:"图片"。
综合过滤条件:"android.widget.ImageView"+"android.widget.FrameLayout"+"node.getContentDescription().toString().contains("图片")"
e.发了一段小视频:
同样我们无法知道视频的内容,这里只是单纯的获取视频的秒数,和语音类似,但是获取过滤方法不同,其实小视频秒数也就是个TextView,并且它的父布局是FrameLayout,我们知道视频的秒数都是符合:00:00这种格式的,所以这个也是个很重要的过滤条件。
综合过滤条件:"android.widget.TextView"+“android.widget.FrameLayout”+“符合 00:00格式”
4.分析完后,我们思路就有了:
a.首先我们先取得根布局的节点,然后通过遍历获取到每个RelativeLayout下的LinearLayout,因为该LinearLayout存在resource-id(com.tencent.mm:id/p,微信版本6.5.4),所以我们可以很容易可以获取到符合该ID的所有LinearLayout,然后我们取出最后一个LinearLayout,这个也就是装载着我们最新的那条消息啦。
b.然后我们再在该LinearLayout下遍历它的所有控件,通过上面所讲的各种过滤条件,判断发的是什么类型的消息并取出我们所需要的即可。
注:关于resource-id直接在上一步的查看布局下可看到,因为resource-id随着版本的迭代可能也会发生改变,Demo中那个LinearLayout的resource-id是基于微信6.5.4滴,如果以后有版本更新的话我们直接修改代码中的那个ID就行啦。
获取聊天信息核心代码:
代码不多,也加了注释,直接看代码即可:
/** * 遍历所有控件:这里分四种情况 * 文字聊天: 一个TextView,并且他的父布局是android.widget.RelativeLayout * 语音的秒数: 一个TextView,并且他的父布局是android.widget.RelativeLayout,但是他的格式是0"的格式,所以可以通过这个来区分 * 图片:一个ImageView,并且他的父布局是android.widget.FrameLayout,描述中包含“图片”字样(发过去的图片),发回来的图片现在还无法监听 * 表情:也是一个ImageView,并且他的父布局是android.widget.LinearLayout * 小视频的秒数:一个TextView,并且他的父布局是android.widget.FrameLayout,但是他的格式是00:00"的格式,所以可以通过这个来区分 * * @param node */ public void GetChatRecord(AccessibilityNodeInfo node) { for (int i = 0; i < node.getChildCount(); i++) { AccessibilityNodeInfo nodeChild = node.getChild(i); //聊天内容是:文字聊天(包含语音秒数) if ("android.widget.TextView".equals(nodeChild.getClassName()) && "android.widget.RelativeLayout".equals(nodeChild.getParent().getClassName().toString())) { if (!TextUtils.isEmpty(nodeChild.getText())) { String RecordText = nodeChild.getText().toString(); //这里加个if是为了防止多次触发TYPE_VIEW_SCROLLED而打印重复的信息 if (!RecordText.equals(ChatRecord)) { ChatRecord = RecordText; //判断是语音秒数还是正常的文字聊天,语音的话秒数格式为5" if (ChatRecord.contains(""")) { Toast.makeText(this, ChatName + "发了一条" + ChatRecord + "的语音", Toast.LENGTH_SHORT).show(); Log.e("WeChatLog",ChatName + "发了一条" + ChatRecord + "的语音"); } else { //这里在加多一层过滤条件,确保得到的是聊天信息,因为有可能是其他TextView的干扰,例如名片等 if (nodeChild.isLongClickable()) { Toast.makeText(this, ChatName + ":" + ChatRecord, Toast.LENGTH_SHORT).show(); Log.e("WeChatLog",ChatName + ":" + ChatRecord); } } return; } } } //聊天内容是:表情 if ("android.widget.ImageView".equals(nodeChild.getClassName()) && "android.widget.LinearLayout".equals(nodeChild.getParent().getClassName().toString())) { Toast.makeText(this, ChatName+"发的是表情", Toast.LENGTH_SHORT).show(); Log.e("WeChatLog",ChatName+"发的是表情"); return; } //聊天内容是:图片 if ("android.widget.ImageView".equals(nodeChild.getClassName())) { //安装软件的这一方发的图片(另一方发的暂时没实现) if("android.widget.FrameLayout".equals(nodeChild.getParent().getClassName().toString())){ if(!TextUtils.isEmpty(nodeChild.getContentDescription())){ if(nodeChild.getContentDescription().toString().contains("图片")){ Toast.makeText(this, ChatName+"发的是图片", Toast.LENGTH_SHORT).show(); Log.e("WeChatLog",ChatName+"发的是图片"); } } } } //聊天内容是:小视频秒数,格式为00:00 if ("android.widget.TextView".equals(nodeChild.getClassName()) && "android.widget.FrameLayout".equals(nodeChild.getParent().getClassName().toString())) { if (!TextUtils.isEmpty(nodeChild.getText())) { String second = nodeChild.getText().toString().replace(":", ""); //正则表达式,确定是不是纯数字,并且做重复判断 if (second.matches("[0-9]+") && !second.equals(VideoSecond)) { VideoSecond = second; Toast.makeText(this, ChatName + "发了一段" + nodeChild.getText().toString() + "的小视频", Toast.LENGTH_SHORT).show(); Log.e("WeChatLog","发了一段" + nodeChild.getText().toString() + "的小视频"); } } } GetChatRecord(nodeChild); } }
使用方法:
设置-辅助功能-无障碍-点击WeChatLog开启即可(或者在设置中查找辅助功能等)
已知Bug:
没安装服务的另一方发的图片暂时无法监听到,后面改善
图片和表情没做重复信息过滤处理,所以如果触发了TYPE_VIEW_SCROLLED并且最新那条是这两个的话会出现重复。
华为7.0系统无法使用
写在最后:
个人兴趣研究,不建议用在非法途径上!!
上面是大部分的核心代码,不是完整的Demo,其实也就一个服务类而已,想要Demo的留言我发给你。
欢迎一起讨论学习:471497226@qq.com