> 技术文档 > Android 无障碍服务

Android 无障碍服务


1、无障碍服务介绍

无障碍服务是 Android 框架的一项功能,旨在代表 Android 设备上安装的应用向用户提供备选导航反馈。无障碍服务可以代表应用与用户通信,例如,通过将文字转换为语音,或在用户将鼠标悬停在屏幕的重要区域时提供触感反馈

1.1、 什么无障碍服务

无障碍服务(Accessibility Service)是 Android 系统提供的一种特殊服务,旨在帮助残障用户更好地使用设备。它通过监控系统事件(如界面变化、用户操作)并提供辅助功能(如语音反馈、界面操作)来增强用户体验。

在 Android 测试中,无障碍服务不仅可以用于辅助功能开发,还可以用于自动化测试、界面分析和监控。通过无障碍服务,开发者可以模拟用户操作、获取界面信息,甚至实现跨应用的自动化测试。

1.2、常见无障碍服务示例

  • Switch Access(开关控制):可让行动不便的 Android 用户使用一个或多个开关与设备互动。

  • Voice Access(Beta 版):可让行动不便的 Android 用户通过语音指令控制设备。

  • TalkBack:一种屏幕阅读器,通常供视障用户或盲人用户使用。

  • AccessibilityService(自定义

1.2.1、什么是 Switch Access(开关控制)?

Switch Access 是 Android 系统提供的一种无障碍功能,旨在帮助存在行动障碍的用户使用一种或多种“开关设备”来控制手机,而无需直接触摸屏幕。

Switch Access 的核心特点

特性

说明

非接触式操作

用户可通过物理按钮、蓝牙设备或摄像头表情检测等“开关”来操作界面

逐项扫描模式

系统自动高亮屏幕上的可操作项,用户点击“开关”即可选择

完整交互控制

包括点击、滑动、输入、返回、主屏幕、通知栏等全部操作均可通过开关完成

自定义开关行为

可配置多个开关分别控制“选中”“确认”等功能

兼容性

内置于 Android 系统,大多数设备从 Android 5.0 开始支持

Switch Access适用人群

Switch Access 主要面向以下人群:

  • 手部活动受限、无法精准触控屏幕的用户;

  • 需要辅助设备(如轮椅按钮、头部控制器、眼动追踪等)的人;

  • 长期使用外部开关设备交互的重度无障碍用户。

Switch Access 支持的控制方式

控制方式

说明

蓝牙物理开关

如按钮、外接键盘、辅助设备

屏幕区域点击

屏幕模拟开关点击(测试或轻度辅助用)

摄像头动作

利用人脸表情、眨眼等作为输入(Android 12+)

键盘按键

支持配置如 Space, Enter, Volume Up/Down 等为开关操作键

Switch Access开启方式

Android 无障碍服务

Android 无障碍服务

Android 无障碍服务

1.2.2、什么是 Voice Access(语音控制)?

Voice Access 是 Google 提供的一种无障碍辅助服务,允许用户通过语音命令完全控制 Android 设备,无需触控屏幕。

语音访问主要服务于手部或身体行动不便的用户,也可用于解放双手的场景(如驾驶、烹饪时操作手机)。

Voice Access 的核心能力

功能

描述

语音控制 UI 操作

点击按钮、滑动屏幕、返回主页、打开通知栏等全部可通过语音完成

自动编号控件

为界面中每个可操作控件打上数字编号,用户只需说出“点击 5”等命令即可

支持文字输入

可以语音输入文本、修改、删除、选择等

连续命令操作

支持“滚动到底部”、“点击下一页”、“打开设置”等连续操作

自然语言指令识别

可使用“回到上一页”“打开微信”“说一下电量”等自然语言命令

Voice Access适用人群
  • 肢体障碍用户,无法触控或操作屏幕;

  • 手部临时不便(如骨折、术后恢复等);

  • 希望通过语音操控设备的普通用户;

  • 驾驶或手忙时需要免触交互的场景。

Voice Access开启方式

Android 无障碍服务

Android 无障碍服务

Android 无障碍服务

1.2.3、什么是 TalkBack?

TalkBack 是 Android 系统内置的屏幕阅读器(Screen Reader),为视力障碍用户提供语音反馈,帮助他们感知、理解并操作手机界面。

简单来说:TalkBack 让“看不见屏幕”的用户,听见并操作屏幕内容。

TalkBack核心功能

功能

说明

语音朗读内容

朗读文本、按钮名称、提示信息、通知等

朗读焦点控件属性

读取组件类型、状态(选中/不可用)、位置等

手势操作导航

通过滑动手势导航焦点(上下左右滑动)

支持输入朗读

输入框输入时同步朗读文字内容

辅助操作功能

长按、双击、切换按钮、滑动等均支持辅助反馈

TalkBack适用用户
  • 视力障碍者(全盲或弱视);

  • 临时不便查看屏幕者;

  • 需要通过听觉完成 UI 操作的用户。

TalkBack开启方式

Android 无障碍服务

Android 无障碍服务

Android 无障碍服务

TalkBack开发者需要注意什么?

要点

建议

提供 contentDescription

所有按钮、图片、非文本控件必须加描述

避免重复冗余朗读

不要将描述和可见文本内容重复添加

支持焦点导航

控件需具备 focus 属性,并正确响应焦点事件

尽量使用原生控件

自定义 View 需实现 AccessibilityDelegate

测试体验顺序

逻辑顺序应符合阅读/操作习惯,从上到下、从左到右

1.2.4、什么是 AccessibilityService(自定义无障碍服务)?

AccessibilityService 是 Android 提供的一种系统级服务接口,允许开发者监听和控制全局界面交互,以便辅助操作、自动化任务或提供无障碍支持。

简单理解:它是开发者级的“屏幕代理”,可以读取界面元素并模拟操作。

AccessibilityService(自定义无障碍服务)可以做什么?

功能

描述

监听界面变化

接收系统或其他 App 的界面事件,如点击、焦点变化、内容变化

获取控件结构

获取任意 App 当前界面的控件层级树(AccessibilityNodeInfo)

模拟用户操作

实现点击、滑动、输入、返回、长按等手势操作

跨应用自动化

可跳转 App、点击系统弹窗、执行自动流程

读取控件属性

获取控件文本、类型、ID、位置、状态等

无UI自动执行

可在后台运行任务,甚至在目标 App 不配合的情况下自动执行流程

自定义无障碍服务适用于
  • 自动化开发 / 测试辅助工具;

  • 无障碍辅助 App 开发;

  • 面向 Android 系统服务层交互的高级控制。

与其他无障碍服务对比

工具

控制方式

服务对象

是否语音反馈

是否模拟点击

是否可跨应用

TalkBack

手势导航+朗读

视障用户

Switch Access

外部开关

肢体障碍

Voice Access

语音命令

肢体不便

AccessibilityService(自定义)

脚本/逻辑控制

开发者/自动化

功能对比:

功能点

自定义 AccessibilityService

TalkBack / VoiceAccess

控件监听

精细控制

不提供 API

自动操作

可模拟各种用户动作

仅用户语音/手势触发

跨应用

支持

支持

UI 控制

可隐藏/后台运行

无控制权限

自动化脚本

支持逻辑判断、循环等

无脚本逻辑能力

面向开发者

开发者工具

面向终端用户

1.3、 无障碍服务在 Android 测试中的重要性

无障碍服务在 Android 测试中的优势:

  1. 无需修改被测应用:无障碍服务直接作用于系统界面层,无需对被测应用进行任何代码改动,天然适用于黑盒测试场景。

  2. 支持跨应用操作:能够感知并操作多个应用的界面,适合测试涉及应用间跳转、系统弹窗等复杂场景。

  3. 强大的界面分析能力:通过 AccessibilityNodeInfo 可访问当前界面完整的控件层级结构与属性信息,支持精准的元素识别与布局验证。

  4. 自动化测试灵活性高:可模拟用户各种操作(如点击、输入、滑动),结合逻辑判断,实现高度自定义的测试脚本与任务流程。

  5. 良好的兼容性:无障碍服务作为 Android 框架的系统能力,在绝大多数设备与系统版本上都能稳定运行。

目前,一些主流的 Android 自动化框架,或多或少都使用到了无障碍服务的功能,比如:

  • Google 官方提供的自动化测试框架 UI Automator 本身就是构建在无障碍服务之上;

  • Google 官方提供的 Android 白盒测试框架,主要依赖于视图层级(View Hierarchy),但在某些场景下会使用无障碍服务来增强功能。

2、UI Automator介绍

UI Automator 是 Google 在 Android 4.1 的时候推出的 Android UI 自动化测试框架。 它可以模拟用户操作(比如:点击、滑动、输入文本等)和获取应用程序的界面信息,帮助开发者构建可靠且高效的自动化测试脚本。

2.1、UI Automator特点与优势

特点

描述

跨应用测试

可跨应用操作任意界面,不局限于当前 App

强大的元素定位

支持根据 text、ID、class 等属性查找控件

模拟复杂用户交互

支持点击、长按、滑动、输入等操作

多设备支持

可连接多个设备进行测试

异步任务处理

提供等待机制处理异步加载

日志报告完善

输出详细日志与调试信息

2.2、UI Automator应用场景

  • 功能自动化测试:自动执行用户场景,验证功能逻辑。

  • UI 验证:检测控件显示、状态、布局是否符合预期。

  • 兼容性测试:测试不同设备和版本系统的表现。

  • 跨 App 场景测试:第三方 App 监控或交互,如微信拉起支付宝、系统设置跳转等。

  • 性能评估:配合统计工具进行响应时间、内存等评估。

  • 用户体验测试:模拟实际用户操作,检测交互体验。

UI Automator 对于 WebView 构建的应用适配不是很好。

2.3、UI Automator 版本对比

UI Automator 主要版本有 1.0 和 2.0,现在大多使用的是 2.0 版本。可通过 UI Automator 官网 查看版本的变更历史。

uiautomator2--> git:https://github.com/openatx/uiautomator2

对比项

UI Automator 1.0

UI Automator 2.0

实现基础

Instrumentation

AccessibilityService

支持系统

Android 4.1+

Android 5.0+

查找方式

UiSelector

BySelector, UiObject2

查找能力

一般

更强,支持层级、动态视图

API 扩展

限制多

支持手势、等待、滚动等

性能

较慢

更快,适配复杂视图结构

2.4、辅助工具

2.4.1、UI Automator Viewer

Android SDK 自带工具,可截图当前界面并展示元素层级与属性,帮助分析 UI 结构。

注意:需 Java 8 环境运行,Java 11+ 可能闪退

Android 无障碍服务

2.4.2、Appium

在 Android 设备上进行自动化操作时,可以使用 Appium 获取 UI 元素的节点信息

2.4.2.1安装 Appium

首先,需要在本地安装 Appium。

安装Appium之前需要先安装好node.js,版本需要>=18.0.0

安装好node.js之后就可以通过npm安装Appium了

npm install -g appiumappium -v # 检查安装版本
2.4.2.2安装必要的依赖
  • Android SDK

  • Java JDK

  • uiautomator2 驱动

使用appium直接安装uiautomator2appium driver install uiautomator2

brew install --cask appium-inspector

2.4.2.3启动 Appium
appium server --allow-cors
2.4.2.4连接设备并使用浏览器打开 Appium Inspector
  • 连接Android设备,通过命令行 adb devices 查看确保设备已连接。

  • 使用浏览器打开 Appium Inspector

  • 在Appium Inspector页面中的JSON Representation输入以下参数

{  \"platformName\": \"Android\",  \"appium:deviceName\": \"4b3cc831\",  \"appium:automationName\": \"UiAutomator2\"}

Android 无障碍服务

Android 无障碍服务

2.4.3、weditor

Weditor 是一个基于 Web 的可视化 UI 层级查看工具,主要用于 Android 无障碍开发、自动化测试等场景。它可以实时查看 Android 设备上的界面结构(UI 层级)、获取控件的各种属性,如 text、resource-id、class 等。

2.4.3.1、weditor安装
安装 Python 环境

Weditor 依赖于 Python(推荐 3.6+):

  • macOS

brew install python3
安装 Weditor

使用 pip 命令安装最新版 Weditor:

pip install -U weditor

安装完成后验证是否成功:

weditor --help

若输出帮助信息,说明安装成功

启动 Weditor 服务
weditor

Android 无障碍服务

2.4.4、uiauto.dev

uiauto.dev帮助你快速编写App UI自动化脚本,它由网页端+客户端组成

  • 支持Android和iOS

  • 支持鼠标选择控件查看属性,控件树辅助精确定位,生成选择器

  • 支持找色功能,方向键微调坐标,获取RGB、HSB

uiauto.dev安装

安装Python 3.8+

安装并启动

pip3 install -U uiautodev -i https://pypi.doubanio.com/simple

uiauto.devor:python3 -m uiautodev

Android 无障碍服务

3、Android 无障碍服务AccessibilityService

3.1、AccessibilityService基础实现步骤

3.1.1. AccessibilityService声明服务

在 AndroidManifest.xml 中注册:

                    

3.1.2.配置服务(res/xml/accessibility_service_config.xml)

3.1.3.编写类继承 AccessibilityService

class MyAccessibilityService : AccessibilityService() {    override fun onAccessibilityEvent(event: AccessibilityEvent) {        // 处理 UI 事件,如点击、滚动等    }    override fun onInterrupt() {        // 被打断时调用(如服务被系统关闭)    }    override fun onServiceConnected() {        // 服务连接成功    }}

3.2、无障碍服务核心逻辑封装

/** * 无障碍服务核心类 * 提供对AccessibilityService的封装和扩展功能 */object AssistsCore {    /** 日志标签 */    var LOG_TAG = \"assists_log\"    /** 当前应用在屏幕中的位置信息缓存 */    private var appRectInScreen: Rect? = null    /**     * 以下是一系列用于快速判断元素类型的扩展函数     * 通过比对元素的className来判断元素类型     */    /** 判断元素是否是FrameLayout */    fun AccessibilityNodeInfo.isFrameLayout(): Boolean {        return className == NodeClassValue.FrameLayout    }    /** 判断元素是否是ViewGroup */    fun AccessibilityNodeInfo.isViewGroup(): Boolean {        return className == NodeClassValue.ViewGroup    }    /** 判断元素是否是View */    fun AccessibilityNodeInfo.isView(): Boolean {        return className == NodeClassValue.View    }    /** 判断元素是否是ImageView */    fun AccessibilityNodeInfo.isImageView(): Boolean {        return className == NodeClassValue.ImageView    }    /** 判断元素是否是TextView */    fun AccessibilityNodeInfo.isTextView(): Boolean {        return className == NodeClassValue.TextView    }    /** 判断元素是否是LinearLayout */    fun AccessibilityNodeInfo.isLinearLayout(): Boolean {        return className == NodeClassValue.LinearLayout    }    /** 判断元素是否是RelativeLayout */    fun AccessibilityNodeInfo.isRelativeLayout(): Boolean {        return className == NodeClassValue.RelativeLayout    }    /** 判断元素是否是Button */    fun AccessibilityNodeInfo.isButton(): Boolean {        return className == NodeClassValue.Button    }    /** 判断元素是否是ImageButton */    fun AccessibilityNodeInfo.isImageButton(): Boolean {        return className == NodeClassValue.ImageButton    }    /** 判断元素是否是EditText */    fun AccessibilityNodeInfo.isEditText(): Boolean {        return className == NodeClassValue.EditText    }    /**     * 获取元素的文本内容     * @return 元素的text属性值,如果为空则返回空字符串     */    fun AccessibilityNodeInfo.txt(): String {        return text?.toString() ?: \"\"    }    /**     * 获取元素的描述内容     * @return 元素的contentDescription属性值,如果为空则返回空字符串     */    fun AccessibilityNodeInfo.des(): String {        return contentDescription?.toString() ?: \"\"    }    /**     * 初始化AssistsCore     * @param application Application实例     */    fun init(application: Application) {        LogUtils.getConfig().globalTag = LOG_TAG    }    /**     * 打开系统的无障碍服务设置页面     * 用于引导用户开启无障碍服务     */    fun openAccessibilitySetting() {        val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)        intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK        ActivityUtils.startActivity(intent)    }    /**     * 检查无障碍服务是否已开启     * @returntrue表示服务已开启,false表示服务未开启     */    fun isAccessibilityServiceEnabled(): Boolean {        return AssistsService.instance != null    }    /**     * 获取当前窗口所属的应用包名     * @return 当前窗口的包名,如果获取失败则返回空字符串     */    fun getPackageName(): String {        return AssistsService.instance?.rootInActiveWindow?.packageName?.toString() ?: \"\"    }    /**     * 通过id查找所有符合条件的元素     * @param id 元素的资源id     * @param text 可选的文本过滤条件     * @return 符合条件的元素列表     */    fun findById(id: String, text: String? = null): List {        var nodeInfos = AssistsService.instance?.rootInActiveWindow?.findById(id) ?: arrayListOf()        nodeInfos = text?.let {            nodeInfos.filter {                if (it.txt() == text) {                    return@filter true                }                return@filter false            }        } ?: let { nodeInfos }        return nodeInfos    }    /**     * 在指定元素范围内通过id查找所有符合条件的元素     * @param id 元素的资源id     * @return 符合条件的元素列表     */    fun AccessibilityNodeInfo?.findById(id: String): List {        if (this == null) return arrayListOf()        findAccessibilityNodeInfosByViewId(id)?.let {            return it        }        return arrayListOf()    }    /**     * 通过文本内容查找所有符合条件的元素     * @param text 要查找的文本内容     * @return 符合条件的元素列表     */    fun findByText(text: String): List {        return AssistsService.instance?.rootInActiveWindow?.findByText(text) ?: arrayListOf()    }    /**     * 根据文本查找元素     * @param searchText 可选的文本过滤条件     * @return 符合所有条件的元素列表     */    fun findBySearchText(        searchText: String? = null    ): List {        return findBySearchText(searchText,0)    }    /**     * 根据文本查找元素     * @param searchText 可选的文本过滤条件     * @param searchType 搜索条件类型 0:完全匹配  1:包含匹配     * @return 符合所有条件的元素列表     */    fun findBySearchText(        searchText: String? = null,        searchType:Int = 0    ): List {        var nodeList = arrayListOf()        searchText?.let {            var allNodes = getAllNodes()            allNodes.forEach { it ->                it.logNode()                if(searchType == 0){                    if(!it.text.isNullOrEmpty() && it.text != \"null\"){                        if(searchText == it.text.toString()){                            nodeList.add(it)                        }                    }                }elseif(searchType == 1){                    if(!it.text.isNullOrEmpty() && it.text != \"null\"){//                        Log.d(\"HYLAll:\", it.text.toString() + \"size:\" + allNodes.size)                        if(it.text?.toString()?.contains(searchText) == true){//                            Log.d(\"HYLResult:\", it.text.toString())                            nodeList.add(it)                        }                    }                }            }        }        return nodeList    }    /**     * 通过文本内容查找所有符合条件的元素     * @param content 要查找的文本内容     * @return 符合条件的元素列表     */    fun findIsMatchText(content: String): AccessibilityNodeInfo? {        if(haveTextView(content) == null){            val rootNode: AccessibilityNodeInfo? = AssistsService.instance?.rootInActiveWindow            if (rootNode != null) {                // 调用方法来解析或处理屏幕内容                val processScreenContent = processScreenContent(rootNode, content)                if(processScreenContent != null){                    return processScreenContent                }            }        }else {            return haveTextView(content)        }        return null    }    private fun processScreenContent(        rootNode: AccessibilityNodeInfo?,        content: String    ): AccessibilityNodeInfo? {        // 遍历节点,获取屏幕内容        if (rootNode == null) return null        for (i in 0 until rootNode.childCount) {            val childNode = rootNode.getChild(i)            if (childNode != null) {                // 获取节点的文本                var text = childNode.text                if (text == null) {                    text = childNode.contentDescription                }                if (!TextUtils.isEmpty(text)) {                    Log.d(\"ScreenContent\", \"Text: $text${childNode.viewIdResourceName}\".trimIndent())                }                if (isMatchExecuteUnit(childNode, content)) {                    return childNode                }                // 递归检查子节点                val processScreenContent = processScreenContent(childNode, content)                if (processScreenContent!= null) {                    return processScreenContent                }            }        }        return null    }    private fun haveTextView(content: String): AccessibilityNodeInfo? {        var rootNode: AccessibilityNodeInfo? = AssistsService.instance?.rootInActiveWindow ?: return null        var targets: List? = null        if (!TextUtils.isEmpty(content)) {            targets = rootNode.findByText(content)        }        if (!targets.isNullOrEmpty()){            for (i in targets.indices) {                if (isMatchExecuteUnit(targets[i], content)) {                    val target = targets[i]                    return target                }            }        }        return null    }    private fun isMatchExecuteUnit(rootNode: AccessibilityNodeInfo?,text:String?): Boolean {        if (rootNode == null) returnfalse        var textMatch = true        if (text != null) {            var nodeText = rootNode.text            if (nodeText == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {                nodeText = rootNode.hintText            }            if (nodeText == null) {                nodeText = rootNode.contentDescription            }            if (nodeText == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {                nodeText = rootNode.tooltipText            }            if (nodeText == null) {                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {                    nodeText = rootNode.paneTitle                }            }            if (nodeText == null) {                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {                    nodeText = rootNode.stateDescription                }            }            Log.d(\"ScreenContent\", \"nodeText: $nodeText${rootNode.viewIdResourceName}\".trimIndent())            rootNode.logNode()            textMatch = nodeText != null && text == nodeText.toString()        }        return textMatch    }    /**     * 查找所有文本完全匹配的元素     * @param text 要匹配的文本内容     * @return 文本完全匹配的元素列表     */    fun findByTextAllMatch(text: String): List {        val listResult = arrayListOf()        val list = AssistsService.instance?.rootInActiveWindow?.findByText(text)        list?.let {            it.forEach {                if (TextUtils.equals(it.text, text)) {                    listResult.add(it)                }            }        }        return listResult    }    /**     * 在指定元素范围内通过文本查找所有符合条件的元素     * @param text 要查找的文本内容     * @return 符合条件的元素列表     */    fun AccessibilityNodeInfo?.findByText(text: String): List {        if (this == null) return arrayListOf()        findAccessibilityNodeInfosByText(text)?.let {            return it        }        return arrayListOf()    }    /**     * 判断元素是否包含指定文本     * @param text 要检查的文本内容     * @returntrue表示包含指定文本,false表示不包含     */    fun AccessibilityNodeInfo?.containsText(text: String): Boolean {        if (this == null) returnfalse        getText()?.let {            if (it.contains(text)) returntrue        }        contentDescription?.let {            if (it.contains(text)) returntrue        }        returnfalse    }    /**     * 获取元素的所有文本内容(包括text和contentDescription)     * @return 包含所有文本内容的列表     */    fun AccessibilityNodeInfo?.getAllText(): ArrayList {        if (this == null) return arrayListOf()        val texts = arrayListOf()        getText()?.let {            texts.add(it.toString())        }        contentDescription?.let {            texts.add(it.toString())        }        return texts    }    /**     * 根据多个条件查找元素     * @param className 元素的类名     * @param viewId 可选的资源id过滤条件     * @param text 可选的文本过滤条件     * @param des 可选的描述文本过滤条件     * @return 符合所有条件的元素列表     */    fun findByTags(        className: String,        viewId: String? = null,        text: String? = null,        des: String? = null    ): List {        var nodeList = arrayListOf()        getAllNodes().forEach {            if (TextUtils.equals(className, it.className)) {                nodeList.add(it)            }        }        nodeList = viewId?.let {            if (it.isEmpty()) return@let nodeList            return@let arrayListOf().apply {                addAll(nodeList.filter {                    return@filter it.viewIdResourceName == viewId                })            }        } ?: let {            return@let nodeList        }        nodeList = text?.let {            if (it.isEmpty()) return@let nodeList            return@let arrayListOf().apply {                addAll(nodeList.filter {                    return@filter it.txt() == text                })            }        } ?: let { return@let nodeList }        nodeList = des?.let {            if (it.isEmpty()) return@let nodeList            return@let arrayListOf().apply {                addAll(nodeList.filter {                    return@filter it.des() == des                })            }        } ?: let { return@let nodeList }        return nodeList    }    /**     * 在指定元素范围内根据多个条件查找元素     * @param className 元素的类名     * @param viewId 可选的资源id过滤条件     * @param text 可选的文本过滤条件     * @param des 可选的描述文本过滤条件     * @return 符合所有条件的元素列表     */    fun AccessibilityNodeInfo.findByTags(        className: String,        viewId: String? = null,        text: String? = null,        des: String? = null    ): List {        var nodeList = arrayListOf()        getNodes().forEach {            if (TextUtils.equals(className, it.className)) {                nodeList.add(it)            }        }        nodeList = viewId?.let {            return@let arrayListOf().apply {                addAll(nodeList.filter {                    return@filter it.viewIdResourceName == viewId                })            }        } ?: let {            return@let nodeList        }        nodeList = text?.let {            return@let arrayListOf().apply {                addAll(nodeList.filter {                    return@filter it.txt() == text                })            }        } ?: let { return@let nodeList }        nodeList = des?.let {            return@let arrayListOf().apply {                addAll(nodeList.filter {                    return@filter it.des() == des                })            }        } ?: let { return@let nodeList }        return nodeList    }    /**     * 查找第一个符合指定类型的父元素     * @param className 要查找的父元素类名     * @return 找到的父元素,如果未找到则返回null     */    fun AccessibilityNodeInfo.findFirstParentByTags(className: String): AccessibilityNodeInfo? {        val nodeList = arrayListOf()        findFirstParentByTags(className, nodeList)        return nodeList.firstOrNull()    }    /**     * 递归查找符合指定类型的父元素     * @param className 要查找的父元素类名     * @param container 用于存储查找结果的列表     */    fun AccessibilityNodeInfo.findFirstParentByTags(className: String, container: ArrayList) {        getParent()?.let {            if (TextUtils.equals(className, it.className)) {                container.add(it)            } else {                it.findFirstParentByTags(className, container)            }        }    }    /**     * 获取当前窗口中的所有元素     * @return 包含所有元素的列表     */    fun getAllNodes(): ArrayList {        val nodeList = arrayListOf()        AssistsService.instance?.rootInActiveWindow?.getNodes(nodeList)        return nodeList    }    /**     * 获取指定元素下的所有子元素     * @return 包含所有子元素的列表     */    fun AccessibilityNodeInfo.getNodes(): ArrayList {        val nodeList = arrayListOf()        this.getNodes(nodeList)        return nodeList    }    /**     * 递归获取元素的所有子元素     * @param nodeList 用于存储子元素的列表     */    private fun AccessibilityNodeInfo.getNodes(nodeList: ArrayList) {        nodeList.add(this)        if (nodeList.size > 10000) return // 防止无限递归        for (index in 0 until this.childCount) {            getChild(index)?.getNodes(nodeList)        }    }    /**     * 查找元素的第一个可点击的父元素     * @return 找到的可点击父元素,如果未找到则返回null     */    fun AccessibilityNodeInfo.findFirstParentClickable(): AccessibilityNodeInfo? {        arrayOfNulls(1).apply {            findFirstParentClickable(this)            return this[0]        }    }    /**     * 递归查找可点击的父元素     * @param nodeInfo 用于存储查找结果的数组     */    private fun AccessibilityNodeInfo.findFirstParentClickable(nodeInfo: Array) {        if (parent?.isClickable == true) {            nodeInfo[0] = parent            return        } else {            parent?.findFirstParentClickable(nodeInfo)        }    }    /**     * 获取元素的直接子元素(不包括子元素的子元素)     * @return 包含直接子元素的列表     */    fun AccessibilityNodeInfo.getChildren(): ArrayList {        val nodes = arrayListOf()        for (i in 0 until this.childCount) {            val child = getChild(i)            nodes.add(child)        }        return nodes    }    /**     * 执行手势操作     * @param gesture 手势描述对象     * @param nonTouchableWindowDelay 窗口变为不可触摸后的延迟时间     * @return 手势是否执行成功     */    suspend fun dispatchGesture(        gesture: GestureDescription,        nonTouchableWindowDelay: Long = 100,    ): Boolean {        val completableDeferred = CompletableDeferred()        val gestureResultCallback = object : AccessibilityService.GestureResultCallback() {            override fun onCompleted(gestureDescription: GestureDescription?) {                CoroutineWrapper.launch { AssistsWindowManager.touchableByAll() }                completableDeferred.complete(true)            }            override fun onCancelled(gestureDescription: GestureDescription?) {                CoroutineWrapper.launch { AssistsWindowManager.touchableByAll() }                completableDeferred.complete(false)            }        }        val runResult = AssistsService.instance?.let {            AssistsWindowManager.nonTouchableByAll()            delay(nonTouchableWindowDelay)            runMain { it.dispatchGesture(gesture, gestureResultCallback, null) }        } ?: let {            returnfalse        }        if (!runResult) returnfalse        return completableDeferred.await()    }    /**     * 执行点击或滑动手势     * @param startLocation 起始位置坐标     * @param endLocation 结束位置坐标     * @param startTime 开始延迟时间     * @param duration 手势持续时间     * @return 手势是否执行成功     */    suspend fun gesture(        startLocation: FloatArray,        endLocation: FloatArray,        startTime: Long,        duration: Long,    ): Boolean {        val path = Path()        path.moveTo(startLocation[0], startLocation[1])        path.lineTo(endLocation[0], endLocation[1])        return gesture(path, startTime, duration)    }    /**     * 执行自定义路径的手势     * @param path 手势路径     * @param startTime 开始延迟时间     * @param duration 手势持续时间     * @return 手势是否执行成功     */    suspend fun gesture(        path: Path,        startTime: Long,        duration: Long,    ): Boolean {        val builder = GestureDescription.Builder()        val strokeDescription = GestureDescription.StrokeDescription(path, startTime, duration)        val gestureDescription = builder.addStroke(strokeDescription).build()        val deferred = CompletableDeferred()        val runResult = runMain {            return@runMain AssistsService.instance?.dispatchGesture(gestureDescription, object : AccessibilityService.GestureResultCallback() {                override fun onCompleted(gestureDescription: GestureDescription) {                    deferred.complete(true)                }                override fun onCancelled(gestureDescription: GestureDescription) {                    deferred.complete(false)                }            }, null) ?: let {                return@runMain false            }        }        if (!runResult) returnfalse        val result = deferred.await()        return result    }    /**     * 获取元素在屏幕中的位置信息     * @return 包含元素位置信息的Rect对象     */    fun AccessibilityNodeInfo.getBoundsInScreen(): Rect {        val boundsInScreen = Rect()        getBoundsInScreen(boundsInScreen)        return boundsInScreen    }    /**     * 点击元素     * @return 点击操作是否成功     */    fun AccessibilityNodeInfo.click(): Boolean {        return performAction(AccessibilityNodeInfo.ACTION_CLICK)    }    /**     * 长按元素     * @return 长按操作是否成功     */    fun AccessibilityNodeInfo.longClick(): Boolean {        return performAction(AccessibilityNodeInfo.ACTION_LONG_CLICK)    }    /**     * 在指定坐标位置执行点击手势     * @param x 横坐标     * @param y 纵坐标     * @param duration 点击持续时间     * @return 手势是否执行成功     */    suspend fun gestureClick(        x: Float,        y: Float,        duration: Long = 10    ): Boolean {        return gesture(            floatArrayOf(x, y), floatArrayOf(x, y),            0,            duration,        )    }    /**     * 在元素位置执行点击手势     * @param offsetX X轴偏移量     * @param offsetY Y轴偏移量     * @param switchWindowIntervalDelay 窗口切换延迟时间     * @param duration 点击持续时间     * @return 手势是否执行成功     */    suspend fun AccessibilityNodeInfo.nodeGestureClick(        offsetX: Float = ScreenUtils.getScreenWidth() * 0.01953f,        offsetY: Float = ScreenUtils.getScreenWidth() * 0.01953f,        switchWindowIntervalDelay: Long = 250,        duration: Long = 25    ): Boolean {        runMain { AssistsWindowManager.nonTouchableByAll() }        delay(switchWindowIntervalDelay)        val rect = getBoundsInScreen()        val result = gesture(            floatArrayOf(rect.left.toFloat() + offsetX, rect.top.toFloat() + offsetY),            floatArrayOf(rect.left.toFloat() + offsetX, rect.top.toFloat() + offsetY),            0,            duration,        )        delay(switchWindowIntervalDelay)        runMain { AssistsWindowManager.touchableByAll() }        return result    }    /**     * 在节点的右下角附近某一偏移位置执行点击(只用于评星🌟)     *     * @param offsetX 距离节点右边的偏移量(可为负数,单位 px)     * @param offsetY 距离节点下边的偏移量(可为负数,单位 px)     * @param switchWindowIntervalDelay 切换窗口等待时间     * @param duration 手势持续时间     */    suspend fun AccessibilityNodeInfo.nodeGestureClickAtBottomRightOffset(        haveOffsetY:Boolean = true,        switchWindowIntervalDelay: Long = 250,        duration: Long = 500,        horizontalRatio: Float = 0.95f,        verticalRatio: Float = 0.95f    ): Boolean {        require(horizontalRatio in 0f..1f) { \"horizontalRatio必须介于0-1之间\" }        require(verticalRatio in 0f..1f) { \"verticalRatio必须介于0-1之间\" }        runMain {            Log.d(AssistsCore.LOG_TAG, \"🛡️ 禁用窗口触摸\")            AssistsWindowManager.nonTouchableByAll()        }        delay(switchWindowIntervalDelay)        val rect = getBoundsInScreen().also {            Log.d(AssistsCore.LOG_TAG, \"📐 原始区域: ${it.toShortString()}\")            if (!it.isValid()) {                Log.e(AssistsCore.LOG_TAG, \"❌ 无效区域: ${it.toShortString()}\")                returnfalse            }        }        val safeRight = rect.right - 1        val safeBottom = rect.bottom - 1        Log.d(AssistsCore.LOG_TAG, \"⚙️ 安全边界: right=$safeRight, bottom=$safeBottom\")        val clickX = (rect.left + (safeRight - rect.left) * horizontalRatio)            .coerceIn(rect.left.toFloat(), safeRight.toFloat())            .also { Log.d(AssistsCore.LOG_TAG, \"➡️ 计算X: ${rect.left} + (${safeRight - rect.left}*$horizontalRatio) = $it\") }        val clickY = (rect.top + (safeBottom - rect.top) * verticalRatio)            .coerceIn(rect.top.toFloat(), safeBottom.toFloat())            .also { Log.d(AssistsCore.LOG_TAG, \"⬇️ 计算Y: ${rect.top} + (${safeBottom - rect.top}*$verticalRatio) = $it\") }        val intX = clickX.roundToInt().also {            Log.d(AssistsCore.LOG_TAG, \"🔄 X舍入: $clickX → $it\")        }        val intY = clickY.roundToInt().also {            Log.d(AssistsCore.LOG_TAG, \"🔄 Y舍入: $clickY → $it\")        }        if (intX !in rect.left..safeRight || intY !in rect.top..safeBottom) {            Log.e(AssistsCore.LOG_TAG, \"\"\"            ❗️ 坐标越界!            有效区域: [${rect.left},${rect.top}]-[${rect.right},${rect.bottom}]            安全边界: [right=$safeRight, bottom=$safeBottom]            实际坐标: ($intX, $intY)        \"\"\".trimIndent())            returnfalse        }        Log.d(AssistsCore.LOG_TAG, \"🎯 执行点击 ($intX, $intY)\")        var offsetY = 0        if(haveOffsetY){            offsetY = 191        }else{            offsetY = 0        }        val result = gesture(            floatArrayOf(clickX, clickY + offsetY),            floatArrayOf(clickX, clickY + offsetY),            0,            duration        ).also {            Log.d(AssistsCore.LOG_TAG, if (it) \"✅ 点击成功\"else\"❌ 点击失败\")        }        delay(switchWindowIntervalDelay)        runMain {            Log.d(AssistsCore.LOG_TAG, \"🔓 恢复窗口触摸\")            AssistsWindowManager.touchableByAll()        }        return result    }    // Rect扩展函数    private fun Rect.isValid(): Boolean = !isEmpty && width() > 0 && height() > 0    fun Rect.toShortString(): String = \"[$left,$top]-[$right,$bottom]\"    /**     * 在元素位置执行点击手势     * @param offsetX X轴偏移量     * @param offsetY Y轴偏移量     * @param switchWindowIntervalDelay 窗口切换延迟时间     * @param duration 点击持续时间     * @return 手势是否执行成功     */    suspend fun AccessibilityNodeInfo.nodeGestureCenterClick(        switchWindowIntervalDelay: Long = 250,        duration: Long = 25    ): Boolean {        runMain { AssistsWindowManager.nonTouchableByAll() }        delay(switchWindowIntervalDelay)        val rect = getBoundsInScreen()        val result = gesture(            floatArrayOf((rect.left.toFloat() + rect.right.toFloat())/2, (rect.top.toFloat() + rect.bottom.toFloat())/2),            floatArrayOf((rect.left.toFloat() + rect.right.toFloat())/2, (rect.top.toFloat() + rect.bottom.toFloat())/2),            0,            duration,        )        delay(switchWindowIntervalDelay)        runMain { AssistsWindowManager.touchableByAll() }        return result    }    /**     * 执行返回操作     * @return 返回操作是否成功     */    fun back(): Boolean {        return AssistsService.instance?.performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK) ?: false    }    /**     * 返回主屏幕     * @return 返回主屏幕操作是否成功     */    fun home(): Boolean {        return AssistsService.instance?.performGlobalAction(AccessibilityService.GLOBAL_ACTION_HOME) ?: false    }    /**     * 打开通知栏     * @return 打开通知栏操作是否成功     */    fun notifications(): Boolean {        return AssistsService.instance?.performGlobalAction(AccessibilityService.GLOBAL_ACTION_NOTIFICATIONS) ?: false    }    /**     * 显示最近任务     * @return 显示最近任务操作是否成功     */    fun recentApps(): Boolean {        return AssistsService.instance?.performGlobalAction(AccessibilityService.GLOBAL_ACTION_RECENTS) ?: false    }    /**     * 向元素粘贴文本     * @param text 要粘贴的文本     * @return 粘贴操作是否成功     */    fun AccessibilityNodeInfo.paste(text: String?): Boolean {        performAction(AccessibilityNodeInfo.ACTION_FOCUS)        AssistsService.instance?.let {            val clip = ClipData.newPlainText(\"${System.currentTimeMillis()}\", text)            val clipboardManager = (it.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager)            clipboardManager.setPrimaryClip(clip)            clipboardManager.primaryClip            return performAction(AccessibilityNodeInfo.ACTION_PASTE)        }        returnfalse    }    /**     * 选择元素中的文本     * @param selectionStart 选择起始位置     * @param selectionEnd 选择结束位置     * @return 文本选择操作是否成功     */    fun AccessibilityNodeInfo.selectionText(selectionStart: Int, selectionEnd: Int): Boolean {        val selectionArgs = Bundle()        selectionArgs.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT, selectionStart)        selectionArgs.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_END_INT, selectionEnd)        return performAction(AccessibilityNodeInfo.ACTION_SET_SELECTION, selectionArgs)    }    /**     * 设置元素的文本内容     * @param text 要设置的文本     * @return 设置文本操作是否成功     */    fun AccessibilityNodeInfo.setNodeText(text: String?): Boolean {        text ?: returnfalse        return performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, bundleOf().apply {            putCharSequence(                AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE,                text            )        })    }    /**     * 根据基准宽度计算实际X坐标     * @param baseWidth 基准宽度     * @param x 原始X坐标     * @return 计算后的实际X坐标     */    fun getX(baseWidth: Int, x: Int): Int {        val screenWidth = ScreenUtils.getScreenWidth()        return (x / baseWidth.toFloat() * screenWidth).toInt()    }    /**     * 根据基准高度计算实际Y坐标     * @param baseHeight 基准高度     * @param y 原始Y坐标     * @return 计算后的实际Y坐标     */    fun getY(baseHeight: Int, y: Int): Int {        var screenHeight = ScreenUtils.getScreenHeight()        if (screenHeight > baseHeight) {            screenHeight = baseHeight        }        return (y.toFloat() / baseHeight * screenHeight).toInt()    }    /**     * 获取当前应用在屏幕中的位置     * @return 应用窗口的位置信息,如果未找到则返回null     */    fun getAppBoundsInScreen(): Rect? {        return AssistsService.instance?.let {            return@let findById(\"android:id/content\").firstOrNull()?.getBoundsInScreen()        }    }    /**     * 初始化并缓存当前应用在屏幕中的位置     * @return 应用窗口的位置信息     */    fun initAppBoundsInScreen(): Rect? {        return getAppBoundsInScreen().apply {            appRectInScreen = this        }    }    /**     * 获取当前应用在屏幕中的宽度     * @return 应用窗口的宽度     */    fun getAppWidthInScreen(): Int {        return appRectInScreen?.let {            return@let it.right - it.left        } ?: ScreenUtils.getScreenWidth()    }    /**     * 获取当前应用在屏幕中的高度     * @return 应用窗口的高度     */    fun getAppHeightInScreen(): Int {        return appRectInScreen?.let {            return@let it.bottom - it.top        } ?: ScreenUtils.getScreenHeight()    }    /**     * 向前滚动可滚动元素     * @return 滚动操作是否成功     */    fun AccessibilityNodeInfo.scrollForward(): Boolean {        return performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD)    }    /**     * 向后滚动可滚动元素     * @return 滚动操作是否成功     */    fun AccessibilityNodeInfo.scrollBackward(): Boolean {        return performAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD)    }    suspend fun launchApp(intent: Intent): Boolean {        val completableDeferred = CompletableDeferred()        val view = View(AssistsService.instance).apply {            setOnClickListener {                runCatching {                    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)                    AssistsService.instance?.startActivity(intent)                    completableDeferred.complete(true)                }.onFailure {                    completableDeferred.complete(false)                }            }        }        runMain { AssistsWindowManager.add(view) }        CoroutineWrapper.launch {            delay(250)            val clickResult = gestureClick(ScreenUtils.getScreenWidth() / 2.toFloat(), ScreenUtils.getScreenHeight() / 2.toFloat())            if (!clickResult) {                completableDeferred.complete(false)            }            delay(250)            runMain { AssistsWindowManager.removeView(view) }        }        return completableDeferred.await()    }    suspend fun launchApp(packageName: String): Boolean {        val completableDeferred = CompletableDeferred()        val view = View(AssistsService.instance).apply {            setOnClickListener {                runCatching {                    val intent = AssistsService.instance?.packageManager?.getLaunchIntentForPackage(packageName)                    intent?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)                    AssistsService.instance?.startActivity(intent)                    completableDeferred.complete(true)                }.onFailure {                    completableDeferred.complete(false)                }            }        }        runMain { AssistsWindowManager.add(view) }        CoroutineWrapper.launch {            delay(250)            val clickResult = gestureClick(ScreenUtils.getScreenWidth() / 2.toFloat(), ScreenUtils.getScreenHeight() / 2.toFloat())            if (!clickResult) {                completableDeferred.complete(false)            }            delay(250)            runMain { AssistsWindowManager.removeView(view) }        }        return completableDeferred.await()    }    /**     * 在日志中输出元素的详细信息     * @param tag 日志标签     */    fun AccessibilityNodeInfo.logNode(tag: String = LOG_TAG) {        StringBuilder().apply {            val rect = getBoundsInScreen()            append(\"-------------------------------------\\n\")            append(\"位置:left=${rect.left}, top=${rect.top}, right=${rect.right}, bottom=${rect.bottom}, width=${rect.width()}, height=${rect.height()} \\n\")            append(\"文本:$text \\n\")            append(\"内容描述:$contentDescription \\n\")            append(\"id:$viewIdResourceName \\n\")            append(\"类型:${className} \\n\")            append(\"是否已经获取到到焦点:$isFocused \\n\")            append(\"是否可滚动:$isScrollable \\n\")            append(\"是否可点击:$isClickable \\n\")            append(\"是否可用:$isEnabled \\n\")            Log.d(tag, toString())        }    }}