去年写过微信抢红包插件的实现,可是今年春节的时候发现微信更新以后本身写的插件居然会停在开红包的页面没法继续向下执行,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地址。固然适配过程当中遇到的问题我仍是会记录在这里。node
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(); } }
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); } }
在Android Studio中开启Android Device Monitor,选择设备后点击Dump View Hierarchy for UI Automator便可查看android
在manifest中的配置:git
<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资源文件:github
<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:接受事件的时间间隔api
固然,除了以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: 未定义类型,即除上面三种以外的类型app
接下来,或许你能够本身尝试下使用AccessibilityService实现app的自动安装/批量安装,去学习吧,骚年!ide