Android 通过AccessibilityService实现微信聊天记录导出

接上Android 微信聊天记录、联系人备份并导出为表格继续讲

不太了解AccessibilityService可以看看这篇文章

基本原理:

首先打开 DDMS 捕捉界面元素

拿到resourceid,调用方法

List<AccessibilityNodeInfo> mListView = rootNode.findAccessibilityNodeInfosByViewId("com.tencent.mm:id/a-c");

能拿到一个 listview 的 list, 事实上 mListView 的长度只有1再通过

myListView.get(0)

就可以准确的拿到 listview了,然后可以调用滚动的方法

/* 滚动 */
listNode.performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD) //向上滚动
listNode.performAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD) //向下滚动

每滚动一次,根据 resourceId 获取当前屏幕的textview 集合,再遍历集合,调用 textview.getText() 方法,就能拿到聊天记录了; 

下面直接贴代码,讲的很详细了,1:1的注释

package com.cxk.wechatlog;

import android.accessibilityservice.AccessibilityService;
import android.content.Intent;
import android.graphics.Rect;
import android.os.Environment;
import android.text.TextUtils;
import android.util.Log;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.Toast;

import java.util.List;

/**
 * Created by 曾轲
 * <p>
 * 获取即时微信聊天记录服务类,该功能在微信6.5.8上可正常使用
 */

public class WeChatLogService extends AccessibilityService {

    /**
     * 聊天对象
     */
    private String ChatName;

    /**
     * 小视频的秒数,格式为00:00
     */
    private String VideoSecond;


    private String ChatRecord;

    /**
     *  根据聊天列表两次滚动后最底部的人是不是同一个,来判断聊天列表是否到了底部
     */
    private boolean listIsBottom = false;
    private String bottomName = "";

    /**
     *  根据聊天消息滚动前后三条是否重复,用来判断消息界面是否滚动到了最顶部
     */
    private boolean chatMessageIsTop = false;
    private String chatMessage1 = "";
    private String chatMessage2 = "";
    private String chatMessage3 = "";
    String topMessage1= "";
    String topMessage2= "";
    String topMessage3= "";
    /**
     * 聊天人的姓名
     */
    String chatName= "";

    /**
     * 聊天信息保存目录
     */
    private String mCurrApkPath =  Environment.getExternalStorageDirectory().getPath() + "/";

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        int eventType = event.getEventType();
        switch (eventType) {
            case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED: {
                String currentActivity = event.getClassName().toString();
                //如果在微信界面
                if (currentActivity.equals(WeChatTextWrapper.WechatClass.WECHAT_CLASS_LAUNCHUI)) {
                    Log.e("界面","微信主页");
                    AccessibilityNodeInfo rootNodeInfo = getRootInActiveWindow();
                    //获取聊天列表的 listview
                    List<AccessibilityNodeInfo> listview = rootNodeInfo.findAccessibilityNodeInfosByViewId("com.tencent.mm:id/bny");
                    if (listview!=null&&listview.size() != 0) {
                        //如果 listview 没有滑动到最底部,就一直无限循环遍历聊天记录
                        while (!listIsBottom) {
                            //获取当前页面聊天 list的单个条目和聊天人名字
                            List<AccessibilityNodeInfo> ChatList = rootNodeInfo.findAccessibilityNodeInfosByViewId("com.tencent.mm:id/agu");
                            List<AccessibilityNodeInfo> NameList = rootNodeInfo.findAccessibilityNodeInfosByViewId("com.tencent.mm:id/agw");
                            //如果没有聊天信息,直接跳出,并终止循环
                            if(NameList.size()==0||NameList==null){
                                listIsBottom=true;
                               return;
                            }
                            //获取当前list 最后一个人员的名字,与上一次滑动比较,如果相同说明到了最底部
                            String lastName = NameList.get(NameList.size() - 1).getText().toString();
                            if (bottomName.equals(lastName)) {
                                listIsBottom = true;
                            } else {
                                bottomName = lastName;
                                listIsBottom = false;
                                //遍历循环列表,进入聊天详情页进行备份
                                if (ChatList!=null&ChatList.size() != 0) {
                                    for (int j = 0; j < ChatList.size(); j++) {
                                        //页面停顿防止过快无法找到目标
                                        try {
                                            Thread.sleep(500);
                                        } catch (InterruptedException e) {
                                            e.printStackTrace();
                                        }
                                        //进入聊天界面
                                        ChatList.get(j).performAction(AccessibilityNodeInfo.ACTION_CLICK);
                                        //延迟防止页面还没完全载入,拿不到根节点信息
                                        try {
                                            Thread.sleep(500);
                                        } catch (InterruptedException e) {
                                            e.printStackTrace();
                                        }
                                        AccessibilityNodeInfo chatRootNodeInfo = getRootInActiveWindow();
                                        //遍历下一个人的聊天记录,需要重置定置状态
                                        chatMessageIsTop=false;
                                        getWeChatLog(chatRootNodeInfo);
                                    }
                                }
                                //循环遍历完当前页面的列表后滑动遍历下一个页面
                                try {
                                    Thread.sleep(500);
                                } catch (InterruptedException e) {
                                    e.printStackTrace();
                                }
                                listview.get(0).performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
                                try {
                                    Thread.sleep(1000);
                                } catch (InterruptedException e) {
                                    e.printStackTrace();
                                }
                            }

                           }
                        }else{

                    }

                    } else if (currentActivity.equals(WeChatTextWrapper.WechatClass.WECHAT_CLASS_CONTACTINFOUI)) {
                        Log.e("界面","联系人界面");
                    } else if (currentActivity.equals(WeChatTextWrapper.WechatClass.WECHAT_CLASS_CHATUI)) {
                        Log.e("界面","聊天界面");
                    }
                }
                break;
            }


    }

    /**
     * 遍历获取聊天消息
     * @param rootNode
     */
    private void getWeChatLog(AccessibilityNodeInfo rootNode) {
        if (rootNode != null) {
            //获取聊天人的姓名
            List<AccessibilityNodeInfo> listName = rootNode.findAccessibilityNodeInfosByViewId("com.tencent.mm:id/gp");
            if(listName!=null&&listName.size()!=0){
               chatName = listName.get(0).getText().toString();
                Log.e("name",chatName);
            }

            //获取聊天详情页的listview
            List<AccessibilityNodeInfo> listChatRecord = rootNode.findAccessibilityNodeInfosByViewId("com.tencent.mm:id/a3e");
            //如果没有聊天记录,直接返回
            if(listChatRecord!=null&&listChatRecord.size()==0){
                Log.e("消息类型","当前没有聊天记录");
                performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK);
                return;
            }
            //利用姓名创建文件


            //当消息详情不在最顶部时无限循环遍历消息记录
            while(!chatMessageIsTop){
            //有聊天记录,开始遍历循环
            //获取聊天头像list
            List<AccessibilityNodeInfo> imageName = rootNode.findAccessibilityNodeInfosByViewId("com.tencent.mm:id/ik");
            //获取聊天信息list
            List<AccessibilityNodeInfo> record = rootNode.findAccessibilityNodeInfosByViewId("com.tencent.mm:id/im");

            //有聊天头像说明有聊天记录(但是不一定有文字信息,可能是图片,分享等等)
            if (imageName!=null&&imageName.size() != 0) {
                //如果record的 size 为零说明当前截取到的 list 没有文字
                if (record.size() == 0) {
                    Log.e("消息类型","当前没有文字消息");
                    //判断当前这条消息是不是和上一条一样,防止重复
                        //获取聊天对象
                        ChatName = imageName.get(0).getContentDescription().toString().replace("头像", "");
                        Log.e("AAAA", ChatName + ":" + "对方发的是图片或者表情");

                        try {
                            Thread.sleep(800);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        //当前 listview 显示的都是非文字消息,乡下滚动一次后再次遍历循环
                        listChatRecord.get(0).performAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
                        return;
                //有文本消息,开始辨析备份文本消息
                }else {
                    //获取当前页面顶部的三条文字,与上一次滚动的三条文字对比,判断是否滚动到最顶部了
                    switch (record.size()) { //判断 size 防止索引越界
                        case 1:
                             topMessage1 = record.get(record.size() - 1).getText().toString();
                             topMessage2="";
                             chatMessage2="";
                             topMessage3="";
                             chatMessage3="";
                            break;

                        case 2:
                             topMessage1 = record.get(record.size() - 1).getText().toString();
                             topMessage2 = record.get(record.size() - 2).getText().toString();

                            topMessage3="";
                            chatMessage3="";
                            break;
                        case 3:
                             topMessage1 = record.get(record.size() - 1).getText().toString();
                             topMessage2 = record.get(record.size() - 2).getText().toString();
                             topMessage3 = record.get(record.size() - 3).getText().toString();

                            break;

                            default:
                                topMessage1 = record.get(record.size() - 1).getText().toString();
                                topMessage2 = record.get(record.size() - 2).getText().toString();
                                topMessage3 = record.get(record.size() - 3).getText().toString();

                                break;
                    }
                        //如果三条消息都重复证明已经滚动到了最顶部,返回聊天列表
                        if(chatMessage1.equals(topMessage1)&&chatMessage2.equals(topMessage2)&&chatMessage3.equals(topMessage3)){
                            chatMessageIsTop=true;
                            performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK);
                        }else{
                            //不是第一条,重新赋值最后三条消息记录,遍历保存文字信息
                            chatMessage1=topMessage1;
                            chatMessage2=topMessage2;
                            chatMessage3=topMessage3;
                            chatMessageIsTop=false;
                            //遍历获取文字,并写入文件
                            for (int i = record.size()-1; i >= 0; i--) {
                                //保存消息
                                //Log.e("文字消息",record.get(i).getText().toString());
                                //Log.e("index",record.get(i).toString());
                                //获取文本消息的位置,根据位置来判断但前消息是谁发的
                                Rect outBounds = new Rect();
                                record.get(i).getBoundsInScreen(outBounds);
                                //因为对方发送的消息的左侧 left 坐标是永远不会变的,自己发送的消息会被换行折叠,因此可以用此来区分
                                String leftIndex = outBounds.left+"";
                                //不同手机这个值不一样,需要自己适配
                                if(leftIndex.equals("156")){
                                    //对方发送的消息
                                    saveMassage(chatName,record.get(i).getText().toString());
                                }else{
                                    //自己发送的消息
                                    saveMassage("我",record.get(i).getText().toString());
                                }

                            }
                            //遍历完毕后滚动下一屏聊天记录开始,并停止.8秒等待聊天记录加载
                            listChatRecord.get(0).performAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
                            try {
                                Thread.sleep(800);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                    }

                    //有聊天列表但是没有聊天头像和文字消息.类似于招行信用卡的公众号,不作处理直接返回聊天列表遍历下一个
                    }else{
                        chatMessageIsTop=true;
                        performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK);
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                         }
                    }
                }
            }
        }

    private void saveMassage(String chatName, String message) {


    }


    /**
     * 遍历所有控件,找到头像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);
        }
    }


    /**
     * 必须重写的方法:系统要中断此service返回的响应时会调用。在整个生命周期会被调用多次。
     */
    @Override
    public void onInterrupt() {
        Toast.makeText(this, "我快被终结了啊-----", Toast.LENGTH_SHORT).show();
    }

    /**
     * 服务开始连接
     */
    @Override
    protected void onServiceConnected() {
        Toast.makeText(this, "服务已开启", Toast.LENGTH_SHORT).show();
        super.onServiceConnected();
    }

    /**
     * 服务断开
     *
     * @param intent
     * @return
     */
    @Override
    public boolean onUnbind(Intent intent) {
        Toast.makeText(this, "服务已被关闭", Toast.LENGTH_SHORT).show();
        return super.onUnbind(intent);
    }

    /**
     * 遍历所有控件:这里分四种情况
     * 文字聊天: 一个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);
        }
    }


}
package com.cxk.wechatlog;

public class WeChatTextWrapper {
    public static final String WECAHT_PACKAGENAME = "com.tencent.mm";


    public static class WechatClass{
        //微信首页
        public static final String WECHAT_CLASS_LAUNCHUI = "com.tencent.mm.ui.LauncherUI";
        //微信联系人页面
        public static final String WECHAT_CLASS_CONTACTINFOUI = "com.tencent.mm.plugin.profile.ui.ContactInfoUI";
        //微信聊天页面
        public static final String WECHAT_CLASS_CHATUI = "com.tencent.mm.ui.chatting.ChattingUI";
    }


    public static class WechatId{
        /**
         * 通讯录界面
         */
        public static final String WECHATID_CONTACTUI_LISTVIEW_ID = "com.tencent.mm:id/ih";
        public static final String WECHATID_CONTACTUI_ITEM_ID = "com.tencent.mm:id/iy";
        public static final String WECHATID_CONTACTUI_NAME_ID = "com.tencent.mm:id/j1";

        /**
         * 聊天界面
         */
        public static final String WECHATID_CHATUI_EDITTEXT_ID = "com.tencent.mm:id/a_z";
        public static final String WECHATID_CHATUI_USERNAME_ID = "com.tencent.mm:id/ha";
        public static final String WECHATID_CHATUI_BACK_ID = "com.tencent.mm:id/h9";
        public static final String WECHATID_CHATUI_SWITCH_ID = "com.tencent.mm:id/a_x";
    }
}
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.cxk.wechatlog">

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses-permission>
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <service
            android:name=".WeChatLogService"
            android:enabled="true"
            android:exported="true"
            android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
            <intent-filter>
                <action android:name="android.accessibilityservice.AccessibilityService" />
            </intent-filter>
            <meta-data
                android:name="android.accessibilityservice"
                android:resource="@xml/wechatlog_service_config"></meta-data>
        </service>
    </application>

</manifest>

源码地址 :

https://github.com/KeZengOo/AccessibilityServiceWeChatHelper

注意事项 :

1.微信每一次更新都会导致 resourceId 变化,所以尽量不要更新或者及时更改代码中的 resourceId

2.旧版本的微信每一个文本框都是用的 textview,可以直接调用 getText()获取聊天文字,新版本为了防止这个操作,变成自定义的 View 了,目前我还没有找到解决方案....希望有大神能点拨点拨...

3.我的代码在微信6.5.8上运行完美,高于该版本的都不行(参考第二条)

4.目前只是第一个 demo 版本,有很多不足的地方,遍历到公众号,等非聊天窗口时,可能会有异常

5.安装后必须在设置里打开辅助功能里的对应开关,再切换到微信,即可正常运行

6.如果觉得还不错,请给我点个赞吧

评论 18
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值