AccessibilityService重新整理:微信自动抢红包、微信自动向附近的人打招呼

4,381 阅读5分钟

先说说遇到的一些问题

 去年写过微信抢红包插件的实现,但春节的时候发现微信更新之后我的插件竟然会停在开红包的页面无法继续向下执行,debug之后发现问题是微信的开红包按钮现在被改成了图片,导致我使用findAccessibilityNodeInfosByText()找不到有效的子节点,也就无法实现模拟点击去打开红包。 于是乎我开始尝试通过获取控件ID去实现,迅速打开IDE,使用Android Device Monitor查看开红包按钮的控件id,后面会附上使用方法,然后使用findAccessibilityNodeInfosByViewId()来获取开红包按钮的节点,结果当然是可以的。 但是这就带来了另一个问题:如果每次微信发版后这个按钮控件的id发生变化,那插件也就只能跟随着修改代码才能正常使用。 针对这个问题我目前的做法是开一个ArrayList记录微信开红包button所使用过的id值,然后去遍历id值通过findAccessibilityNodeInfosByViewId()获取节点,当然用map存储id值及其对应微信版本号用来做版本兼容会更好些,谁让我懒呢,懒得去获取微信版本号。当然,还有种暴力的方法,就是遍历开红包页面的节点树并模拟点击其下的每一个能点击的button,因为其实界面里能点击的就只有关闭按钮和开红包按钮,但关闭按钮其实是个imageView而不是button。

 坑总是一个接一个,在最近的微信版本更新后,我发现不仅仅是控件id会发生改变,插件关注的某些activity类名也开始被引入混淆,以及聊天页面对消息推送的处理方式也变了,适配的代码我已经更新到github。 以后微信红包如果还有其他修改,我会把适配后的代码直接更新到github的demo,所以下方的代码片不一定是最新的,感兴趣的同学可以上github去star一下,demo地址。当然适配过程中遇到的问题我还是会记录在这里。

核心代码片如下:

抢红包:

if ("com.tencent.mm.plugin.luckymoney.ui.LuckyMoneyReceiveUI".equals(event.getClassName())) {
    //当前在红包待开页面,去拆红包
    getLuckyMoney();
} else if ("com.tencent.mm.plugin.luckymoney.ui.LuckyMoneyDetailUI".equals(event.getClassName())) {
    //拆完红包后看详细纪录的界面
    openNext("查看我的红包记录");
} else if ("com.tencent.mm.ui.LauncherUI".equals(event.getClassName())) {
    //在聊天界面,去点中红包
    openLuckyEnvelope();
}

自动加人:

if (eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED && event.getClassName().equals("com.tencent.mm.ui.LauncherUI")) {
    //记录打招呼人数置零
    i = 0;
    //当前在微信聊天页就点开发现
    openNext("发现");
    //然后跳转到附近的人
    openDelay(1000, "附近的人");
} else if (event.getClassName().equals("com.tencent.mm.plugin.nearby.ui.NearbyFriendsUI") && eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
    prepos = 0;
    //当前在附近的人界面就点选人打招呼
    AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();
    List<AccessibilityNodeInfo> list = nodeInfo.findAccessibilityNodeInfosByText("米以内");
    Log.d("name", "附近的人列表人数: " + list.size());
    if (i < (list.size() * page)) {
        list.get(i % list.size()).performAction(AccessibilityNodeInfo.ACTION_CLICK);
        list.get(i % list.size()).getParent().performAction(AccessibilityNodeInfo.ACTION_CLICK);
    } else if (i == list.size() * page) {
        //本页已全部打招呼,所以下滑列表加载下一页,每次下滑的距离是一屏
        for (int i = 0; i < nodeInfo.getChild(0).getChildCount(); i++) {
            if (nodeInfo.getChild(0).getChild(i).getClassName().equals("android.widget.ListView")) {
                AccessibilityNodeInfo node_lsv = nodeInfo.getChild(0).getChild(i);
                node_lsv.performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
                page++;
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException mE) {
                            mE.printStackTrace();
                        }
                        AccessibilityNodeInfo nodeInfo_ = getRootInActiveWindow();
                        List<AccessibilityNodeInfo> list_ = nodeInfo_.findAccessibilityNodeInfosByText("米以内");
                        Log.d("name", "列表人数: " + list_.size());
                        //滑动之后,上一页的最后一个item为当前的第一个item,所以从第二个开始打招呼
                        list_.get(1).performAction(AccessibilityNodeInfo.ACTION_CLICK);
                        list_.get(1).getParent().performAction(AccessibilityNodeInfo.ACTION_CLICK);
                    }
                }).start();
            }
        }
    }
} else if (event.getClassName().equals("com.tencent.mm.plugin.profile.ui.ContactInfoUI") && eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
    if (prepos == 1) {
        //从打招呼界面跳转来的,则点击返回到附近的人页面
        performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK);
        i++;
    } else if (prepos == 0) {
        //从附近的人跳转来的,则点击打招呼按钮
        AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();
        if (nodeInfo == null) {
            Log.w(TAG, "rootWindow为空");
            return;
        }
        List<AccessibilityNodeInfo> list = nodeInfo.findAccessibilityNodeInfosByText("打招呼");
        if (list.size() > 0) {
            list.get(list.size() - 1).performAction(AccessibilityNodeInfo.ACTION_CLICK);
            list.get(list.size() - 1).getParent().performAction(AccessibilityNodeInfo.ACTION_CLICK);
        } else {
            //如果遇到已加为好友的则界面的“打招呼”变为“发消息",所以直接返回上一个界面并记录打招呼人数+1
            performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK);
            i++;
        }
    }
} else if (event.getClassName().equals("com.tencent.mm.ui.contact.SayHiEditUI") && eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
    //当前在打招呼页面
    prepos = 1;
    //输入打招呼的内容并发送
    inputHello(hello);
    openNext("发送");
}

打开通知栏消息:

private void openNotification(AccessibilityEvent event) {
    if (event.getParcelableData() == null || !(event.getParcelableData() instanceof Notification)) {
        return;
    }
    //将通知栏消息打开
    Notification notification = (Notification) event.getParcelableData();
    PendingIntent pendingIntent = notification.contentIntent;
    try {
        pendingIntent.send();
    } catch (PendingIntent.CanceledException e) {
        e.printStackTrace();
    }
}

点击匹配的nodeInfo @param str text关键字:

private void openNext(String str) {
    AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();
    if (nodeInfo == null) {
        Log.w(TAG, "rootWindow为空");
        Toast.makeText(this, "rootWindow为空", Toast.LENGTH_SHORT).show();
        return;
    }
    List<AccessibilityNodeInfo> list = nodeInfo.findAccessibilityNodeInfosByText(str);
    Log.d("name", "匹配个数: " + list.size());
    if (list.size() > 0) {
        list.get(list.size() - 1).performAction(AccessibilityNodeInfo.ACTION_CLICK);
        list.get(list.size() - 1).getParent().performAction(AccessibilityNodeInfo.ACTION_CLICK);
    } else {
        Toast.makeText(this, "找不到有效的节点", Toast.LENGTH_SHORT).show();
    }
}

自动输入打招呼内容:

private void inputHello(String hello) {
    AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();
    //找到当前获取焦点的view
    AccessibilityNodeInfo target = nodeInfo.findFocus(AccessibilityNodeInfo.FOCUS_INPUT);
    if (target == null) {
        Log.d(TAG, "inputHello: null");
        return;
    }
    ClipboardManager clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
    ClipData clip = ClipData.newPlainText("label", hello);
    clipboard.setPrimaryClip(clip);
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
        target.performAction(AccessibilityNodeInfo.ACTION_PASTE);
    }
}

关于如何获取app页面中控件的id:

在Android Studio中开启Android Device Monitor,选择设备后点击Dump View Hierarchy for UI Automator即可查看

配置使用AccessibilityService:

在manifest中的配置:

<uses-permission android:name="android.permission.BIND_ACCESSIBILITY_SERVICE" />
<service
    android:enabled="true"
    android:exported="true"
    android:label="@string/app_name"
    android:name=".AutoService"
    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/envelope_service_config"/>
</service>

meta-data中的xml资源文件:

<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:accessibilityEventTypes="typeNotificationStateChanged|typeWindowStateChanged|typeWindowContentChanged"
    android:accessibilityFeedbackType="feedbackGeneric"
    android:accessibilityFlags=""
    android:canRetrieveWindowContent="true"
    android:description="@string/app_name"
    android:notificationTimeout="100"
    android:packageNames="com.tencent.mm,com.huawei.android.launcher" />  

其中packageName用于配置你想要监测的包名,如果多个则用逗号隔开 accessibilityEventTypes表示该服务可监测界面中哪些事件类型,如窗口打开,滑动等,具体值可查看api accessibilityFeedbackType:表示反馈方式,比如是语音播放,还是震动 canRetrieveWindowContent:表示该服务能否访问活动窗口中的内容 notificationTimeout:接受事件的时间间隔

当然,除了以meta-data的方式静态配置,也可通过在服务启动时的onServiceConnected()方法中调用setServiceInfo(AccessibilityServiceInfo)进行动态配置。

补充:

几种常用accessibilityEventType事件类型: TYPE_WINDOW_STATE_CHANGED: 窗口状态改变事件类型,打开PopupWindow、dialog、menu等 TYPE_NOTIFICATION_STATE_CHANGED: 通知栏事件 TYPE_WINDOW_CONTENT_CHANGED: 窗口中内容改变 TYPE_VIEW_SCROLLED: 控件滑动事件 TYPE_WINDOWS_CHANGED: 显示窗口改变 TYPE_VIEW_TEXT_CHANGED : editText控件的内容发生改变 TYPE_TOUCH_INTERACTION_START: 用户开始触摸屏幕 TYPE_TOUCH_INTERACTION_END: 用户停止触摸屏幕

其中TYPE_WINDOW_CONTENT_CHANGED 又可以细分为4个二级类型: 1.CONTENT_CHANGE_TYPE_SUBTREE: 节点发生增减 2.CONTENT_CHANGE_TYPE_TEXT: 节点文本发生改变 3.CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION: 节点的内容描述发生改变,即控件的contentDescription属性发生改变 4.CONTENT_CHANGE_TYPE_UNDEFINED: 未定义类型,即除上面三种之外的类型

接下来,或许你可以自己尝试下使用AccessibilityService实现app的自动安装/批量安装,去学习吧,骚年! demo地址