Android 辅助功能服务AccessibilityService

Android提供辅助功能服务的目的在于帮助那些具有视觉、身体或年龄相关限制的用户更轻松的使用Android设备和应用,例如当用户悬停在屏幕的重要区域上时将文本转换为语音或触觉反馈,从而使一些有视力缺陷的用户也能够使用。除此之外,我们还可以使用AccessibilityService将一些人工操作进行自动化处理,从而将人从这些无聊繁琐的重复操作中解放出来。

1、首先需要定义一个继承自AccessibilityService的服务(这里命名为MyAccessibilityService),并在AndroidManifest中声明:

<service
    android:name=".MyAccessibilityService"
    android:description="@string/accessibility_service_description"
    android:label="@string/accessibility_service_label"
    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/accessibility_service_config"/>
</service>

其中accessibility_service_description是有关这个服务的功能说明,accessibility_service_label是服务的名称,这些会在设置里面的辅助服务中显示出来,用户需要在里面开启服务才能使用。permissionintent-filter是固定写法。meta-data主要用于对服务进行一些配置,配置的具体内容在 res/xml/accessibility_service_config.xml 文件里面:

<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:description="@string/accessibility_service_description"
    android:packageNames="com.example.android.apis"
    android:accessibilityEventTypes="typeAllMask"
    android:accessibilityFlags="flagDefault"
    android:accessibilityFeedbackType="feedbackSpoken"
    android:notificationTimeout="100"
    android:canRetrieveWindowContent="true"
    android:settingsActivity="com.example.android.accessibility.ServiceSettingsActivity"/>

服务的配置选项具体可参考 https://developer.android.com/reference/android/accessibilityservice/AccessibilityServiceInfo.html 这里仅解释一下几个比较重要的配置属性:
android:packageNames服务要监控的应用的包名,如果有多个则用逗号连起来,空着表示监听所有的应用。
android:accessibilityEventTypes服务要监控的事件类型,如通知、窗口改变、点击、焦点改变等等,如果有多个可以用 | 连起来,typeAllMask代表所有类型。
android:accessibilityFeedbackType服务反馈的方式,如语音、震动等等,feedbackAllMask代表所有类型。
android:notificationTimeout 接受事件的时间间隔(毫秒)
android:canRetrieveWindowContent 服务能否获取窗口里面的内容
这些配置除了在xml里面写之外,还可以在代码中建立一个AccessibilityServiceInfo对象,然后通过setServiceInfo()来设置。

2、辅助服务继承AccessibilityService类并覆盖该类中的以下方法:
onServiceConnected 系统成功连接到辅助功能服务时调用,可以执行执行任何一次性设置步骤,包括连接到用户反馈系统服务,如音频管理器或设备振动器。还可以在此调用setServiceInfo()设置服务配置。
onAccessibilityEvent 当系统检测到与Accessibility服务指定的事件过滤参数匹配的AccessibilityEvent时调用。这是必须实现的方法,通常需要在该方法中根据AccessibilityEvent作出判断并执行一些处理。
onInterrupt 当系统想要中断服务提供的反馈时调用,通常是响应用户操作,如将焦点移动到其他控件。
onUnbindonDestroy 当系统即将关闭辅助功能服务时调用,可以执行任何一次性关机程序,包括取消分配用户反馈系统服务,例如音频管理器或设备振动器。

3、AccessibilityEvent 是辅助功能服务中一个非常重要的类,它主要用于提供有关用户界面交互的信息。当用户界面中发生了服务需要关注的事件时系统就会发送AccessibilityEvent事件,并传递到onAccessibilityEvent方法。通常,用得比较多的是event.getEventType()event.getClassName(),分别用于获取当前事件的类型和发生该事件的类名,通过这两个的判断可以过滤想要处理的事件,并进行操作。例如,当点击一个按钮时,会发送一个type为TYPE_VIEW_CLICKED,className为android.widget.Button的事件,如果我们需要在某个按钮被点击时做一些操作,就可以在onAccessibilityEvent中对event进行判断。
AccessibilityEvent 的Type包括:

TYPE_VIEW_CLICKED
TYPE_VIEW_LONG_CLICKED
TYPE_VIEW_FOCUSED
TYPE_VIEW_SELECTED
TYPE_VIEW_TEXT_CHANGED
TYPE_WINDOW_STATE_CHANGED
TYPE_NOTIFICATION_STATE_CHANGED
TYPE_TOUCH_EXPLORATION_GESTURE_START
TYPE_TOUCH_EXPLORATION_GESTURE_END
TYPE_VIEW_HOVER_ENTER
TYPE_VIEW_HOVER_EXIT
TYPE_VIEW_SCROLLED
TYPE_VIEW_TEXT_SELECTION_CHANGED
TYPE_WINDOW_CONTENT_CHANGED
TYPE_ANNOUNCEMENT
TYPE_GESTURE_DETECTION_START
TYPE_GESTURE_DETECTION_END
TYPE_TOUCH_INTERACTION_START
TYPE_TOUCH_INTERACTION_END
TYPE_VIEW_ACCESSIBILITY_FOCUSED
TYPE_WINDOWS_CHANGED
TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED

4、AccessibilityNodeInfo 同样是辅助功能服务中一个非常重要的类,它代表了一个View的状态信息。我们可以通过event.getSource()获取发生事件的控件的信息,也可以通过getRootInActiveWindow()获取当前窗口中的根节点的信息。前提是服务配置中android:canRetrieveWindowContenttrue,并且发生事件的窗口为当前窗口,否则这两个方法返回的值都为null
那么,我们得到了AccessibilityNodeInfo能做什么呢?AccessibilityNodeInfo和View一样,也是一个具有层级关系的节点树,一个AccessibilityNodeInfo里面可以有多个AccessibilityNodeInfo的子节点,当我们想要获取一个目标节点时,可以先获取根节点,然后再通过递归遍历其子节点去寻找,也可以通过findAccessibilityNodeInfosByViewId()findAccessibilityNodeInfosByText()方法去寻找。当找到目标节点后,可以对其执行想要的操作,比如点击,滚动,填入文字等等。
举个例子,当我们想要在当前界面点击第一个文字为“确定”的按钮时:

AccessibilityNodeInfo rootNode = getRootInActiveWindow();
if (rootNode != null) {
    List<AccessibilityNodeInfo> nodes = rootNode.findAccessibilityNodeInfosByText("确定");
    for (AccessibilityNodeInfo node : nodes) {
        if (TextUtils.equals(node.getText(), "确定") && TextUtils.equals(node.getClassName(), "android.widget.Button")) {
            node.performAction(AccessibilityNodeInfo.ACTION_CLICK);
        }
    }
}

首先调用getRootInActiveWindow()获取当前界面的根节点,然后再通过findAccessibilityNodeInfosByText()找到所有包含目标文字的节点(注意,这里是包含目标文字,而非完全与目标文字相同),之后再遍历这个列表,找到第一个Text完全等于目标文字,且控件类型为Button的节点,最后再调用performAction(AccessibilityNodeInfo.ACTION_CLICK)执行点击事件。
AccessibilityNodeInfo 中的Action包括:

ACTION_ACCESSIBILITY_FOCUS
ACTION_ARGUMENT_COLUMN_INT
ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN
ACTION_ARGUMENT_HTML_ELEMENT_STRING
ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT
ACTION_ARGUMENT_PROGRESS_VALUE
ACTION_ARGUMENT_ROW_INT
ACTION_ARGUMENT_SELECTION_END_INT
ACTION_ARGUMENT_SELECTION_START_INT
ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE
ACTION_CLEAR_ACCESSIBILITY_FOCUS
ACTION_CLEAR_FOCUS
ACTION_CLEAR_SELECTION
ACTION_CLICK
ACTION_COLLAPSE
ACTION_COPY
ACTION_CUT
ACTION_DISMISS
ACTION_EXPAND
ACTION_FOCUS
ACTION_LONG_CLICK
ACTION_NEXT_AT_MOVEMENT_GRANULARITY
ACTION_NEXT_HTML_ELEMENT
ACTION_PASTE
ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY
ACTION_PREVIOUS_HTML_ELEMENT
ACTION_SCROLL_BACKWARD
ACTION_SCROLL_FORWARD
ACTION_SELECT
ACTION_SET_SELECTION
ACTION_SET_TEXT
FOCUS_ACCESSIBILITY
FOCUS_INPUT
MOVEMENT_GRANULARITY_CHARACTER
MOVEMENT_GRANULARITY_LINE
MOVEMENT_GRANULARITY_PAGE
MOVEMENT_GRANULARITY_PARAGRAPH
MOVEMENT_GRANULARITY_WORD

其中用的比较多的有点击、长按、滚动等等,如果想为EditText设置文字可以用:

Bundle arguments = new Bundle();
arguments.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, text);
inputNode.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    打开长按电源键的弹框

推荐阅读更多精彩内容