先说说遇到的一些问题
去年写过微信抢红包插件的实现,但是今年春节的时候发现微信更新之后自己写的插件竟然会停在开红包的页面无法继续向下执行,debug之后发现问题是微信团队把开红包按钮的文本内容现在改成了一张图片,导致我使用findAccessibilityNodeInfosByText()找不到有效的子节点,也就无法实现模拟点击去打开红包。
于是乎我开始尝试通过获取指定控件的ID去实现,迅速打开IDE,使用Android Device Monitor查看开红包按钮的控件id,后面会附上使用方法,然后使用findAccessibilityNodeInfosByViewId()来获取开红包按钮的节点,结果当然是可以的。
但是这就带来了另一个问题:如果微信每次发版都修改这个按钮的控件id,那我的插件也就只能每次都跟随着修改代码才能正常使用,事实也证明微信的确是每次发版都会修改此控件的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 表示该服务能否访问活动窗口中的内容,为false时getRootInActiveWindow()获取结果为null
notificationTimeout 接受事件的时间间隔
当然,除了以meta-data的方式静态配置,也可通过在服务启动时的onServiceConnected()方法中调用setServiceInfo(AccessibilityServiceInfo)进行动态配置。
接下来,或许你可以自己尝试下使用AccessibilityService实现app的自动安装/批量安装,去学习吧,骚年!
demo地址:点击打开链接
补充:
几种常用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 未定义类型,即除上面三种之外的类型