Android小记 —— AccessibilityService

5,480 阅读5分钟

AccessibilityService 是什么

  • 常规功能:是一种辅助性的人机交互方式

AccessibilityService 设计的目的就是帮助残障用户更好地使用手机,并希望开发者仅为了这个目进行开发。屏幕阅读器(Talkback)是正确用法的一个示范,点击屏幕后TTS语音读出触摸到的位置的内容,有视力障碍的用户就可以通过声音了解屏幕上发生了什么以及应该怎么操作。

  • 意外功能:自动抢红包等自动操作程序

中文互联网环境下,AccessibilityService 最常见的用法就是在抢红包,全自动化的流程比人类的反应快上不少,甚至曾经有些手机产商的系统就提供这种功能。虽然不是好的行为,但也是技术在生活中的一种应用。

AccessibilityService 还有很多奇特的用法,除了方便用户、让手机更好用之外,它还能让手机更难用。比如这个「格雷盒子」,使用 AccessibilityService 进行了自动化申请权限和修改系统设置,还能限制用户跳转到禁用的App中,实现了比较严格的家长控制功能。

AccessibilityService 是具有一定风险的功能,开启服务后用户隐私是根本无法保护的,用户收到的通知、屏幕上显示的大部分内容、各种输入框甚至未做特殊处理的密码输入框里的内容都可以完全静默获取。强烈建议非必须的功能不要通过AccessibilityService实现。

AccessibilityService 怎么用

1. 配置服务

继承 AccessibilityService,自定义 Service

class NewAccessibilityService: AccessibilityService() {
    override fun onServiceConnected() {
        super.onServiceConnected()
        // 用户在设置中启动了服务
    }

    override fun onUnbind(intent: Intent?): Boolean {
        // 服务被结束
        return super.onUnbind(intent)
    }

    override fun onInterrupt() {
        // feedback 被打断
    }

    override fun onAccessibilityEvent(event: AccessibilityEvent?) {
        // 事件回调
    }
}

完善 AndroidManifest.xml 中的注册信息

<service android:name=".access.NewAccessibilityService"
    android:label="辅助功能名称"
    android:enabled="true"
    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/custom_config" />
</service>

custom_config.xml 的内容

<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:description="@string/prts_as_desp"
    android:accessibilityEventTypes="typeAllMask"
    android:accessibilityFlags="flagDefault"
    android:accessibilityFeedbackType="feedbackAllMask"
    android:notificationTimeout="100"
    android:canRetrieveWindowContent="true"
    android:canPerformGestures="true"
    android:packageNames="com.tencent.mm">

</accessibility-service>

2. 筛选事件

事件回调是驱动 AccessibilityService 的核心,无障碍服务实现的难点之一是在众多事件中准确定位我们需要的那个 AccessibilityEvent。

区分事件的维度主要有以下几个:

  1. eventType
  2. packageName
  3. className
  4. text
  5. contentDescription
  6. contentChangeTypes

通常需要结合多个参数的值才能确定事件的处理方式。

比较常见的 eventType 有这几个:

  • TYPE_VIEW_CLICKED | 点击View
  • TYPE_VIEW_TEXT_CHANGED | EditText中文本变化
  • TYPE_WINDOW_STATE_CHANGED | 窗口状态变化(比如切换了Activity)
  • TYPE_NOTIFICATION_STATE_CHANGED | 收到通知
  • TYPE_WINDOW_CONTENT_CHANGED | 窗口内容变化(比如View树发生变化)
  • TYPE_VIEW_SCROLLED | 滑动View

完整文档看这里:developer.android.google.cn/reference/k…

text 的内容是一个列表,内容包含 UI 显示的文本的时候可能被当前设置语言影响,使用 text 进行过滤的时候需要注意多语言的支持。

contentChangeTypes 仅在 eventType 为 TYPE_WINDOW_CONTENT_CHANGED 或者 TYPE_WINDOW_STATE_CHANGED 的时候有效,要注意返回的 Int 值是一个 bit mask。

3. 做出响应

对事件作出何种响应就由具体的需求决定了,当设置了 canRetrieveWindowContent 为 true 的时候,可以通过 event.getSource() 或者 getRootInActiveWindow() 获取当前页面中的 AccessibilityNodeInfo 树的根节点。AccessibilityNodeInfo 节点与 View 树结构大致相同,找到需要的节点之后可以用 nodeInfo.performAction(action) 操作 View。

寻找需要的节点也是一个难点,AccessibilityNodeInfo 中提供了两个 find 方法简化调用代码:findAccessibilityNodeInfosByViewIdfindAccessibilityNodeInfosByText。分别是通过 View 的 id 和 View 的 text 判断,返回值都是列表,之后还需要进一步判断。

View 之外的操作通过 performGlobalAction(action) 调用,比如系统的虚拟导航操作或者物理键。目前 GlobalAction 有这些:

  • GLOBAL_ACTION_BACK
  • GLOBAL_ACTION_HOME
  • GLOBAL_ACTION_RECENTS
  • GLOBAL_ACTION_NOTIFICATIONS
  • GLOBAL_ACTION_QUICK_SETTINGS
  • GLOBAL_ACTION_POWER_DIALOG
  • GLOBAL_ACTION_TOGGLE_SPLIT_SCREEN
  • GLOBAL_ACTION_LOCK_SCREEN
  • GLOBAL_ACTION_TAKE_SCREENSHOT

基于 View 体系的 AccessibilityNodeInfo 能进行的操作还是有限的,具体到点击某个 View 的某个区域或者从一个 View 的某一处开始滑动到哪里,就控制不了了。于是 API 24 之后增加了 dispatchGesture 函数实现完全的模拟用户手势操作。

dispatchGesture 的参数 GestureDescription 描述了一个手势操作,由一到多个 StrokeDescription 组成。每个 StrokeDescription 包含手势的 Path(没错,是我们熟悉的android.graphics.Path)、启动延迟时间和手势持续时间,组合这些参数可以实现相当复杂的轨迹绘制以及多点触控等精密操作。

一个例子:微信自动回复

markdown 放不了视频,截屏做了个gif:

整个流程无手动控制。

另一个例子:定位连续点击

有些手游有自律功能,但选择关卡和确认进入的地方还要用户操作,就是不支持自动连刷。AccessibilityService 可以帮助用户解放双手。

手游的页面是一整个 View,不能通过 AccessibilityNodeInfo 拿到任何有效信息。比较合理的解决方法是增加录屏功能,定期获取截取屏幕图像进行数字图像处理,通过某些特征值判断当前所在页面(比如缩放后使用预置特征区域截图进行模板匹配之类的)。

考虑到需要缩放,其实游戏的 UI 在适配了不同屏幕尺寸之后,一个按钮在屏幕里的相对位置是很固定的,所以直接通过按钮在屏幕中的相对位置划定区域,定时去点击对应区域也能满足需求。但缺点是稳定性差点,定时的延迟是固定的,中途被打断就难以恢复自动了。

录制了一段明日方舟经典关卡「主线1-7」的连刷视频,时间比较长,转成 gif 也有 70M+,只能上传 bilibili 了,点此跳转观看,由于是竖屏截图,效果不太好。


两个例子是写在一个项目中的,还在更新别的自用功能,完整代码就不放了,想了解的话单独找我要就好了。

以上内容只是 AccessibilityService 的一部分,如有错误欢迎评论指出。