AccessibilityService类似与按键精灵的东西,编写脚本让他自动点点点,自动化测试 这其实不是一个新的东西了,老久之前就有了, 官方原意:优化残障人士的使用体验的,而在我大天朝: 抢红包,自动安装,一键XXX等等,可谓欣欣向荣。 使用AccessibilityService也非常Easy,核心要点就是: 通过UI Automator找到节点,通过resource-id,text,content-desc等 唯一特征定位到具体的节点,接着执行各种模拟操作,点,滚动,填充, 用法比较简单的,大部分时间会花在试错和逻辑调整上! 来一发通过AccessibilityService实现的自动加好友以及拉人进群聊的Gif体验下: Gif加速了一点,不过完成加好友以及拉人总共也就耗时15s,是相当客观的啦。 下面就来介绍下AccessibilityService这个玩意怎么用吧~
AccessibilityService用法简介 1.自定义Service继承AccessibilityService 如题,自定义一个AccessibilityService类,重写两个主要方法:onInterrupt()
:辅助功能中断的回调,基本不用理,核心还是onAccessibilityEvent(AccessibilityEvent event)
。 当界面发生了什么事情,比如顶部Notification,界面更新,内容变化等, 会触发这个方法,你可以根据不同的事件响应不同的操作,比如小猪这个 就是当顶部出现加好友的Notification的event时,跳转到加好友页。 点开AccessibilityEvent类可以看到一堆的事件类型~
事件类型
描述
TYPE_VIEW_CLICKED
View被点击
TYPE_VIEW_LONG_CLICKED
View被长按
TYPE_VIEW_SELECTED
View被选中
TYPE_VIEW_FOCUSED
View获得焦点
TYPE_VIEW_TEXT_CHANGED
View文本变化
TYPE_WINDOW_STATE_CHANGED
打开了一个PopupWindow,Menu或Dialog
TYPE_NOTIFICATION_STATE_CHANGED
Notification变化
TYPE_VIEW_HOVER_ENTER
一个View进入悬停
TYPE_VIEW_HOVER_EXIT
一个View退出悬停
TYPE_TOUCH_EXPLORATION_GESTURE_START
触摸浏览事件开始
TYPE_TOUCH_EXPLORATION_GESTURE_END
触摸浏览事件完成
TYPE_WINDOW_CONTENT_CHANGED
窗口的内容发生变化,或子树根布局发生变化
TYPE_VIEW_SCROLLED
View滚动
TYPE_VIEW_TEXT_SELECTION_CHANGED
Edittext文字选中发生改变事件
TYPE_ANNOUNCEMENT
应用产生一个通知事件
TYPE_VIEW_ACCESSIBILITY_FOCUSED
获得无障碍焦点事件
TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED
无障碍焦点事件清除
TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY
在给定的移动粒度下遍历视图文本的事件
TYPE_GESTURE_DETECTION_START
开始手势监测
TYPE_GESTURE_DETECTION_END
结束手势监测
TYPE_TOUCH_INTERACTION_START
触摸屏幕事件开始
TYPE_TOUCH_INTERACTION_END
触摸屏幕事件结束
TYPE_WINDOWS_CHANGED
屏幕上的窗口变化事件,需要API 21+
TYPE_VIEW_CONTEXT_CLICKED
View中的上下文点击事件
TYPE_ASSIST_READING_CONTEXT
辅助用户读取当前屏幕事件
好吧,上面的表其实并没什么大用,我还是习惯直接把event.toString()
给打印出来, 然后自行去判断~ 如图就可以拿到event类型,以及产生对应事件的类名,核心是这两个, 除此之外还有Text和ContentDescription等。 比如我那个监听Notification跳转到添加好友页的:
这里就是对事件类型做了下判断,然后获取contentIntent,跳转而已。 简单点讲就是: 你在这个方法里,去判断一波事件类型和className, 然后再获取控件,做一些点击,滚动,填充文本等。
2.服务的配置 自定义完这个服务要想让他启用你还得执行下面的操作:Step 1 :在res文件夹下创建xml文件夹,新建一个配置的xml文件(名字自己定)
1 2 3 4 5 6 7 8 9 <?xml version="1.0" encoding="utf-8" ?> <accessibility-service xmlns:android ="http://schemas.android.com/apk/res/android" android:accessibilityEventTypes ="typeNotificationStateChanged|typeWindowStateChanged|typeWindowContentChanged" android:accessibilityFeedbackType ="feedbackGeneric" android:accessibilityFlags ="flagDefault" android:canRetrieveWindowContent ="true" android:notificationTimeout ="100" android:packageNames ="com.tencent.mm" android:settingsActivity ="com.coderpig.wechathelper.MainActivity" />
属性简介如下: accessibilityEventTypes:设置监听的事件种类,用|
隔开,监听所有可以用typeAllMask; accessibilityFeedbackType:服务提供的反馈类型,feedbackGeneric通用反馈; accessibilityFlags:辅助功能附加的标志,flagDefault默认的配置 canRetrieveWindowContent:辅助功能服务是否能够取回活动窗口内容的属性 notificationTimeout:响应时间 packageNames:监听的应用包名,用|
隔开,不填,默认监听所有应用的事件 settingsActivity:允许用户修改辅助功能的activity类名
Step 2 :接着AndroidManifest.xml文件中对该Service进行配置 先是添加一个权限:android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
接着是Service的配置:
这里是你那个配置文件xml文件的文件名,其他照抄。Step 3 :安装到手机后,需要在手机设置的无障碍处开启服务 一般在设置的辅助功能处能找到:
如果Logcat那里能看到打印的LOG,说明服务正常运行,接下来要找控件节点
3.找控件 这里可以用到神器UI Automator来查看布局层次,打开Android Studio,Ctrl + Shift + A
,输入 monitor
依次点击:选中设备 -> Dump View Hierarchy for UI Automator
注:AndroidStudio3中monitor已经去掉了,需要到到SDK/tools中运行
稍等一会,右侧就会出现当前页面的布局层次图,如图随手选中一个邀请的节点:
右侧可以拿到对应的信息,一般比较常用的是这几个,有一点要注意!!! resource-id不一定是唯一的 获得控件基本都会通过下述这个方法: getRootInActiveWindow( ):获取当前整个活动窗口的根节点 返回的是一个AccessibilityNodeInfo 类,代表View的状态信息, 提供了下述几个非常实用的方法:
getParent:获取父节点。
getChild:获取子节点。
performAction:在节点上执行一个动作。
findAccessibilityNodeInfosByText:通过字符串查找节点元素。
findAccessibilityNodeInfosByViewId:通过视图id查找节点元素。
后面的这两个方法会返回一个AccessibilityNodeInfo列表,一般操作是 遍历,然后筛选特定节点,比如我程序里的,获得底部Tab节点为”通讯录”, 然后点击,跳转后遍历,筛选”群聊”的节点,点击。
另外,UI Automator有时并不可靠(实时问题),我建议写多一个遍历节点 的方法,可以更清楚里面的控件情况:
拿到控件,接着就到触发事件了。
4.触发事件 通过调用performAction()
传入一个时间类型即可触发相应时间,比如点击,长按等 事件就多了,自己点开AccessibilityNodeInfo类查看吧,这里介绍下最常用的几个事件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 performAction(AccessibilityNodeInfo.ACTION_CLICK); performAction(AccessibilityNodeInfo.ACTION_LONG_CLICK); performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); performAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); ClipboardManager clipboard = (ClipboardManager)this .getSystemService(Context.CLIPBOARD_SERVICE); ClipData clip = ClipData.newPlainText("text" , "填充内容" ); clipboard.setPrimaryClip(clip); info.performAction(AccessibilityNodeInfo.ACTION_FOCUS); info.performAction(AccessibilityNodeInfo.ACTION_PASTE); Bundle arguments = new Bundle (); arguments.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, "填充内容" ); info.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, arguments);
除了控件触发事件外,AccessibilityService提供了一个performGlobalAction()
,用于执行 一些通用的事件:
事件类型
描述
GLOBAL_ACTION_BACK
点击返回按钮
GLOBAL_ACTION_HOME
点击home
GLOBAL_ACTION_NOTIFICATIONS
打开通知
GLOBAL_ACTION_RECENTS
打开最近应用
GLOBAL_ACTION_QUICK_SETTINGS
打开快速设置
GLOBAL_ACTION_POWER_DIALOG
打开长按电源键的弹框
另外在实际开发中,直接调用这些全局方法又是并没有生效, 我在调GLOBAL_ACTION_BACK的时候就发现有时不会回退, 个人的解决方案是使用handler.postDelay()
延时执行:
除了这样玩以外,我还利用时间差,串行去执行几个任务,比如:
上面的步骤是: 进入群聊聊天信息页后,列表滚动两次,接着依次: 1.延时1s后,找到添加成员按钮并点击; 2.延时2.3s后,把名字填充到EditText里 3.延时3s后,点击确定按钮
就不用过于依赖onAccessibilityEvent方法,除了用handler.postDelay外, 还可以用Thread.sleep(休眠时长),用到的点大概就这么多,其余的自行探究吧。
小结 本节讲解一波如何通过AccessibilityService来实现自动加好友以及拉人进群, 之前是打算用xposed来写的,后面发现没我想像中简单,而且很多用安卓机的都 不会搞机(基),root也不会,后来还是选择了AccessibilityService,简单易用, 当然后面还是会研究一波xposed实现的,敬请期待~ 对了,还有,之前那个网页端的机器人被封原因估计是信息秒回,如果有还用 itchat那个做机器人的,建议回复的时间可以稍微延长些; 关于AccessibilityService更多内容可见:Android辅助功能 Building Accessibility Services Developing an Accessibility Service
附:关键代码 代码有Bug的话正常,后续会优化下逻辑,感觉写得有点杂~
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 package com.coderpig.wechathelper;import android.accessibilityservice.AccessibilityService;import android.app.Notification;import android.app.PendingIntent;import android.os.Bundle;import android.os.Handler;import android.util.Log;import android.view.accessibility.AccessibilityEvent;import android.view.accessibility.AccessibilityNodeInfo;import java.util.List;public class HelperService extends AccessibilityService { private static final String TAG = "HelperService" ; private Handler handler = new Handler (); private String userName = "123" ; @Override public void onAccessibilityEvent (AccessibilityEvent event) { int eventType = event.getEventType(); CharSequence classNameChr = event.getClassName(); String className = classNameChr.toString(); Log.d(TAG, event.toString()); switch (eventType) { case AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED: if (event.getParcelableData() != null && event.getParcelableData() instanceof Notification) { Notification notification = (Notification) event.getParcelableData(); String content = notification.tickerText.toString(); if (content.contains("请求添加你为朋友" )) { PendingIntent pendingIntent = notification.contentIntent; try { pendingIntent.send(); } catch (PendingIntent.CanceledException e) { e.printStackTrace(); } } } break ; case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED: switch (className) { case "com.tencent.mm.plugin.subapp.ui.friend.FMessageConversationUI" : addFriend(); break ; case "com.tencent.mm.plugin.profile.ui.SayHiWithSnsPermissionUI" : verifyFriend(); break ; case "com.tencent.mm.plugin.profile.ui.ContactInfoUI" : performBackClick(); break ; case "com.tencent.mm.ui.LauncherUI" : if (!userName.equals("123" )) { openGroup(); } break ; case "com.tencent.mm.ui.contact.ChatroomContactUI" : if (!userName.equals("123" )) { inviteGroup(); } break ; case "com.tencent.mm.ui.chatting.ChattingUI" : if (!userName.equals("123" )) { openGroupSetting(); } break ; case "com.tencent.mm.plugin.chatroom.ui.ChatroomInfoUI" : if (userName.equals("123" )) { performBackClick(); } else { addToGroup(); } break ; case "com.tencent.mm.ui.base.i" : dialogClick(); break ; } break ; case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED: } } private void addFriend () { AccessibilityNodeInfo nodeInfo = getRootInActiveWindow(); if (nodeInfo != null ) { List<AccessibilityNodeInfo> list = nodeInfo .findAccessibilityNodeInfosByText("接受" ); if (list != null && list.size() > 0 ) { for (AccessibilityNodeInfo n : list) { n.performAction(AccessibilityNodeInfo.ACTION_CLICK); } } else { performBackClick(); } } } private void verifyFriend () { AccessibilityNodeInfo nodeInfo = getRootInActiveWindow(); if (nodeInfo != null ) { userName = nodeInfo.findAccessibilityNodeInfosByViewId("com.tencent.mm:id/d0n" ).get(0 ).getText().toString(); AccessibilityNodeInfo finishNode = nodeInfo.findAccessibilityNodeInfosByViewId("com.tencent.mm:id/hd" ).get(0 ); finishNode.performAction(AccessibilityNodeInfo.ACTION_CLICK); } } private void openGroup () { AccessibilityNodeInfo nodeInfo = getRootInActiveWindow(); if (nodeInfo != null ) { List<AccessibilityNodeInfo> nodes = nodeInfo.findAccessibilityNodeInfosByViewId("com.tencent.mm:id/ca5" ); for (AccessibilityNodeInfo info : nodes) { if (info.getText().toString().equals("通讯录" )) { info.getParent().performAction(AccessibilityNodeInfo.ACTION_CLICK); handler.postDelayed(new Runnable () { @Override public void run () { AccessibilityNodeInfo nodeInfo = getRootInActiveWindow(); if (nodeInfo != null ) { List<AccessibilityNodeInfo> nodes = nodeInfo.findAccessibilityNodeInfosByViewId("com.tencent.mm:id/j5" ); for (AccessibilityNodeInfo info : nodes) { if (info.getText().toString().equals("群聊" )) { info.getParent().getParent().performAction(AccessibilityNodeInfo.ACTION_CLICK); break ; } } } } }, 500L ); } } } } private void inviteGroup () { AccessibilityNodeInfo nodeInfo = getRootInActiveWindow(); if (nodeInfo != null ) { List<AccessibilityNodeInfo> nodes = nodeInfo.findAccessibilityNodeInfosByViewId("com.tencent.mm:id/a9v" ); for (AccessibilityNodeInfo info : nodes) { if (info.getText().toString().equals("小猪的Python学习交流群" )) { info.getParent().performAction(AccessibilityNodeInfo.ACTION_CLICK); break ; } } } } private void openGroupSetting () { AccessibilityNodeInfo nodeInfo = getRootInActiveWindow(); if (nodeInfo != null ) { nodeInfo.findAccessibilityNodeInfosByViewId("com.tencent.mm:id/he" ).get(0 ).performAction(AccessibilityNodeInfo.ACTION_CLICK); } } private void addToGroup () { AccessibilityNodeInfo nodeInfo = getRootInActiveWindow(); if (nodeInfo != null ) { List<AccessibilityNodeInfo> listNodes = nodeInfo.findAccessibilityNodeInfosByViewId("android:id/list" ); if (listNodes != null && listNodes.size() > 0 ) { AccessibilityNodeInfo listNode = listNodes.get(0 ); listNode.performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); listNode.performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); final AccessibilityNodeInfo scrollNodeInfo = getRootInActiveWindow(); if (scrollNodeInfo != null ) { handler.postDelayed(new Runnable () { @Override public void run () { List<AccessibilityNodeInfo> nodes = scrollNodeInfo.findAccessibilityNodeInfosByViewId("com.tencent.mm:id/d0b" ); for (AccessibilityNodeInfo info : nodes) { if (info.getContentDescription().toString().equals("添加成员" )) { info.getParent().performAction(AccessibilityNodeInfo.ACTION_CLICK); break ; } } } },1000L ); handler.postDelayed(new Runnable () { @Override public void run () { List<AccessibilityNodeInfo> editNodes = getRootInActiveWindow().findAccessibilityNodeInfosByViewId("com.tencent.mm:id/arz" ); if (editNodes != null && editNodes.size() > 0 ) { AccessibilityNodeInfo editNode = editNodes.get(0 ); Bundle arguments = new Bundle (); arguments.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, userName); editNode.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, arguments); } } }, 2300L ); handler.postDelayed(new Runnable () { @Override public void run () { List<AccessibilityNodeInfo> cbNodes = getRootInActiveWindow().findAccessibilityNodeInfosByViewId("com.tencent.mm:id/kr" ); if (cbNodes != null ) { AccessibilityNodeInfo cbNode = null ; if (cbNodes.size() == 1 ) { cbNode = cbNodes.get(0 ); } else if (cbNodes.size() == 2 ) { cbNode = cbNodes.get(1 ); } if (cbNode != null ) { cbNode.getParent().performAction(AccessibilityNodeInfo.ACTION_CLICK); AccessibilityNodeInfo sureNode = getRootInActiveWindow().findAccessibilityNodeInfosByViewId("com.tencent.mm:id/hd" ).get(0 ); sureNode.performAction(AccessibilityNodeInfo.ACTION_CLICK); } } } }, 3000L ); } } } } private void dialogClick () { AccessibilityNodeInfo inviteNode = getRootInActiveWindow().findAccessibilityNodeInfosByViewId("com.tencent.mm:id/aln" ).get(0 ); inviteNode.performAction(AccessibilityNodeInfo.ACTION_CLICK); userName = "123" ; handler.postDelayed(new Runnable () { @Override public void run () { List<AccessibilityNodeInfo> sureNodes = getRootInActiveWindow().findAccessibilityNodeInfosByViewId("com.tencent.mm:id/aln" ); if (sureNodes != null && sureNodes.size() > 0 ) { AccessibilityNodeInfo sureNode = sureNodes.get(0 ); sureNode.performAction(AccessibilityNodeInfo.ACTION_CLICK); } } },1000L ); } private void performBackClick () { handler.postDelayed(new Runnable () { @Override public void run () { performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK); } }, 300L ); } public void recycle (AccessibilityNodeInfo info) { if (info.getChildCount() == 0 ) { Log.i(TAG, "child widget----------------------------" + info.getClassName().toString()); Log.i(TAG, "showDialog:" + info.canOpenPopup()); Log.i(TAG, "Text:" + info.getText()); Log.i(TAG, "windowId:" + info.getWindowId()); Log.i(TAG, "desc:" + info.getContentDescription()); } else { for (int i = 0 ; i < info.getChildCount(); i++) { if (info.getChild(i) != null ) { recycle(info.getChild(i)); } } } } @Override public void onInterrupt () { } }
转自:https://blog.csdn.net/coder_pig/article/details/79871063 扩展阅读:AccessibilityService Android 辅助功能笔记 辅助功能 AccessibilityService笔记(2)