【杰哥带你玩转Android自动化】AccessibilityService基础
0x1、引言
Hi,我是杰哥,忙了好一阵子,终于有点时间来继续填坑啦~
间隔太久没更新,读者估计都忘记这个专栏了,所以在开始本节前,再重复下这段话:
所有Android自动化框架和工具中 操作Android设备的功能实现 都基于 adb 和 无障碍服务AccessibilityService。
前面所学的 adb
更倾向于 PC端控制Android设备自动化,无论有线连接还是无线连接,你都需要一台PC 来跑脚本。它的 不方便 还体现在:你写了一个很屌的脚本,跟亲朋好友Share,他们还得 安装配置一波运行环境 才能用。
而本节要学的 无障碍服务AccessibilityService
则更倾向于 APP控制Android设备自动化,把编写好的脚本打包成 Android APK安装包,直接把apk发给别人,安装了启动下无障碍服务,直接能用,相比之下方便得不是一星半点。当然,编写脚本需要一点 一点基本的Android开发经验。
AccessibilityService,别看名字长,其实一点都不难,本节学习路线如下:
- 简单了解下AccessibilityService是什么东西;
- AccessibilityService的基本使用,先跑起来再说;
- 掌握一些常用伎俩;
- 动手写个超简单的案例:自动登录Windows/Mac微信
没有前戏,我直接开始~
0x2、AccessibilityService简介
Android官方文档中有个专题 → 【打造无障碍应用】 其中包含了对 无障碍相关 的一系列解读,在Android开发者的公号里也有两篇讲解的文章:
感兴趣的可移步至相关文章进行阅读,这里不展开讲,我们更关注的是 无障碍服务的使用。点开官方文档:《创建自己的无障碍服务》,这样介绍到:
无障碍服务是一种应用,可提供界面增强功能,来协助残障用户或可能暂时无法与设备进行全面互动的用户完成操作。例如,正在开车、照顾孩子或参加喧闹聚会的用户可能需要其他或替代的界面反馈方式。Android 提供了标准的无障碍服务(包括 TalkBack),开发者也可以创建和分发自己的服务。
简而言之就是:优化残障人士使用Android设备和应用程序的体验
读者看完这段话,估计是一脸懵逼,落地一下就是:利用这个服务自动控制其它APP的各种操作,如点击、滑动、输入等。然后文档下面有一个 注意:
只能是为了!!!
2333,在国内是不存在的,它的应用场景五花八门,凡是和 自动点 有关的都离不开它,如:灰产微商工具、开屏广告跳过、自动点击器、红包助手、自动秒杀工具、一键XX、第三方应用监听等等。em...读者暂且把它理解成一个可以拿来帮助我们自动点点点的工具就好,接着说下怎么用。
0x3、AccessibilityService基本使用
AccessibilityService无障碍服务 说到底,还是一个 服务,那妥妥滴继承 Service,并具有它的生命周期和一些特性。
用户手动到设置里启动无障碍服务,系统绑定服务后,会回调 onServiceConnected()
,而当用户在设置中手动关闭、杀死进程、或开发者调用 disableSelf()
时,服务会被关闭销毁。
关于它的基本用法非常简单,四步走~
① 自定义AccessibilityService
继承 AccessibilityService
,重写 onInterrupt()
和 onAccessibilityEvent()
方法,示例代码如下:
import android.accessibilityservice.AccessibilityService
import android.util.Log
import android.view.accessibility.AccessibilityEvent
class JumpAdAccessibilityService: AccessibilityService() {
val TAG = javaClass.simpleName
override fun onAccessibilityEvent(event: AccessibilityEvent?) {
Log.d(TAG, "onAccessibilityEvent:$event")
}
override fun onInterrupt() {
Log.d(TAG, "onInterrupt")
}
}
上述两个方法是 必须重写 的:
onInterrupt()
→ 服务中断时回调;onAccessibilityEvent()
→ 接收到系统发送AccessibilityEvent时回调,如:顶部Notification,界面更新,内容变化等,我们可以筛选特定的事件类型,执行不同的响应。比如:顶部出现WX加好友的Notification Event,跳转到加好友页自动通过。
具体的Event类型可参见文尾附录,另外两个 可选 的重写方法:
onServiceConnected()
→ 当系统成功连接无障碍服务时回调,可在此调用 setServiceInfo() 对服务进行配置调整onUnbind()
→ 系统将要关闭无障碍服务时回调,可在此进行一些关闭流程,如取消分配的音频管理器
② Service注册
上面说了AccessbilityService本质还是Service,所以需要在 AndroidManifest.xml
中进行注册:
<service
android:name=".JumpAdAccessibilityService"
android:exported="false"
android:label="跳过广告哈哈哈哈"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
</service>
Tips:设置 android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE" 是为了确保只有系统可以绑定此服务。而 android:label 是设置在无障碍服务那里文案,其它照抄。
③ 监听相关配置
就是监听什么类型的Event,监听什么app等的配置,配置方法有两种,二选一 即可~
动态配置
重写 onServiceConnected()
,配置代码示例如下:
override fun onServiceConnected() {
val serviceInfo = AccessibilityServiceInfo().apply {
eventTypes = AccessibilityEvent.TYPES_ALL_MASK
feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC
flags = AccessibilityServiceInfo.DEFAULT
packageNames = arrayOf("com.tencent.mm") //监听的应用包名,支持多个
notificationTimeout = 10
}
setServiceInfo(serviceInfo)
}
属性与可选值详解可见文尾附录,接着说另一种配置方式~
静态配置
Android 4.0 后,可以在AndroidManifest.xml中添加一个引用配置文件的<meta-data>元素:
<service android:name=".JumpAdAccessibilityService"
...
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/accessible_service_config_jump_ad" />
可以看到resource属性会引用了一个xml文件,我们来创建这个文件:
在 res 文件夹下 新建xml文件夹 (有的话不用建),然后 新建一个配置xml文件 (名字自己定),如:
内容如下:
<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeAllMask"
android:accessibilityFeedbackType="feedbackGeneric"
android:accessibilityFlags="flagDefault"
android:canRetrieveWindowContent="true"
android:description="@string/accessibility_desc"
android:notificationTimeout="100"
android:packageNames="com.tencent.mm"
android:settingsActivity="cn.coderpig.jumpad.MainActivity" />
属性与可选值详解可见文尾附录,说下两种配置方式的优缺点:
静态配置可配置属性更多,适合参数不需要动态改变的场景,动态配置属性有限,但灵活性较高,可按需修改参数,可以搭配使用。
④ 启用无障碍服务
二选一配置完毕后,运行APP,然后依次打开手机 (不同手机系统会有些许差异):设置 → 无障碍 → 找到我们的APP → 显示关闭说明无障碍服务没起来,点开:
开关打开后,会弹出授权窗口,点击允许:
上面我们设置监听的包名是com.tencent.mm,打开微信,也可以看到控制台陆续输出一些日志信息:
可以,虽然没具体干点啥,但服务算是支棱起来了!!!
0x3、一些常用伎俩
无障碍服务的常用伎俩有这四个:判断无障碍服务是否开启、结点查找、结点交互、全局交互。接着一一讲解:
① 判断无障碍服务是否打开
这个没啥好讲的,直接上工具代码:
fun Context.isAccessibilitySettingsOn(clazz: Class<out AccessibilityService?>): Boolean {
var accessibilityEnabled = false // 判断设备的无障碍功能是否可用
try {
accessibilityEnabled = Settings.Secure.getInt(
applicationContext.contentResolver,
Settings.Secure.ACCESSIBILITY_ENABLED
) == 1
} catch (e: Settings.SettingNotFoundException) {
e.printStackTrace()
}
val mStringColonSplitter = SimpleStringSplitter(':')
if (accessibilityEnabled) {
// 获取启用的无障碍服务
val settingValue: String? = Settings.Secure.getString(
applicationContext.contentResolver,
Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES
)
if (settingValue != null) {
// 遍历判断是否包含我们的服务
mStringColonSplitter.setString(settingValue)
while (mStringColonSplitter.hasNext()) {
val accessibilityService = mStringColonSplitter.next()
if (accessibilityService.equals(
"${packageName}/${clazz.canonicalName}",
ignoreCase = true
)
) return true
}
}
}
return false
}
每次打开我们的APP都调用下这个方法判断无障碍服务是否打开,没有弹窗或者给提示,引导用户去 无障碍设置页设置下,跳转代码如下:
startActivity(Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS))
设置完返回APP,再获取一次服务状态,所以建议在 onResume() 中调用,并做一些对应的UI更新操作。
② 节点查找
比如我要点击某个按钮,我需要先查找到节点,然后再触发点击交互,所以得先定位到节点。下述两个方法可以 获取当前页面节点信息AccessibilityNodeInfo:
AccessibilityEvent.getSource()
AccessibilityService.getRootInActiveWindow()
但要注意两个节点个数不一定相等,而获取到 AccessibilityNodeInfo 实例后可以通过下述方法定位结点(可能匹配到多个,所以返回类型是List<AccessibilityNodeInfo>):
AccessibilityNodeInfo.findAccessibilityNodeInfosByText()
→ 通过Text查找;AccessibilityNodeInfo.findAccessibilityNodeInfosByViewId()
→ 通过节点ID查找
根据文本匹配就不用说了,注意它是contain()包含匹配,不是equals()的方式就好,这里主要说下如何获取 节点ID,需要用到一些工具,前三个是最常见的工具,从旧到新依次是:
1、HierarchyView
老牌分析工具,早期Android SDK有快捷方式,新版找不到了,得自己点击:android-sdk目录下的tools\monitor.bat 启动 Android Device Monitor:
然后点击生成节点数,会dump出节点树,点击相应节点获取所需数据:
直接生成当前页面节点树,方便易用,而且不止布局分析,还有方法调用跟踪、文件管理器等,百宝箱啊,不过小卡,用的时候鼠标一直显示Loading。
2、UI Automator Viewer
比HierarchyView更纯粹,只有生成当前页面节点树的功能,新版同样找不到快捷方式了,得点击
android-sdk目录下的 tools\bin\uiautomatorviewer.bat 启动:
用法也同样简单,而且支持保存节点树,不卡~
3、LayoutInspector
AS 3.0后取消了老旧的DDMS后提供的界面更友好的全新工具,依次点击:Tools → Layout Inspector 打开:
然后选择要监听的进程:
选择完可能会一直转加载不出来,因为默认勾选了 Enable Live Layout Inspector 它会实时加载布局内容,关掉它。
依次点击:File → Settings → Experimental → 找到Layout Inspector → 取消勾选
确定后,此时入口变成了这个:
选择要查看的进程,OK,有多个Windows还可以自行选择:
这里笔者试了几次没load出微信的布局,不知道电脑太辣鸡还是手机问题:
试了一个简单页面倒可以:
还有一点,选进程只能选可debug的进程,所以想调所有进程的话,要么虚拟机,要么Root了的真机,2333,虽然高档,但是用起来没前两个顺手。
4、其它工具
除上面三个之外其它都是一些小众工具了,如 autojs,划出左侧面板 → 打开悬浮框 → 点击悬浮图标展开扇形菜单 → 点击蓝色的 → 选择布局范围分析 → 点击需要获得结点信息的区域。具体步骤如下图所示:
开发者助手等工具获取方式也是类型。这里顺带安利一波笔者在《学亿点有备无患的"姿势"》 写的工具代码 → 获取当前页面所有控件信息,直接调用下方法:
解析好的节点树直接dump出来,获取id就是这么so easy~
③ 节点交互
除了根据ID或文本定位到节点的方法外,还可以调用下述方法进行循环迭代:
- getParent() → 获取父节点;
- getChild() → 获取子节点;
- getChildCount() → 获取节点的子节点数;
获取节点后,可以调用 performAction() 方法对节点执行一个动作,如点击、长按、滑动等,直接上工具代码:
// 点击
fun AccessibilityNodeInfo.click() = performAction(AccessibilityNodeInfo.ACTION_CLICK)
// 长按
fun AccessibilityNodeInfo.longClick() =
performAction(AccessibilityNodeInfo.ACTION_LONG_CLICK)
// 向下滑动一下
fun AccessibilityNodeInfo.scrollForward() =
performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD)
// 向上滑动一下
fun AccessibilityNodeInfo.scrollBackward() =
performAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD)
// 填充文本
fun AccessibilityNodeInfo.input(content: String) = performAction(
AccessibilityNodeInfo.ACTION_SET_TEXT, Bundle().apply {
putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, content)
}
)
④ 全局交互
除了控件触发事件外,AccessibilityService提供了一个 performGlobalAction()
来执行一些通用交互,示例如下:
performGlobalAction(GLOBAL_ACTION_BACK) // 返回键
performGlobalAction(GLOBAL_ACTION_HOME) // Home键
关于AccessibilityService常用伎俩就这些,接着写个超简单的例子来练练手~
0x4、超简单案例:自动登录Windows/Mac微信
登录过微信的PC,下次登录需要在手机上点击确定登录:
我有强迫症,每次下班都会退掉PC的微信,上班再重新登,每次都要点一下,不得不说有点蠢。
完全可以用本节学的姿势写一个自动登录的小jio本啊,简单,也好演示脚本开发的基本流程~
① 判断无障碍服务是否开启
直接在《AccessibilityService基本使用》的代码基础上进行开发,先撸出一个骚气的设置页:
接着是控件初始化,事件设置的一些简单逻辑:
运行下看看效果:
② 看下需要监听什么类型的Event
先把无障碍配置文件里的 android:accessibilityEventTypes
设置为 typeAllMask
,即监听所有类型的Event。接着直接把 onAccessibilityEvent()
的参数 event
打印出来:
运行后,开启无障碍服务,接着点击登录/或者扫二维码,微信弹出登录页面,可以看到下述日志:
即打开登录页会触发 TYPE_WINDOW_STATE_CHANGED
类型的 Event,且页面为 ExtDeviceWXLoginUI
。
行吧,那就只关注这类型的Event,把 android:accessibilityFeedbackType
设置为 typeWindowStateChanged
,改下 onAccessibilityEvent()
里的处理逻辑:
③ 找到登录按钮并触发点击
懒得用工具扣,直接用adb的脚本打印出节点树,直接就定位要找的节点了:
行吧,可以根据文本查找,也可以根据id查找,前者是contain()的方式匹配,包含登录文本的节点都会被选中:
而这里的id是唯一的,所以直接根据id进行查找,找到后触发点击:
运行下看看效果:
脚本检测到登录页面,自动点击登录按钮,迅雷不及掩耳之势页面就关了~
0x5、小结
本节过了一下 AccessibilityService无障碍服务 的基础姿势,并写了一个超简单的微信自动登录案例演示脚本编写的大概过程,相信读者学完可以动手尝试编写一些简单的脚本。而在实际开发中还会遇到一些问题,如:获取到控件,但无法点击,在后续实战环节中会一一涉猎,剧透下,下一节会带着大家来开发一个:微信僵尸好友检测工具,敬请期待~
参考文献
- 官方文档:《创建自己的无障碍服务》
- AccessibilityService使用入门
- (AccessibilityService) Android 辅助功能笔记
- 辅助功能 AccessibilityService笔记(2)
附录:属性、参数、可选值详解
Tips:下述内容可能过时,或者有部分不准确,建议以官方文档和源码为准
android:accessibilityEventTypes → AccessibilityServiceInfo.eventTypes
服务监听的事件类型,可选值有这些,支持多个,属性值用|分隔,代码设置值用or分隔
描述 | xml属性值 | 代码设置值 |
---|---|---|
所有类型的事件 | typeAllMask | xxx |
一个应用产生一个通知事件 | typeAnnouncement | TYPE_ANNOUNCEMENT |
辅助用户读取当前屏幕事件 | typeAssistReadingContext | TYPE_ASSIST_READING_CONTEXT |
view中上下文点击事件 | typeContextClicked | TYPE_VIEW_CONTEXT_CLICKED |
监测到的手势事件完成 | typeGestureDetectionEnd | TYPE_GESTURE_DETECTION_END |
开始手势监测事件 | typeGestureDetectionStart | TYPE_GESTURE_DETECTION_START |
Notification变化事件 | typeNotificationStateChanged | TYPE_NOTIFICATION_STATE_CHANGED |
触摸浏览事件完成 | typeTouchExplorationGestureEnd | TYPE_TOUCH_EXPLORATION_GESTURE_END |
触摸浏览事件开始 | typeTouchExplorationGestureStart | TYPE_TOUCH_EXPLORATION_GESTURE_START |
用户触屏事件结束 | typeTouchInteractionEnd | TYPE_TOUCH_INTERACTION_END |
触摸屏幕事件开始 | typeTouchInteractionStart | TYPE_TOUCH_INTERACTION_START |
无障碍焦点事件清除 | typeViewAccessibilityFocusCleared | TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED |
获得无障碍的焦点事件 | typeViewAccessibilityFocused | TYPE_VIEW_ACCESSIBILITY_FOCUSED |
View被点击 | typeViewClicked | TYPE_VIEW_CLICKED |
View被长按 | typeViewLongClicked | TYPE_VIEW_LONG_CLICKED |
View被选中 | typeViewSelected | TYPE_VIEW_SELECTED |
View获得焦点 | typeViewFocused | TYPE_VIEW_FOCUSED |
一个View进入悬停 | typeViewHoverEnter | TYPE_VIEW_HOVER_ENTER |
一个View退出悬停 | typeViewHoverExit | TYPE_VIEW_HOVER_EXIT |
View滚动 | typeViewScrolled | TYPE_VIEW_SCROLLED |
View文本变化 | typeViewTextChanged | TYPE_VIEW_TEXT_CHANGED |
View文字选中发生改变事件 | typeViewTextSelectionChanged | TYPE_VIEW_TEXT_SELECTION_CHANGED |
窗口的内容发生变化,或子树根布局发生变化 | typeWindowContentChanged | TYPE_WINDOW_CONTENT_CHANGE |
新的弹出层导致的窗口变化(dialog、menu、popupwindow) | typeWindowStateChanged | TYPE_WINDOW_STATE_CHANGED |
屏幕上的窗口变化事件,需要API 21+ | typeWindowsChanged | TYPE_WINDOWS_CHANGED |
UIanimator中在一个视图文本中进行遍历会产生这个事件,多个粒度遍历文本。一般用于语音阅读context | typeViewTextTraversedAtMovementGranularity | TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY |
android:accessibilityFeedbackType → AccessibilityServiceInfo.feedbackType
操作相关按钮后,服务给用户的反馈类型,可选值如下:
描述 | xml属性值 | 代码设置值 |
---|---|---|
取消所有的反馈方式,一般用这个 | feedbackAllMask | FEEDBACK_ALL_MASK |
可听见的(非语音反馈) | feedbackAudible | FEEDBACK_AUDIBLE |
通用反馈 | feedbackGeneric | FEEDBACK_GENERIC |
触觉反馈(震动) | feedbackHaptic | FEEDBACK_HAPTIC |
语音反馈 | feedbackSpoken | FEEDBACK_SPOKEN |
视觉反馈 | feedbackVisual | FEEDBACK_VISUAL |
盲文反馈 | 不支持 | FEEDBACK_BRAILLE |
android:accessibilityFlags → AccessibilityServiceInfo.flags
辅助功能附加的标志,可选值有这些,支持多个,属性值用|分隔,代码设置值用or分隔:
描述 | xml属性值 | 代码设置值 |
---|---|---|
默认配置 | flagDefault | DEFAULT |
为WebView中呈现的内容提供更好的辅助功能支持 | flagRequestEnhancedWebAccessibility | FLAG_REQUEST_ENHANCED_WEB_ACCESSIBILITY |
使用该flag表示可获取到view的ID | flagReportViewIds | FLAG_REPORT_VIEW_IDS |
获取到一些被表示为辅助功能无权获取到的view | flagIncludeNotImportantViews | FLAG_INCLUDE_NOT_IMPORTANT_VIEWS |
监听系统的物理按键 | flagRequestFilterKeyEvents | FLAG_REQUEST_FILTER_KEY_EVENTS |
监听系统的指纹手势 API 26+ | flagRequestFingerprintGestures | FLAG_REQUEST_FINGERPRINT_GESTURES |
系统进入触控探索模式,出现一个鼠标在用户的界面 | flagRequestTouchExplorationMode | FLAG_REQUEST_TOUCH_EXPLORATION_MODE |
如果辅助功能可用,提供一个辅助功能按钮在系统的导航栏 API 26+ | flagRequestAccessibilityButton | FLAG_REQUEST_ACCESSIBILITY_BUTTON |
要访问所有交互式窗口内容的系统,这个标志没有被设置时,服务不会收到TYPE_WINDOWS_CHANGE事件 | flagRetrieveInteractiveWindows | FLAG_RETRIEVE_INTERACTIVE_WINDOWS |
系统内所有的音频通道,使用由STREAM_ACCESSIBILTY音量控制USAGE_ASSISTANCE_ACCESSIBILITY | flagEnableAccessibilityVolume | FLAG_ENABLE_ACCESSIBILITY_VOLUME |
android:canRetrieveWindowContent
服务是否能取回活动窗口内容的属性,与flagRetrieveInteractiveWindows搭配使用,无法在运行时更改此配置。
android:notificationTimeout → AccessibilityServiceInfo.notificationTimeout
同一类型的两个辅助功能事件发送到服务的最短间隔(毫秒,两个辅助功能事件之间的最小周期)
android:packageNames → AccessibilityServiceInfo.packageNames
监听的应用包名,多个用逗号(,)隔开,两种方式设置监听所有应用的事件:
- 不设置此属性;
- 赋值null → android:packageNames="@null"
网上一堆说空字符串的,都是没经过验证的,用空字符串你啥都捕获不到!!!
android:settingsActivity → AccessibilityServiceInfo.settingsActivityName
允许修改辅助功能的activity类名,就是你自己的无障碍服务的设置页。
android:description
该服务的简单说明,会显示在无障碍服务说明页:
android:canPerformGestures
是否可以执行手势,API 24新增
链接:https://juejin.cn/post/7169033859894345765
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。