欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

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

最编程 2024-04-27 19:51:41
...

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

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

推荐阅读