AccessibilityService使用入门

AccessibilityService设计初衷在于帮助残障用户使用android设备和应用,在后台运行,可以监听用户界面的一些状态转换,例如页面切换、焦点改变、通知、Toast等,并在触发AccessibilityEvents时由系统接收回调。后来被开发者另辟蹊径,用于一些插件开发,比如微信红包助手,还有一些需要监听第三方应用的插件。

最好的资料:官方文档

生命周期

AccessbilityService的生命周期由系统专门管理,并遵循Service的基本生命周期,它只能由用户自己在设置中手动启动,系统绑定到服务后,会调用它的onServiceConnected()方法。
当用户手动在设置中关闭服务,或者开发者调用disableSelf()方法时,该服务会被关闭销毁。

基本配置

  1. 继承AccessbilityService
class AliAccessibilityService : AccessibilityService() {
    private val TAG = "AliAccessibilityService"

    //服务中断时的回调
    override fun onInterrupt() {
        Log.d(TAG, "onInterrupt")
    }

    //接收到系统发送AccessibilityEvent时的回调
    override fun onAccessibilityEvent(event: AccessibilityEvent) {
        Log.d(TAG, "onAccessibilityEvent:event:$event")
    }
}
  1. AccessbilityService本质上还是一个Service,所以要在AndroidManifest中注册该服务
        <service
            android:name=".AliAccessibilityService"
            android:label="支付测试"
            android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
            <intent-filter>
                <action android:name="android.accessibilityservice.AccessibilityService" />
            </intent-filter>
        </service>

android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"是为了确保只有系统可以绑定该服务。

  1. 配置你需要监听的事件类型、要监听哪个程序,最小监听间隔等属性。这里有两种方式可以进行配置,一种是在manifest中通过meta-data配置,一种是在代码中通过setServiceInfo(AccessibilityServiceInfo)设置。
    方式一:通过meta-data设置
        <service
            android:name=".AliAccessibilityService"
            android: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/accessible_service_config_ali" />
        </service>

accessible_service_config_ali.xml

<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:accessibilityEventTypes="typeWindowStateChanged|typeWindowContentChanged|typeNotificationStateChanged"
    android:accessibilityFeedbackType="feedbackGeneric"
    android:accessibilityFlags="flagReportViewIds"
    android:canRetrieveWindowContent="true"
    android:description="@string/pay_test"
    android:notificationTimeout="10"
    android:canPerformGestures="true"
    android:packageNames="com.eg.android.AlipayGphone" />

配置的具体含义我们下面再讲。
方式二:在代码中通过setServiceInfo设置

    override fun onServiceConnected() {
        val serviceInfo = AccessibilityServiceInfo().apply {
            eventTypes = AccessibilityEvent.TYPES_ALL_MASK
            feedbackType = AccessibilityServiceInfo.FEEDBACK_ALL_MASK
            packageNames = arrayOf("com.eg.android.AlipayGphone")//支付宝包名,可以多个
            notificationTimeout = 10
        }
        setServiceInfo(serviceInfo)
    }

这里我建议使用meta-data的方式进行配置,因为我实践过程中发现有的属性不能通过代码配置。

  1. 指引用户去手动打开该服务
    首先判断该服务是否为开启状态:
    public static boolean isAccessibilitySettingsOn(Context mContext, Class<? extends AccessibilityService> clazz) {
        int accessibilityEnabled = 0;
        final String service = mContext.getPackageName() + "/" + clazz.getCanonicalName();
        try {
            accessibilityEnabled = Settings.Secure.getInt(mContext.getApplicationContext().getContentResolver(),
                    Settings.Secure.ACCESSIBILITY_ENABLED);
        } catch (Settings.SettingNotFoundException e) {
            e.printStackTrace();
        }
        TextUtils.SimpleStringSplitter mStringColonSplitter = new TextUtils.SimpleStringSplitter(':');
        if (accessibilityEnabled == 1) {
            String settingValue = Settings.Secure.getString(mContext.getApplicationContext().getContentResolver(),
                    Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
            if (settingValue != null) {
                mStringColonSplitter.setString(settingValue);
                while (mStringColonSplitter.hasNext()) {
                    String accessibilityService = mStringColonSplitter.next();
                    if (accessibilityService.equalsIgnoreCase(service)) {
                        return true;
                    }
                }
            }
        }
        return false;
    }

没有开启的话则跳转到该服务的开启页面,由用户手动开启

startActivity(Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS))

到这里,我们的准备工作就已经完成了,打开支付宝,我们可以看到这样一条日志:

onAccessibilityEvent:event:EventType: TYPE_WINDOW_STATE_CHANGED; EventTime: 639278268; PackageName: com.eg.android.AlipayGphone; MovementGranularity: 0; Action: 0 [ ClassName: com.eg.android.AlipayGphone.AlipayLogin; Text: [支付宝]; ContentDescription: null; ItemCount: -1; CurrentItemIndex: -1; IsEnabled: true; IsPassword: false; IsChecked: false; IsFullScreen: true; Scrollable: false; BeforeText: null; FromIndex: -1; ToIndex: -1; ScrollX: -1; ScrollY: -1; MaxScrollX: -1; MaxScrollY: -1; AddedCount: -1; RemovedCount: -1; ParcelableData: null ]; recordCount: 0

其中包括了这个event的类别,当前页面的包名、类名、一些文本信息等,具体有哪些属性可以查看AccessibilityEvent的toString方法。

检索窗口内容

除了监听界面变化之外,我们还可以对界面中的内容进行一些操作,前提是我们配置了以下属性,允许服务检索窗口内容:

android:canRetrieveWindowContent="true"

我们可以通过getWindows获取屏幕上的可以看见的窗口,它返回一个列表,按降序的方式,排在第一个的就是最顶层的窗口。
当然了,一般情况下我们关心的只是最新的那个活动窗口,称为当前活动窗口。我们可以通过以下方法来检索其中的控件:

val root = rootInActiveWindow //获取当前活动窗口的根节点
val node = root.findAccessibilityNodeInfosByViewId("com.alipay.mobile.payee:id/payee_NextBtn") //通过控件id来获取某个控件
val node = root.findAccessibilityNodeInfosByText("确定") //通过text来获取某个控件
val node = root.findFoucs(int falg) //寻找拥有特殊焦点的控件(FOCUS_INPUT 或 FOCUS_ACCESSIBILITY)

至于viewId的获取,我们可以通过android Device Monitor工具来查看,对于3.0之后的android studio,可以通过命令行工具进入sdk的tools目录,运行下面命令:

monitor

交互

事件交互

拿到AccessibilityNodeInfo对象后,我们可以进行一些列的操作,包括getChild()、getParent()、getBoundsInScreen()、isClickable等等一些列获取属性的操作,当然也可以进行交互性的操作,比如点击(当然前提是这个控件的clickable为true):

node[0].performAction(AccessibilityNodeInfo.ACTION_CLICK)

除了操作界面内控件之外,我们还可以通过performGlobalAction(int action)执行一些全局操作,比如点击back键、home键等等。

performGlobalAction(GLOBAL_ACTION_BACK)
performGlobalAction(GLOBAL_ACTION_HOME)
performGlobalAction(GLOBAL_ACTION_NOTIFICATIONS)
performGlobalAction(GLOBAL_ACTION_RECENTS)

手势交互

除了Action交互之外,我们还可以模拟人的手势进行操作,这是在Android 24中新加的一个api:

dispatchGesture(gesture, callback, handler)

它接收一个GestureDescription(手势描述)、一个GestureResultCallback(结果回调)和一个Handler。简单封装一下大概是这样的:

/**
 * 通过AccessibilityService在屏幕上模拟手势
 * @param path 手势路径
 */
@RequiresApi(Build.VERSION_CODES.N)
fun AccessibilityService.gestureOnScreen(
        path: Path,
        startTime:Long = 0,
        duration:Long = 100,
        callback:AccessibilityService.GestureResultCallback,
        handler: Handler? = null
){
    val builder = GestureDescription.Builder()
    builder.addStroke(GestureDescription.StrokeDescription(path, startTime, duration))
    val gesture = builder.build()
    dispatchGesture(gesture, callback, handler)
}

/**
 * 通过AccessibilityService在屏幕上某个位置单击
 */
@RequiresApi(Build.VERSION_CODES.N)
fun AccessibilityService.clickOnScreen(
        x:Float,
        y:Float,
        callback:AccessibilityService.GestureResultCallback,
        handler: Handler? = null
){
    val p = Path()
    p.moveTo(x,y)
    gestureOnScreen(p,callback = callback,handler = handler)
}

到这里,AccessibilityService的基本使用方法已经介绍的差不多了,接下来就是根据自己的项目需求进行组装,八仙过海各显神通了。

附录(重要属性介绍)

xml属性 说明 类别
accessibilityEventTypes 指定要监听的事件类型 typeAllMask:接收所有事件。
-------窗口事件相关(常用)---------
typeWindowStateChanged:监听窗口状态变化,比如打开一个popupWindow,dialog,Activity切换等等。
typeWindowContentChanged:监听窗口内容改变,比如根布局子view的变化。
typeWindowsChanged:监听屏幕上显示的系统窗口中的事件更改。 此事件类型只应由系统分派。
typeNotificationStateChanged:监听通知变化,比如notifacation和toast。
-----------View事件相关--------------
typeViewClicked:监听view点击事件。
typeViewLongClicked:监听view长按事件。
typeViewFocused:监听view焦点事件。
typeViewSelected:监听AdapterView中的上下文选择事件。
typeViewTextChanged:监听EditText的文本改变事件。
typeViewHoverEnter、typeViewHoverExit:监听view的视图悬停进入和退出事件。
typeViewScrolled:监听view滚动,此类事件通常不直接发送。
typeViewTextSelectionChanged:监听EditText选择改变事件。
typeViewAccessibilityFocused:监听view获得可访问性焦点事件。
typeViewAccessibilityFocusCleared:监听view清除可访问性焦点事件。
------------手势事件相关---------------
typeGestureDetectionStart、typeGestureDetectionEnd:监听手势开始和结束事件。
typeTouchInteractionStart、typeTouchInteractionEnd:监听用户触摸屏幕事件的开始和结束。
typeTouchExplorationGestureStart、typeTouchExplorationGestureEnd:监听触摸探索手势的开始和结束。
accessibilityFeedbackType 指定反馈方式 feedbackAllMask、feedbackGeneric、feedbackAudible、feedbackSpoken、feedbackHaptic、feedbackVisual
canRetrieveWindowContent 是否希望能够检索活动窗口内容。此设置无法在运行时更改。 true or false
description 该服务的简要说明 @StringRes
notificationTimeout 两个相同类型的可访问性事件之间的最短间隔时间(以毫秒为单位) number
packageNames 要监听的应用的包名 string
canPerformGestures 是否可以执行手势(api 24新增) true or false
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350