60.Android 全面理解AccessibilityService

场景

自己想写一个抢红包工具,于是需要了解AccessibilityService,在此记录一下学习使用的过程。目录如下:

1.什么是AccessibilityService
2.AccessibilityService的基本使用
  2.1 创建服务类
  2.2 声明服务
  2.3 服务参数设置
    2.3.1 通过<meta-data>进行配置
    2.3.2 通过setServiceInfo()方法进行配置
  2.4 启动服务
  2.5 获取事件信息
3.AccessibilityEvent类详解

1.什么是AccessibilityService

Google为了让Android系统更实用,为用户提供了无障碍辅助服务AccessibilityService

AccessibilityService运行在后台,并且能够收到由系统发出的一些事件(即:AccessibilityEvent事件,这些事件表示用户界面一系列的状态变化),比如焦点改变、输入内容变化、按钮被点击了等等,该种服务能够请求获取当前活动窗口并查找其中的内容。换言之,界面中产生的任何变化都会产生一个AccessibilityEvent事件,并由系统通知给AccessibilityService。这就像监视器监视着界面的一举一动,一旦界面发生变化,立刻发出通知:大家请注意,界面发生了变化。

2.AccessibilityService的基本使用

2.1 创建服务类
编写自己的服务类,需要继承AccessibilityService类.其中要实现onAccessibilityEvent(AccessibilityEvent event)onInterruput()两个重要的方法:

/**
 *  抢红包服务
 */
public class LuckyMoneyService extends AccessibilityService {

    /**
     *  当服务连上时的回调
     */
    @Override
    protected void onServiceConnected() {
        super.onServiceConnected();

    }

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
       
    }
}
方法名 含义解析
onServiceConnected() 系统成功绑定该服务时被触发,也就是当你在设置中开启相应的服务,系统成功的绑定了该服务时会触发,通常我们可以在这里做一些初始化操作
onAccessibilityEvent(AccessibilityEvent event) 有关AccessibilityEvent事件的回调函数.系统通过sendAccessibiliyEvent()不断的发送AccessibilityEvent到此处
disableSelf() 禁用当前服务,也就是在服务可以通过该方法停止运行
findFoucs(int falg) 查找拥有特定焦点类型的控件
getRootInActiveWindow() 如果配置能够获取窗口内容,则会返回当前活动窗口的根结点
getSeviceInfo() 获取当前服务的配置信息
performGlobalAction(int action) 执行全局操作,比如返回,回到主页,打开最近等操作
setServiceInfo(AccessibilityServiceInfo info) 设置当前服务的配置信息
getSystemService(String name) 获取系统服务
onKeyEvent(KeyEvent event) 如果允许服务监听按键操作,该方法是按键事件的回调,需要注意,这个过程发生了系统处理按键事件之前

2.2 声明服务
像其他Service服务一样,需要在AndroidManifest.xml中声明该服务.除此之外,该服务还必须配置以下两项:

配置<intent-filter>,其name为固定的:
android.accessibilityservice.AccessibilityService
声明BIND_ACCESSIBILITY_SERVICE权限,以便系统能够绑定该服务(4.0版本后要求)
注意:任何一点配置错误,系统都无反应,因此其固定配置如下:

<service
        android:name=". LuckyMoneyService"
        android:enabled="true"
        android:exported="true"
        android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
    <intent-filter>
        <action android:name="android.accessibilityservice.AccessibilityService"/>
    </intent-filter>
</service>

2.3 服务参数设置
AndroidManifest.xml声明了该服务之后,接下来就是需要对该服务进行一些参数设置了.该服务能够被配置用来接受指定类型的事件,监听指定package,检索窗口内容,获取事件类型的时间等等.目前有两种配置方法:

方法一:4.0之后提供了可以通过<meta-data>标签进行配置
方法二:通过setServiceInfo()方法进行配置

2.3.1 通过<meta-data>进行配置

AndroidManifest.xml声明的service中,其中:
accessibility_service_label是服务的名称,这些会在设置里面的辅助服务中显示出来,用户需要在里面开启服务才能使用。
accessibility_service_description是有关这个服务的功能说明,也可以在meta-data指定的配置文件中声明,效果同:android:description="抢红包服务,请开启"
permission和intent-filter是固定写法。

提供一个meta-data标签,meta-data主要用于对服务进行一些配置,配置的具体内容通过android:resource指定相应的配置文件:

<service
            android:name=".new_service.LuckyMoneyService"
            android:enabled="true"
            android:exported="true"
            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/lucky_money_accessibility_service_config" />
</service>

接下来我们来看lucky_money_accessibility_service_config的相关配置。
提示:该配置文件是在res目录下创建xml文件,并在其中创建配置文件lucky_money_accessibility_service_config.xml,该文件名可自定义。

<?xml version="1.0" encoding="utf-8"?>
<accessibility-service
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:description="抢红包服务,请开启"
    android:accessibilityEventTypes="typeNotificationStateChanged|typeWindowStateChanged"
    android:packageNames="com.tencent.mm"
    android:accessibilityFeedbackType="feedbackGeneric"
    android:notificationTimeout="100"
    android:accessibilityFlags="flagDefault"
    android:canRetrieveWindowContent="true"/>

现在我们对配置中的重要属性进行说明:
description="抢红包服务,请开启"是有关这个服务的功能说明。

accessibilityEventTypes:服务要监控的事件类型,如通知、窗口改变、点击、焦点改变等等,如果有多个可以用 | 连起来,具体的值可以在AccessibilityEvent类中查到,如typeAllMask表示接受所有的事件通知。

android:packageNames服务要监控的应用的包名,如果有多个则用逗号连起来,空着表示监听所有的应用。

android:accessibilityFeedbackType服务反馈的方式,如语音、震动等等,feedbackAllMask代表所有类型。

notificationTimeout:接受事件的时间间隔,通常将其设置为100即可.

canRetrieveWindowContent:服务能否获取窗口里面的内容。也就是如果你希望在服务中获取窗体内容的化,则需要设置其值为true.

这些配置除了在xml里面写之外,还可以在代码中建立一个AccessibilityServiceInfo对象,然后通过setServiceInfo()来设置。见下方2.3.2:

2.3.2 通过setServiceInfo()方法进行配置
也可以通过setServiceInfo(AccessibilityServiceInfo)为其配置信息,除此之外,通过该方法可以在运行期间动态修改服务配置.需要注意,该方法只能用来配置动态属性:eventTypes,feedbackType,flags,notificaionTimeout及packageNames.

通常是在onServiceConnected()进行配置,如下代码:

@Override
    protected void onServiceConnected() {
        AccessibilityServiceInfo serviceInfo = new AccessibilityServiceInfo();
        serviceInfo.eventTypes = AccessibilityEvent.TYPES_ALL_MASK;
        serviceInfo.feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC;
        serviceInfo.packageNames = new String[]{"com.tencent.mm"}; 
        serviceInfo.notificationTimeout=100;
        setServiceInfo(serviceInfo);
    }

在这里涉及到了AccessibilityServiceInfo类,做个说明:
AccessibilityServiceInfo该类被用于配置AccessibilityService信息,该类中包含了大量用于配置的常量字段及用来xml属性,比如常见的:
android:accessibilityEventTypesandroid:canRequestFilterKeyEventsandroid:packageNames等等。

2.4 启动服务
当我们做完以上操作,便可将app安装到手机.安装成功后,在设置->辅助功能中便可以找到我们的服务.该服务默认处在关闭状态,需要手动开启。代码方式打开如下:

Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);
context.startActivity(intent);

2.5 获取事件信息
上面我们说到,onAccessibilityEvent(AccessibilityEvent event)是该服务的核心方法,其中参数event封装来自界面相关事件的信息,比如我们可以获得该事件的事件类型,进而根据起类型选择不同的处理方式:

 @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
       
    }

这里我们先对AccessibilityEvent进行简单的说明:
当用户发生发生变化时,系统会发送一系列的AccessibilityEvent事件,比如按钮被点击时会发送TYPE_VIEW_CLICKED类型的事件.

下面会对AccessibilityEvent类详细介绍。

3.AccessibilityEvent类详解

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

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
方法 说明
getEventType() 事件类型,如上面列举
getSource() 获取事件源对应的结点信息
getClassName() 获取事件源对应类的类型,比如点击事件是有某个Button产生的,那么此时获取的就是Button的完整类名: android.widget.Button
getText() 获取事件源的文本信息,比如事件是有TextView发出的,此时获取的就是TextView的text属性.如果该事件源是树结构,那么此时获取的是这个树上所有具有text属性的值的集合
isEnabled() 事件源(对应的界面控件)是否处在可用状态
getItemCount() 如果事件源是树结构,将返回该树根节点下子节点的数量

特别注意⚠️:
系统不断的产生各种事件,有些是界面控件产生的,有些是系统产生的。对于由界面控件的产生的事件,通常我们将该控件称之为事件源。但并不是所有的事件都能通过getSource()方法获取到事件源,比如像通知消息类型的事件TYPE_NOTIFICATION_STATE_CHANGED

另外,AccessibilityService中还有一个performGlobalAction()方法,用于执行一些通用的事件,主要包括:

GLOBAL_ACTION_BACK    点击返回按钮
GLOBAL_ACTION_HOME    点击home
GLOBAL_ACTION_NOTIFICATIONS    打开通知
GLOBAL_ACTION_RECENTS    打开最近应用
GLOBAL_ACTION_QUICK_SETTINGS    打开快速设置
GLOBAL_ACTION_POWER_DIALOG    打开长按电源键的弹框

4.1 获取窗口内容
仅仅知道事件的信息是不够的,我们还希望通过事件来获取发出该事件(事件源)的信息,比如Button按钮被点击时它的text.一个服务可以配置为允许服务检索窗口内容,即获取窗口内容。

整个窗口内容本质上是关于AccessibilityWindowInfoAccessibilityNodeInfo的树结构,我们可以称之为内容树.(类似View Tree,但由不完全相同)

需要注意,该服务可能配置了只检测了部分事件,而不是全部事件,这就意味着,当内容树发生变化后,该服务可能并不知道。即该服务无法及时的了解当前的内容树是否发生了变化。比如说,你的服务只检测了点击事件,但是此时界面的输入焦点已经变化,这样整个结点树也发生了变化,但是你的服务却不知道,此时你在结点中拿到的窗口内容可能已经不是最新的了。因此,如果你想及时的获知当前窗口的内容,那么就在配置的时候,设置监听全部事件。

正如上面所提到的,要想获取窗口内容,在配置AccessibilityService时需设置canRetrieveWindowContent为true之后,便可以通过一下方法获取窗口内容:AccessibilityEvent.getSource()findFocus(int)getWindow()或者getRootInActiveWindow()

4.2 服务的生命周期
要理解该中服务的生命周期只需要记住一下三点即可:

a:该种服务完全由系统管理,并遵循已有的服务周期.
b:开启一个服务只能由用户在设置中打开,而关闭则只能由用户在设置中关闭或者服务本身通过diableSelf()方法关闭(当然,现在有些第三放软件也可以强制关闭该类型服务)
c:系统绑定该服务之后,会调用onServiceConnected()方法,这个方法可以被重写,在其中,你可以做一些初始化的操作.

4.3 检测服务是否开启
介绍了一些AccessibilityService的基础知识之后,再补充一点关于检测某个服务是否开启的知识.通常来说大体有一下两种方法来检测服务是否启用:

方法一:借助服务管理器AccessibilityManager来判断,但是该方法不能检测app本身开启的服务.

private boolean enabled(String name) {
        AccessibilityManager am = (AccessibilityManager) getSystemService(Context.ACCESSIBILITY_SERVICE);
        List<AccessibilityServiceInfo> serviceInfos = am.getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_GENERIC);
        List<AccessibilityServiceInfo> installedAccessibilityServiceList = am.getInstalledAccessibilityServiceList();
        for (AccessibilityServiceInfo info : installedAccessibilityServiceList) {
            Log.d("MainActivity", "AccessibilityServiceInfo -->" + info.getId());
            if (name.equals(info.getId())) {
                return true;
            }
        }
        return false;
    }

既然谈到了AccessibilityManager,那么在这里我们就做个简单的介绍:
AccessibilityManager是系统级别的服务,用来管理AccessibilityService服务,比如分发事件,查询系统中服务的状态等等,更多信息参考官方文档,常见方法如下:

方法 说明
getAccessibilityServiceList() 获取服务列表(api 14之后废弃,用下面的方法代替)
getInstalledAccessibilityServiceList() 获取已安装到系统的服务列表
getEnabledAccessibilityServiceList(int feedbackTypeFlags) 获取已启用的服务列表
isEnabled() 判断服务是否启用
sendAccessibilityEvent(AccessibilityEvent event) 发送事件

方法二:我们知道大部分的系统属性都在settings中进行设置,比如wifi,蓝牙状态等,而这些信息主要是存储在settings对应的的数据库中(system表和serure表),这就意味我们可以通过直接读取setting设置来判断相关服务是否开启:

private boolean checkStealFeature1(String service) {
        int ok = 0;
        try {
            ok = Settings.Secure.getInt(getApplicationContext().getContentResolver(), Settings.Secure.ACCESSIBILITY_ENABLED);
        } catch (Settings.SettingNotFoundException e) {
        }

        TextUtils.SimpleStringSplitter ms = new TextUtils.SimpleStringSplitter(':');
        if (ok == 1) {
            String settingValue = Settings.Secure.getString(getApplicationContext().getContentResolver(), Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
            if (settingValue != null) {
                ms.setString(settingValue);
                while (ms.hasNext()) {
                    String accessibilityService = ms.next();
                    if (accessibilityService.equalsIgnoreCase(service)) {
                        return true;
                    }

                }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容