Android AccessibilityService 实现自动发送微信消息功能

最近项目上做了这么一个功能,实现微信消息的自动发送功能。
一顿google后,发现Android提供了辅助服务的方式,可以实现这个功能红包插件想必大家都听过或者使用过,原理实际上也是通过AccessibilityService来实现的。

原理
AccessibilityService运行在后台,并且能够收到由系统发出的一些事件(AccessibilityEvent,这些事件表示用户界面一系列的状态变化),比如焦点改变,输入内容变化,按钮被点击了等等,该种服务能够请求获取当前活动窗口并查找其中的内容.换言之,界面中产生的任何变化都会产生一个时间,并由系统通知给AccessibilityService.这就像监视器监视着界面的一举一动,一旦界面发生变化,立刻发出警报.
参考:http://08643.cn/p/4cd8c109cdfb
http://www.android-doc.com/reference/android/accessibilityservice/AccessibilityService.html

本文主要介绍的是如何通过AccessibilityService实现自动发送微信的功能。

继承AccessibilityService,编写自己的服务类,必须重写onAccessibilityEvent方法,通过这个方法来监听微信界面的变化,还有onInterrupt方法,当AccessibilityService被中断时会调用这个方法。

public class AutoSendMsgService extends AccessibilityService {
    /**
     * 必须重写的方法,响应各种事件。
     *
     * @param event
     */
    @Override
    public void onAccessibilityEvent(final AccessibilityEvent event) {
        int eventType = event.getEventType();
        switch (eventType) {
            case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED: {

                String currentActivity = event.getClassName().toString();

                if (hasSend) {
                    return;
                }

                if (currentActivity.equals(WeChatTextWrapper.WechatClass.WECHAT_CLASS_LAUNCHUI)) {
                    handleFlow_LaunchUI();
                } else if (currentActivity.equals(WeChatTextWrapper.WechatClass.WECHAT_CLASS_CONTACTINFOUI)) {
                    handleFlow_ContactInfoUI();
                } else if (currentActivity.equals(WeChatTextWrapper.WechatClass.WECHAT_CLASS_CHATUI)) {
                    handleFlow_ChatUI();
                }
            }
            break;
        }
    }

    @Override
    public void onInterrupt() {

    }


}

在清单文件中声明我们的服务类,配置好服务所需参数。

        <service
            android:name=".AutoSendMsgService"
            android:enabled="true"
            android:exported="true"
            android:label="@string/app_name"
            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/auto_reply_service_config"/>
        </service>

服务参数通过auto_reply_service_config文件配置,此文件位于res->xml目录下。

<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:accessibilityEventTypes="typeWindowStateChanged"
    android:accessibilityFeedbackType="feedbackGeneric"
    android:accessibilityFlags="flagDefault"
    android:canRetrieveWindowContent="true"
    android:description="@string/accessibility_description"
    android:notificationTimeout="100"
   //表示监听微信
    android:packageNames="com.tencent.mm" />

AccessibilityService 提供两种方式获取界面上的View信息。
a,findAccessibilityNodeInfosByText 通过界面上的文本获取对应的View
b,findAccessibilityNodeInfosByViewId 通过View的id来获取对应的View
这两种方式都会在本文中使用。

QQ截图20171225220931.png

自动发送微信步骤
手动开启辅助服务->打开微信->点击通讯录->向下滑动通讯录找到对应的联系人->点击联系人进入到联系人信息界面->点击发消息按钮进入聊天界面->文本框中粘贴需要发送的文本信息->点击发送按钮发送
我们来一步步介绍具体过程

第一步,手动开启辅助服务
辅助服务不能通过代码动态启动,只能手动去设置->无障碍->辅助服务->找到我们的辅助服务->开启,如果不开启,辅助服务是不会生效的,可以通过代码跳转到设置页面

Intent intent = new Intent(android.provider.Settings.ACTION_ACCESSIBILITY_SETTINGS);
startActivity(intent);

第二步,启动微信

Intent intent = new Intent();
intent.setFlags(FLAG_ACTIVITY_NEW_TASK);
intent.setClassName(WeChatTextWrapper.WECAHT_PACKAGENAME, WeChatTextWrapper.WechatClass.WECHAT_CLASS_LAUNCHUI);
startActivity(intent);

第三步,微信打开之后,AccessibilityService 会回调onAccessibilityEvent方法,eventType会发生改变,AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED代表当前界面的状态发生了变化,通过 event.getClassName().toString()可以获取当前Activity的类名,当前类名如果是“com.tencent.mm.ui.LauncherUI”,说明微信已经来到进入到了首页。此时,我们就可以点击通讯录进行下一步操作了。

     if (currentActivity.equals(WeChatTextWrapper.WechatClass.WECHAT_CLASS_LAUNCHUI)) {
         handleFlow_LaunchUI();
     }

    private void handleFlow_LaunchUI() {

        try {
            //点击通讯录,跳转到通讯录页面
            WechatUtils.findTextAndClick(this, "通讯录");

            Thread.sleep(50);

            //再次点击通讯录,确保通讯录列表移动到了顶部,微信如果本来就是打开的状态,通讯录可能位于其他位置,从顶部开始查找,这样就能确保遍历到了所有联系人。
            WechatUtils.findTextAndClick(this, "通讯录");

            Thread.sleep(200);

            //遍历通讯录联系人列表,查找联系人
            AccessibilityNodeInfo itemInfo = TraversalAndFindContacts();
            if (itemInfo != null) {
                WechatUtils.performClick(itemInfo);
            } else {
                SEND_STATUS = SEND_FAIL;
                resetAndReturnApp();
            }

        } catch (Exception e) {
            e.printStackTrace();
        }

    }

第四步,遍历查找通讯录主要是放在TraversalAndFindContacts方法中进行的。通讯录界面本身是一个listview,主要流程是,通过listview的id来得到listview对象,id的可以通过andorid SDK提供的uiautomatorviewer工具获取,使用方法自行google ,AccessibilityService只能拿到屏幕上可见的信息,不能取到不可见的listview item信息,遍历当前屏幕上的listview的item也就是联系人信息,看看是不是能找到联系人,如果找不到,就执行向下翻页操作,直到滚动到底部为止,如果中途找到,直接进入下一步,如果找不到,那么微信就发送失败。

    private AccessibilityNodeInfo TraversalAndFindContacts() {

        if (allNameList != null) allNameList.clear();

        //获取窗体内容
        AccessibilityNodeInfo rootNode = getRootInActiveWindow();
        //通过id找到listview对象
        List<AccessibilityNodeInfo> listview = rootNode.findAccessibilityNodeInfosByViewId(WeChatTextWrapper.WechatId.WECHATID_CONTACTUI_LISTVIEW_ID);

        //是否滚动到了底部
        boolean scrollToBottom = false;
        if (listview != null && !listview.isEmpty()) {
            while (true) {
                //获取当前屏幕上的联系人信息
                List<AccessibilityNodeInfo> nameList = rootNode.findAccessibilityNodeInfosByViewId(WeChatTextWrapper.WechatId.WECHATID_CONTACTUI_NAME_ID);
                List<AccessibilityNodeInfo> itemList = rootNode.findAccessibilityNodeInfosByViewId(WeChatTextWrapper.WechatId.WECHATID_CONTACTUI_ITEM_ID);

                if (nameList != null && !nameList.isEmpty()) {
                    for (int i = 0; i < nameList.size(); i++) {
                        if (i == 0) {
                            //必须在一个循环内,防止翻页的时候名字发生重复
                            mRepeatCount = 0;
                        }
                        //item代表listview的item,因为
                        AccessibilityNodeInfo itemInfo = itemList.get(i);
                        AccessibilityNodeInfo nodeInfo = nameList.get(i);
                        String nickname = nodeInfo.getText().toString();
                        Log.d(TAG, "nickname = " + nickname);
                        //判断是不是要发送的联系人
                        if (nickname.equals(WechatUtils.NAME)) {
                            return itemInfo;
                        }
                        if (!allNameList.contains(nickname)) {
                            allNameList.add(nickname);
                        } else if (allNameList.contains(nickname)) {
                            Log.d(TAG, "mRepeatCount = " + mRepeatCount);
                            //判断是不是已经滚动到了底部的方法,这个方法其实有问题,如果微信通信录中有多个重复的联系人,这个判断方法会失效。目前没有找到更好的判断是否滑动到了底部的方法,欢迎各位大神沟通交流。
                            if (mRepeatCount == 3) {
                                //表示已经滑动到顶部了
                                if (scrollToBottom) {
                                    Log.d(TAG, "没有找到联系人");
                                    //此次发消息操作已经完成
                                    hasSend = true;
                                    return null;
                                }
                                scrollToBottom = true;
                            }
                            mRepeatCount++;
                        }
                    }
                }

                if (!scrollToBottom) {
                    //向下滚动
                    listview.get(0).performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
                } else {
                    return null;
                }

                //必须等待,因为需要等待滚动操作完成
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        return null;
    }

第五步、如果找到了联系人,点击联系人,到联系人信息页面,此时onAccessibilityEvent会回调,窗体信息发生变化,通过handleFlow_ContactInfoUI方法处理这个消息,此方法比较简单,找到页面上的发消息按钮,点击进入下一步聊天页面。

     else if (currentActivity.equals(WeChatTextWrapper.WechatClass.WECHAT_CLASS_CONTACTINFOUI)) {
        handleFlow_ContactInfoUI();
     } 

    private void handleFlow_ContactInfoUI() {
        WechatUtils.findTextAndClick(this, "发消息");
    }

第六步,同上,进入到聊天页面onAccessibilityEvent会回调,通过handleFlow_ChatUI方法处理此事件。
这个方法的逻辑:判断聊天对象是不是正确的,因为可能我们打开微信的时候,微信已经处于聊天界面中了,此时需要退到主页,再重新执行前面的操作,直到找到正确的联系人即可。WechatUtils.findViewByIdAndPasteContent,这个方法用来向文本框中粘贴需要发送的信息,sendContent方法,点击发送按钮,发送消息。
还有种情况是,聊天页面可能处于语音状态,这样就需要点击切换文本按钮,切换回发送文本状态,才能找到文本框并且粘贴信息,否则会找不到文本框,消息发送失败。

    else if (currentActivity.equals(WeChatTextWrapper.WechatClass.WECHAT_CLASS_CHATUI)) {
        handleFlow_ChatUI();
    }

    private void handleFlow_ChatUI() {

        //如果微信已经处于聊天界面,需要判断当前联系人是不是需要发送的联系人
        String curUserName = WechatUtils.findTextById(this, WeChatTextWrapper.WechatId.WECHATID_CHATUI_USERNAME_ID);
        if (!TextUtils.isEmpty(curUserName) && curUserName.equals(WechatUtils.NAME)) {
            if (WechatUtils.findViewByIdAndPasteContent(this, WeChatTextWrapper.WechatId.WECHATID_CHATUI_EDITTEXT_ID, WechatUtils.CONTENT)) {
                sendContent();
            } else {
                //当前页面可能处于发送语音状态,需要切换成发送文本状态
                WechatUtils.findViewIdAndClick(this, WeChatTextWrapper.WechatId.WECHATID_CHATUI_SWITCH_ID);

                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                if (WechatUtils.findViewByIdAndPasteContent(this, WeChatTextWrapper.WechatId.WECHATID_CHATUI_EDITTEXT_ID, WechatUtils.CONTENT)) {
                    sendContent();
                }
            }
        } else {
            //回到主界面
            WechatUtils.findViewIdAndClick(this, WeChatTextWrapper.WechatId.WECHATID_CHATUI_BACK_ID);
        }
    }

至此,微信发送完毕,细心的朋友可能已经发现所有的点击,粘贴,查找文本信息等信息等操作都放在WechatUtils这个类中进行的,这是我封装的进行服务操作动作的一个类,主要是方便管理和复用。
贴一个找到文本并且进行点击操作的代码,其他方法感兴趣的自行下载Demo查看。

    public static void findTextAndClick(AccessibilityService accessibilityService, String text) {

        AccessibilityNodeInfo accessibilityNodeInfo = accessibilityService.getRootInActiveWindow();
        if (accessibilityNodeInfo == null) {
            return;
        }

        List<AccessibilityNodeInfo> nodeInfoList = accessibilityNodeInfo.findAccessibilityNodeInfosByText(text);
        if (nodeInfoList != null && !nodeInfoList.isEmpty()) {
            for (AccessibilityNodeInfo nodeInfo : nodeInfoList) {
                if (nodeInfo != null && (text.equals(nodeInfo.getText()) || text.equals(nodeInfo.getContentDescription()))) {
                    performClick(nodeInfo);
                    break;
                }
            }
        }
    }

Demo地址:https://github.com/Clearlee/AutoSendWeChatMsg

?著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,100评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,308评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事?!?“怎么了?”我有些...
    开封第一讲书人阅读 159,718评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,275评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,376评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,454评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,464评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,248评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,686评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,974评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,150评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,817评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,484评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,140评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,374评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,012评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,041评论 2 351

推荐阅读更多精彩内容