AccessibilityService的研究与应用(一)

1,889 阅读4分钟

AccessibilityService的功能非常强大,google的本意是通过AccessibilityService帮助残疾用户更好的使用安卓手机,所以AccessibilityService又名无障碍辅助服务,但是正是应为AccessibilityService的特殊性与可操作性,我们才能通过它做一些有意思的功能或产品。

前两年特别火的微信抢红包插件和支付宝自动收取能量的插件特别火,这些功能其实都是使用AccessibilityService实现的。它们的原理就是遍历view结构,找到对应的node节点执行动作,即可实现相应的功能。我刚开始接触辅助功能的时候,去网上搜索资料就看到了微信抢红包和支付宝收取能量的插件,感兴趣的同学可以自行去网上查找相关的文章。

下面我们先看下辅助功能的常规用法。

1.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="flagReportViewIds|flagRetrieveInteractiveWindows|flagRequestEnhancedWebAccessibility"
    android:canPerformGestures="true"
    android:canRetrieveWindowContent="true"
    android:description="@string/accessibility"
    android:notificationTimeout="1000"
    />

首先要现在xml文件夹下建立accessibility-service,此处是固定写法,用于在清单文件注册辅助功能的服务。

accessibilityEventTypes:表示要监控的事件或者说是动作,比如通知栏收到推送消息、界面文本发生变动
accessibilityFeedbackType:表示反馈的类型,震动或者语音播放
accessibilityFlags:表示辅助功能查找节点的方式方法
canPerformGestures:表示是否接受7.0系统以上的手势分发
canRetrieveWindowContent:表示辅助功能是否可以获取活动window的内容和节点
description:表示描述,在设置中开启辅助功能下面的文字描述,给用户看的
notificationTimeout:表示两个相同类型的可访问性事件之间的最短时间段(以毫秒为单位)被发送到此服务
packageNames:表示指定app生效,如果不设置则效果是全局的

2.集成AccessibilityService,实现自定义的辅助功能监听器

public class MyService extends AccessibilityService {

    @Override
    protected void onServiceConnected() {
        super.onServiceConnected();
        // 服务连接方法
    }

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        // event
    }

    @Override
    public void onInterrupt() {
        // 中断
    }
    
    @Override
    public void onDestroy() {
        super.onDestroy();
        // 服务被销毁
    }
}

此处我们需要继承AccessibilitySerivce,才能正常的使用辅助功能。一般来说,我们会在onAccessibilityEvent中监听event的type,针对于指定的type再去做相关的操作。

3.清单文件中注册辅助功能

<service
    android:name=".MyService"
    android:enabled="true"
    android:exported="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/accessibility"/>
</service>

此处的写法基本上都是固定的,除了xml引用出以及服务的名称。辅助功能本质上也是一个service,如果不在清单文件中注册,是无法正常使用的。

4.获取活动窗口或者非活动窗口

event.getSource():获取传递出事件的事件源的AccessibilityNodeInfo
getWindows():获取默认显示并且用户可以与之交互的窗口
getRootInActiveWindow():获取当前活动窗口的根节点AccessibilityNodeInfo

我们可以通过这几种获取窗口和根节点的方法去查找符合要求的节点。目前有根据text和id查找的方法。

5.node节点查找方法

/**
 * 根据文本获取对应的节点
 * @param service
 * @param text 搜索的文本
 * @param viewId 搜索的viewId 用于辅助检测 可不传
 * @return
 */
private AccessibilityNodeInfo findChildByText(AccessibilityService service,String text,String viewId) {
    if (service == null || TextUtils.isEmpty(text)) {
        return null;
    }
    AccessibilityNodeInfo rootInfo = getRootInfo(service);
    if (rootInfo == null) {
        return null;
    }
    List<AccessibilityNodeInfo> nodeInfos = rootInfo.findAccessibilityNodeInfosByText(text);
    if (nodeInfos != null && nodeInfos.size() > 0) {
        for (AccessibilityNodeInfo info : nodeInfos) {
            if (info == null) {
                continue;
            }
            CharSequence contentText = info.getText();
            if (TextUtils.isEmpty(contentText)) {
                info.recycle();
                continue;
            }
            if (text.equals(contentText)) {
                if (TextUtils.isEmpty(viewId)) {
                    return info;
                }else {
                    String viewIdResourceName = info.getViewIdResourceName();
                    if (viewId.equals(viewIdResourceName)) {
                        return info;
                    }
                }
            }

        }
    }
    return null;
}



/**
 * 根据viewId搜索对应的节点元素 原理同上
 * @param service
 * @param viewId
 * @param text
 * @return
 */
private AccessibilityNodeInfo findChildByViewId(AccessibilityService service,String viewId,String text) {
    if (service == null || TextUtils.isEmpty(viewId)) {
        return null;
    }
    AccessibilityNodeInfo rootInfo = getRootInfo(service);
    if (rootInfo == null) {
        return null;
    }
    List<AccessibilityNodeInfo> nodeInfos = rootInfo.findAccessibilityNodeInfosByViewId(viewId);
    if (nodeInfos != null && nodeInfos.size() > 0) {
        if (TextUtils.isEmpty(text)) {
            return nodeInfos.get(0);
        }else {
            for (AccessibilityNodeInfo info : nodeInfos) {
                if (info == null) {
                    continue;
                }
                CharSequence contentText = info.getText();
                if (TextUtils.isEmpty(contentText)) {
                    info.recycle();
                    continue;
                }
                if (text.equals(contentText)) {
                    if (TextUtils.isEmpty(viewId)) {
                        return info;
                    }else {
                        info.recycle();
                    }
                }
            }
        }
    }
    return null;
}

上面是根据text和id查找节点的两种方式,需要注意根据text和id查询都有可能查询到重复的node节点。尤其是text方式。而id的方式目前一些app对其进行了加密设置,每次的id都是可变的,导致无法使用id的查找方式。

获取活动窗口之后,查询对应的node节点,切记使用完之后需要对节点进行回收操作。

此处的两种方式针对于webview或者自定义view可能不生效,无法对应的节点。这一点之后的文章会进行介绍,如何通过深度遍历搜索view树,然后在匹配到我们想要的节点。

6.获取node节点之后,我们需要执行相应的动作,类似长按,单击或者滑动。方法为performAction

ACTION_CLICK:模拟点击
ACTION_SELECT:模拟选中
ACTION_LONG_CLICK:模拟长按
ACTION_SCROLL_FORWARD:模拟往前滚动
ACTION_SCROLL_BACKWARD:模拟向后滚动

此方法有返回值,可以通过返回值看动作是否执行成功。另外还可以传递bundle参数。

如果是针对于某个app做单独的辅助功能检测,那么这种方式还是挺靠谱的。需要找view的id再去查询对应节点的话,可以使用androidstudio的工具Layout Inspector,连接上真机之后就可以看到view的树结构,找到对应的id。