AccessiblityService知多少

avatar
Android @奇舞团Android团队

最近研究了自动化操作的相关事宜,辅助服务就是其中一项技术。下面介绍一下相关方面技术。这项技术可以用作抢红包、App自动安装卸载、页面内容抓取,WX消息的自动发送、自动发送朋友圈,H5页面内容抓取也可以。

原理

对于那些由于视力、听力或其它身体原因导致不能方便使用Android智能手机的用户,Android提供了Accessibility功能和服务辅助这些用户更加简单地操作设备,包括文字转语音(不支持中文)、触觉反馈、手势操作、轨迹球和手柄操作。开发者可以搭建自己的Accessibility服务,这可以加强可用性,例如声音提示,物理反馈,和其他可选的操作模式。

AccessibilityService是一个运行在后台的服务,有相关回调方法onAccessibilityEvent(AccessibilityEvent event)能够收到由系统发出的一些事件。界面中产生的任何变化都会产生一个事件,并由系统通知给AccessibilityService这就像监视器监视着界面的一举一动,一旦界面发生变化,立刻进行相关操作。

实现

Api 4就已经加入了这项技术,但Android 4.1算是一个分水岭,此版本以后,系统辅助服务增加了与窗口元素的双向交互,可以通过辅助功能服务操作窗口元素,比如点击按钮等

AccessibilityService 实现


import android.accessibilityservice.AccessibilityService;
import android.view.accessibility.AccessibilityEvent;

public class AutoAccessibilityService extends AccessibilityService {

    @Override
    protected void onServiceConnected() {
        super.onServiceConnected();
        //当我们当服务被系统服务链接成功后,这里可以做一些初始化当工作
    }

    @Override
    public void onAccessibilityEvent(AccessibilityEvent accessibilityEvent) {
        int eventType = accessibilityEvent.getEventType();  
        //事件监听回调,开发者可以做很多事情
        switch (eventType) {
            //当窗口的状态发生改变时
            case AccessibilityEvent.TYPE_WINDOWS_CHANGED://窗口变化
                break;
            case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED://对话框、popupwindow、菜单
                 break;
            case AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED://通知
                 break;
            case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED://基于当前event.source中子view的内容变化
                 break;
                .....
                .....
        }
    }

    @Override
    public void onInterrupt() {
       //这个在系统想要中断AccessibilityService返给的响应时会调用。在整个生命周期里会被调用多次。
    }
    
    @Override
    public boolean onUnbind(Intent intent) {
        return super.onUnbind(intent);
        //在系统将要关闭Service时被调用。在这个方法主要做释放资源的工作。
    }
}

AndroidMenifest.xml

<service
    android:name=".AutoAccessibilityService"
    android:enabled="true"
    android:exported="true"
    android:canRetrieveWindowContent="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_config" />
</service>

res/xml/accessibility_service_config

<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
 android:accessibilityEventTypes="typeNotificationStateChanged|typeWindowStateChanged|typeWindowContentChanged|typeWindowsChanged"
    android:accessibilityFeedbackType="feedbackGeneric"
    android:accessibilityFlags="flagDefault|flagRequestEnhancedWebAccessibility"
    android:canRetrieveWindowContent="true"
    android:description="@string/app_name"
    android:notificationTimeout="100"
    android:packageNames="com.weibo"//这是我们要监听的包名
    android:canRequestEnhancedWebAccessibility="true"
    />

服务状态监控回调

added API level 14

AccessibilityManager accessibilityManager = (AccessibilityManager) getSystemService(Context.ACCESSIBILITY_SERVICE);
accessibilityManager.addAccessibilityStateChangeListener(new AccessibilityManager.AccessibilityStateChangeListener() {
        @Override
        public void onAccessibilityStateChanged(boolean isChange) {
            Log.i(TAG,"onAccessibilityStateChanged isChange = " + isChange);
            if(isChange){
                 //do something
            }else{
                //do something
            }
        }
    });

以上就是最基本的实现,但是这个辅助服务需要用户手动开启授权才能用,当然某些黑科技下也是可以不用手动开启的,比如,电脑通过adb命令来控制已经root的手机。呵呵~

举例

抢红包的插件、微信消息的自动发送、自动发送朋友圈网上很多了,就不说了。下面说一下像H5页面的内容怎么抓取,直接上代码:

1、遍历当前页面每个节点


AccessibilityNodeInfo accessibilityNodeInfo = getRootInActiveWindow();//获取当前页面的跟页面的节点
for (int i = 0; i < accessibilityNodeInfo.getChildCount(); i++) {
   AccessibilityNodeInfo child = accessibilityNodeInfo.getChild(i);
   findEveryViewNode(child);
}

2、找出每个节点的子节点的信息

public void findEveryViewNode(AccessibilityNodeInfo node) {
        if (null != node && node.getChildCount() > 0) {
            for (int i = 0; i < node.getChildCount(); i++) {
                AccessibilityNodeInfo child = node.getChild(i);
                if (child == null) {
                    continue;
                }
                Log.i(TAG, "节点数据  ======= " +
                        ",text = " + child.getText()
                        + ", descript = " + child.getContentDescription() +
                        ", className = " + child.getClassName() +
                        ", resId = " + child.getViewIdResourceName());
                // 递归调用
                findEveryViewNode(child);
            }
        }
    }

这里需要注意的是,某些情况下,返回的节点信息会是null。

有了神操作,在自动化的路上,无往不利。

方法举例

/**
 * 模拟点击事件
 */
public void performViewClick(AccessibilityNodeInfo nodeInfo) {
    if (nodeInfo == null) {
        return;
    }
    while (nodeInfo != null) {
        if (nodeInfo.isClickable()) {
            nodeInfo.performAction(AccessibilityNodeInfo.ACTION_CLICK);
            break;
        }
        nodeInfo = nodeInfo.getParent();
    }
}

/**
 * 模拟返回操作
 */
public void performBackClick() {
    performGlobalAction(GLOBAL_ACTION_BACK);
}

/**
 * 模拟Home操作
 */
public void performHomeClick() {
    performGlobalAction(GLOBAL_ACTION_HOME);
}

/**
 * 模拟下滑操作
 */
public void performScrollBackward() {
    performGlobalAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
}

/**
 * 模拟上滑操作
 */
public void performScrollForward() {
    performGlobalAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
}

方法还有很多这里就不一一列举。

源码分析

以点击事件为例,AccessibilityService是如何做到监控捕捉用户行为的

1、AccessibilityEvent产生:

View#performClick()
    -> View#sendAccessibilityEventUncheckedInternal()
    -> ViewGroup#requestSendAccessibilityEvent()
    -> ViewRootImpl#requestSendAccessibilityEvent()
    -> AccessibilityManager#sendAccessibilityEvent(event)
    -> AccessibilityManagerService#sendAccessibilityEvent()
    -> AccessibilityManagerService#notifyAccessibilityServicesDelayedLocked()
    -> Service#notifyAccessibilityEvent(event)

2、AccessibilityEvent处理:

AccessibilityEvent
    -> Binder
    -> IAccessibilityServiceClientWrapper#onAccessibilityEvent(AccessibilityEvent)
    -> HandlerCaller#sendMessage(message); // message中包括AccessibilityEvent
    -> IAccessibilityServiceClientWrapper#executeMessage()
    -> Callbacks#onAccessibilityEvent(event)
    -> AccessibilityService.this.onAccessibilityEvent(event)

总结

在研究这些技术时,深感系统的良心大作,用来给使用手机困难的人使用,同时也提供了使用手机方便的人更加方便。

开发人员的困难点就是对目标程序的研究,分析出各个页面以及节点的信息,并对目的操作流程细化抽象,最终完成目标。

工具推荐:UI Automator Viewer分析页面元素

关注微信公众号,最新技术干货实时推送

image