【用法总结】无障碍AccessibilityService

2024-01-14 23:52

本文主要是介绍【用法总结】无障碍AccessibilityService,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

一、背景

    本文仅用于做学习总结,转换成自己的理解,方便需要时快速查阅,深入研究可以去官网了解更多:官网链接点这里
    之前对接AI语音功能时,发现有些按钮(或文本)在我没有主动注册唤醒词场景下,还是响应了点击,使用profiler跟踪调用堆栈才发现是使用了无障碍服务实现的。因为开发的是系统应用,也没必要主动去打开无障碍服务开关,于是觉得无障碍服务有很大的可发挥空间,于是借助无障碍服务,实现了一个显示当前展示的Window/Activity/Dialog的悬浮窗,用于演示无障碍服务的用法及其强大之处。

二、用法

2.1 新建一个继承AccessibilityService的无障碍服务类,并按需重写回调方法
class AccessibilityTest : AccessibilityService() {override fun onAccessibilityEvent(event: AccessibilityEvent?) {Log.d(TAG, "onAccessibilityEvent: event = $event")}override fun onServiceConnected() {super.onServiceConnected()Log.d(TAG, "onServiceConnected: ")}override fun onUnbind(intent: Intent?): Boolean {Log.d(TAG, "onUnbind: intent = $intent")return super.onUnbind(intent)}override fun onInterrupt() {Log.d(TAG, "onInterrupt: ")}
}
  • onServiceConnected():
        当无障碍服务打开后,有注册的交互事件发生时,如果还没有连接服务,这会先执行连接,并回调这个方法。
        问题:AccessibilityServie继承Service也就是普通的服务,没有绑定前台的notification等可见的界面,会不会在后台过一会儿就断开了,是否会导致某些时候无法捕获到交互的事件??
  • onAccessibilityEvent(event: AccessibilityEvent?):
        当有注册的交互事件,比如:点击、长按、焦点变化等触发时,会回调这个函数
    • event.getEventType(): 获取事件类型,点击:TYPE_VIEW_CLICKED,长按:TYPE_VIEW_LONG_CLICKED,窗口状态变化:TYPE_WINDOW_STATE_CHANGED,详细的可以查阅AccessibilityService类的EventType注解中有枚举出所有的事件类型。
    • event.getPackName(): 获取交互来自的包名
    • event.getClassName(): 如果是Activity/Dialog则是其类的全路径名,如果是View的话则展示当前的View全路径名。
  • onUnbind(intent: Intent?):
        断开服务时回调,用于处理一些资源释放的逻辑。
  • onInterrupt():
2.2 AndroidManifest.xml文件中注册无障碍服务

    这个步骤和普通的Service注册有些不同,需要配置permission、intent-filter和无障碍服务的xml配置文件,基本都是固定的格式,只是按需改一些配置项。

<serviceandroid:name=".accessibility.AccessibilityTest"android:exported="true"android:label="@string/accessibility_tip"android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"android:process=":BackgroundService"><intent-filter><action android:name="android.accessibilityservice.AccessibilityService" /></intent-filter><meta-dataandroid:name="android.accessibilityservice"android:resource="@xml/accessibility_config" />
</service>
  • service节点配置的label标签会在无障碍服务中展示,比如上面的label内容是“accessibility_tip”,那么在无障碍服务中展示就会如下所示“:
    在这里插入图片描述
    在这里插入图片描述
        在/res/xml目录下,新建一个accessibility_config.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:canRetrieveWindowContent="true"android:description="@string/accessibilty_desc" />

⚠️:如果这里指定了包名(android:packageNames的值,多个包名用英文逗号分隔。)则只会收到对应包名应用的事件。

  • android:description属性设置的就是上面的无障碍中accessibility_tip服务最下面的文案介绍。
  • android:accessibilityEventTypes:指定接收的事件类型
  • android:accessibilityFeedbackType:指定接收的反馈类型
2.3 在AccessibilityTest的onServiceConnected方法中动态设置serviceInfo
  • AccessibilityTest->onServiceConnected(): 通过在Service连接到无障碍服务的回调,调用setServiceInfo方法,可以在在运行时调整无障碍服务的配置:
override fun onServiceConnected() {super.onServiceConnected()Log.d(TAG, "onServiceConnected: ")val accessibilityServiceInfo = AccessibilityServiceInfo()accessibilityServiceInfo.eventTypes = (AccessibilityEvent.TYPE_WINDOWS_CHANGEDor AccessibilityEvent.TYPE_WINDOW_STATE_CHANGEDor AccessibilityEvent.TYPE_VIEW_CLICKEDor AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGEDor AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED)accessibilityServiceInfo.feedbackType = AccessibilityServiceInfo.FEEDBACK_ALL_MASKaccessibilityServiceInfo.notificationTimeout = 0accessibilityServiceInfo.flags = AccessibilityServiceInfo.DEFAULT// 如果这里指定了包名则只会收到对应包名应用的事件accessibilityServiceInfo.packageNames = arrayOf("com.yanggui.animatortest")serviceInfo = accessibilityServiceInfo
}

    完成以上的步骤,然后编译运行安装到手机上,然后从无障碍服务中开启,就能够在AccessibilityTest的onAccessibilityEvent中收到各种交互事件了。

无障碍服务跑起来后,会打印如下log:

// 从无障碍服务中打开accessibility_tip服务回调onServiceConnected方法:
14:25:34.170  D  onServiceConnected:
// 打开当前应用会执行onAccessibilityEvent方法,回调一系列的事件信息,封装在// AccessibilityEvent中
14:26:44.792  D  onAccessibilityEvent: event = EventType: TYPE_VIEW_CLICKED; EventTime: 7897173; PackageName: com.google.android.apps.nexuslauncher; MovementGranularity: 0; Action: 0; ContentChangeTypes: []; WindowChangeTypes: [] [ ClassName: android.widget.TextView; Text: [AnimatorTest]; ContentDescription: AnimatorTest; ItemCount: -1; CurrentItemIndex: -1; Enabled: true; Password: false; Checked: false; FullScreen: false; Scrollable: false; BeforeText: null; FromIndex: -1; ToIndex: -1; ScrollX: 0; ScrollY: 0; MaxScrollX: 0; MaxScrollY: 0; ScrollDeltaX: -1; ScrollDeltaY: -1; AddedCount: -1; RemovedCount: -1; ParcelableData: null ]; recordCount: 0
14:26:44.847  D  onAccessibilityEvent: event = EventType: TYPE_WINDOW_CONTENT_CHANGED; EventTime: 7897231; PackageName: com.google.android.apps.nexuslauncher; MovementGranularity: 0; Action: 0; ContentChangeTypes: [CONTENT_CHANGE_TYPE_SUBTREE]; WindowChangeTypes: [] [ ClassName: android.widget.FrameLayout; Text: []; ContentDescription: null; ItemCount: -1; CurrentItemIndex: -1; Enabled: true; Password: false; Checked: false; FullScreen: false; Scrollable: false; BeforeText: null; FromIndex: -1; ToIndex: -1; ScrollX: 0; ScrollY: 0; MaxScrollX: 0; MaxScrollY: 0; ScrollDeltaX: -1; ScrollDeltaY: -1; AddedCount: -1; RemovedCount: -1; ParcelableData: null ]; recordCount: 0
14:26:44.891  D  onAccessibilityEvent: event = EventType: TYPE_WINDOW_STATE_CHANGED; EventTime: 7897277; PackageName: com.yanggui.animatortest; MovementGranularity: 0; Action: 0; ContentChangeTypes: []; WindowChangeTypes: [] [ ClassName: com.yanggui.animatortest.leanback.RowsSupportFragmentActivity; Text: [AnimatorTest]; ContentDescription: null; ItemCount: -1; CurrentItemIndex: -1; Enabled: true; Password: false; Checked: false; FullScreen: true; Scrollable: false; BeforeText: null; FromIndex: -1; ToIndex: -1; ScrollX: 0; ScrollY: 0; MaxScrollX: 0; MaxScrollY: 0; ScrollDeltaX: -1; ScrollDeltaY: -1; AddedCount: -1; RemovedCount: -1; ParcelableData: null ]; recordCount: 0
14:26:44.952  D  onAccessibilityEvent: event = EventType: TYPE_WINDOW_STATE_CHANGED; EventTime: 7897337; PackageName: com.yanggui.animatortest; MovementGranularity: 0; Action: 0; ContentChangeTypes: []; WindowChangeTypes: [] [ ClassName: com.yanggui.animatortest.leanback.RowsSupportFragmentActivity; Text: [AnimatorTest]; ContentDescription: null; ItemCount: -1; CurrentItemIndex: -1; Enabled: true; Password: false; Checked: false; FullScreen: true; Scrollable: false; BeforeText: null; FromIndex: -1; ToIndex: -1; ScrollX: 0; ScrollY: 0; MaxScrollX: 0; MaxScrollY: 0; ScrollDeltaX: -1; ScrollDeltaY: -1; AddedCount: -1; RemovedCount: -1; ParcelableData: null ]; recordCount: 0
// 从无障碍服务中关闭accessibility_tip服务回调onUnbind方法:
14:24:38.264  D  onUnbind: intent = Intent { cmp=com.yanggui.animatortest/.accessibility.AccessibilityTest }
2.4 附:反馈类型(feedbackType)和事件类型(eventType)的枚举值
  • 反馈类型(feedbackType):定义在AccessibilityServiceInfo中
@IntDef(flag = true, prefix = { "FEEDBACK_" }, value = {FEEDBACK_AUDIBLE,FEEDBACK_GENERIC,FEEDBACK_HAPTIC,FEEDBACK_SPOKEN,FEEDBACK_VISUAL,FEEDBACK_BRAILLE
})
@Retention(RetentionPolicy.SOURCE)
public @interface FeedbackType {}
  • 事件类型(eventType):定义在AccessibilityEvent中
@IntDef(flag = true,prefix = {"TYPE_"},value = {TYPE_VIEW_CLICKED, // 点击事件TYPE_VIEW_LONG_CLICKED, // 长按事件TYPE_VIEW_SELECTED, // view选中TYPE_VIEW_FOCUSED, // view上焦,使用遥控操作的需要关注该事件TYPE_VIEW_TEXT_CHANGED, // 表示更改 android.widget.EditText的文本的事件。TYPE_WINDOW_STATE_CHANGED,TYPE_NOTIFICATION_STATE_CHANGED,TYPE_VIEW_HOVER_ENTER,TYPE_VIEW_HOVER_EXIT,TYPE_TOUCH_EXPLORATION_GESTURE_START,TYPE_TOUCH_EXPLORATION_GESTURE_END,TYPE_WINDOW_CONTENT_CHANGED,TYPE_VIEW_SCROLLED,TYPE_VIEW_TEXT_SELECTION_CHANGED,TYPE_ANNOUNCEMENT,TYPE_VIEW_ACCESSIBILITY_FOCUSED,TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED,TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,TYPE_GESTURE_DETECTION_START,TYPE_GESTURE_DETECTION_END,TYPE_TOUCH_INTERACTION_START,TYPE_TOUCH_INTERACTION_END,TYPE_WINDOWS_CHANGED,TYPE_VIEW_CONTEXT_CLICKED,TYPE_ASSIST_READING_CONTEXT,TYPE_SPEECH_STATE_CHANGE})@Retention(RetentionPolicy.SOURCE)public @interface EventType {}

三、实现展示当前Activity/Dialog/Window信息的悬浮窗

    如上介绍的,无障碍服务是能够获取到各种交互事件,从onAccessibilityEvent回调中可轻松拿到交互控件的packageName和className,所以基于无障碍服务能力的支持,也就很容易实现悬浮展示当前Activity的功能了。

3.1 全局悬浮窗的实现
  • 这个业务点的关键知识点是能全局悬浮,且不依赖Activity类型context,也就是不需要windowToken参数的window类型。
private const val TAG = "TopActivityEvent"
class TopActivityEventWindow {companion object {@SuppressLint("StaticFieldLeak")private var rootView: View? = null@SuppressLint("StaticFieldLeak")private var tvContent: TextView? = null@SuppressLint("StaticFieldLeak")private var window: TopActivityEventWindow? = null@SuppressLint("StaticFieldLeak")fun showEvent(ctx: Context, pkgName: String?, activityClassName: String?) {if (window == null) {initEventWindow(ctx)}if (!pkgName.isNullOrBlank() && !activityClassName.isNullOrBlank()) {tvContent?.text = "$pkgName\n$activityClassName"} else {Log.e(TAG, "showEvent: pkgName = $pkgName, activityClassName = $activityClassName")}}private fun initEventWindow(ctx: Context) {rootView =LayoutInflater.from(ctx).inflate(R.layout.layout_top_activity_window, null, false)val windowManager = ctx.getSystemService(Context.WINDOW_SERVICE) as? WindowManagerwindowManager?.apply {val lp = WindowManager.LayoutParams(WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT)lp.type = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) {WindowManager.LayoutParams.TYPE_TOAST} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {// android31及以上 需要使用TYPE_APPLICATION_OVERLAY才能展示,使用ALERT_WINDOW会报没有权限WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY} else {WindowManager.LayoutParams.TYPE_SYSTEM_ALERT}lp.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLElp.gravity = Gravity.TOP or Gravity.LEFTlp.format = PixelFormat.TRANSLUCENTaddView(rootView, lp)}tvContent = rootView?.findViewById(R.id.top_activity_window_text)window = TopActivityEventWindow()}fun dismiss(ctx: Context) {if (window != null) {val windowManager = ctx.getSystemService(Context.WINDOW_SERVICE) as? WindowManagerwindowManager?.removeView(rootView)tvContent = nullrootView = nullwindow = null}}}
}
3.1 在无障碍服务的onAccessibilityEvent中调用悬浮窗的展示逻辑
override fun onAccessibilityEvent(event: AccessibilityEvent?) {Log.d(TAG, "onAccessibilityEvent: event = $event")if (event?.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {TopActivityEventWindow.showEvent(this.applicationContext, "${event.packageName}", "${event.className}")}}
3.2 实现的实际效果
  • 捕获pixel3a-api33模拟器的Launcher展示效果:
    在这里插入图片描述
  • 自己的demo app的主页获取效果:
    在这里插入图片描述
  • Dialog的捕获效果:
    在这里插入图片描述
  • PopupWindow获取不到待解决!!

四、总结

    以上是自定义Android的无障碍服务的基本用法。在清单文件中注册CustomAccessibilityService(在meta-datade android.accessibilityservice为key,填写配置文件xml的路径,关键是intent-filter节点中要写,service节点要写permission),然后重写onAccessiblityEvent()方法拿到交互事件、packageName、className,最后只要在系统设置-无障碍打开,就能很轻松实现一个自己的无障碍服务。这块定义注册Service的套路是固定的,核心是理解不同属性的作用,比如配置接收哪些事件类型和反馈类型,指定包名的方式等,其他的步骤不用纠结,直接在需要时照猫画虎就好了,不必花太多时间研究基础用法了。
    但是无障碍服务支持的能力还远不止于此,还能实现很多丰富的功能,比如:触发指定控件的点击,从而配合语音识别实现点击、跳转等业务逻辑,需要我们进一步阅读官方文档进行学习实践总结。

这篇关于【用法总结】无障碍AccessibilityService的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/606946

相关文章

Java中的数组与集合基本用法详解

《Java中的数组与集合基本用法详解》本文介绍了Java数组和集合框架的基础知识,数组部分涵盖了一维、二维及多维数组的声明、初始化、访问与遍历方法,以及Arrays类的常用操作,对Java数组与集合相... 目录一、Java数组基础1.1 数组结构概述1.2 一维数组1.2.1 声明与初始化1.2.2 访问

Java通过驱动包(jar包)连接MySQL数据库的步骤总结及验证方式

《Java通过驱动包(jar包)连接MySQL数据库的步骤总结及验证方式》本文详细介绍如何使用Java通过JDBC连接MySQL数据库,包括下载驱动、配置Eclipse环境、检测数据库连接等关键步骤,... 目录一、下载驱动包二、放jar包三、检测数据库连接JavaJava 如何使用 JDBC 连接 mys

MySQL 中的 CAST 函数详解及常见用法

《MySQL中的CAST函数详解及常见用法》CAST函数是MySQL中用于数据类型转换的重要函数,它允许你将一个值从一种数据类型转换为另一种数据类型,本文给大家介绍MySQL中的CAST... 目录mysql 中的 CAST 函数详解一、基本语法二、支持的数据类型三、常见用法示例1. 字符串转数字2. 数字

Python中你不知道的gzip高级用法分享

《Python中你不知道的gzip高级用法分享》在当今大数据时代,数据存储和传输成本已成为每个开发者必须考虑的问题,Python内置的gzip模块提供了一种简单高效的解决方案,下面小编就来和大家详细讲... 目录前言:为什么数据压缩如此重要1. gzip 模块基础介绍2. 基本压缩与解压缩操作2.1 压缩文

解读GC日志中的各项指标用法

《解读GC日志中的各项指标用法》:本文主要介绍GC日志中的各项指标用法,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、基础 GC 日志格式(以 G1 为例)1. Minor GC 日志2. Full GC 日志二、关键指标解析1. GC 类型与触发原因2. 堆

MySQL数据库中ENUM的用法是什么详解

《MySQL数据库中ENUM的用法是什么详解》ENUM是一个字符串对象,用于指定一组预定义的值,并可在创建表时使用,下面:本文主要介绍MySQL数据库中ENUM的用法是什么的相关资料,文中通过代码... 目录mysql 中 ENUM 的用法一、ENUM 的定义与语法二、ENUM 的特点三、ENUM 的用法1

JavaSE正则表达式用法总结大全

《JavaSE正则表达式用法总结大全》正则表达式就是由一些特定的字符组成,代表的是一个规则,:本文主要介绍JavaSE正则表达式用法的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下... 目录常用的正则表达式匹配符正则表China编程达式常用的类Pattern类Matcher类PatternSynta

MySQL之InnoDB存储引擎中的索引用法及说明

《MySQL之InnoDB存储引擎中的索引用法及说明》:本文主要介绍MySQL之InnoDB存储引擎中的索引用法及说明,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐... 目录1、背景2、准备3、正篇【1】存储用户记录的数据页【2】存储目录项记录的数据页【3】聚簇索引【4】二

mysql中的数据目录用法及说明

《mysql中的数据目录用法及说明》:本文主要介绍mysql中的数据目录用法及说明,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1、背景2、版本3、数据目录4、总结1、背景安装mysql之后,在安装目录下会有一个data目录,我们创建的数据库、创建的表、插入的

深度解析Python装饰器常见用法与进阶技巧

《深度解析Python装饰器常见用法与进阶技巧》Python装饰器(Decorator)是提升代码可读性与复用性的强大工具,本文将深入解析Python装饰器的原理,常见用法,进阶技巧与最佳实践,希望可... 目录装饰器的基本原理函数装饰器的常见用法带参数的装饰器类装饰器与方法装饰器装饰器的嵌套与组合进阶技巧