安卓无障碍脚本开发全教程

文章目录

第一部分:无障碍服务基础

1.1 无障碍服务概述

安卓无障碍服务(Accessibility Service)是一种特殊类型的服务,旨在帮助残障用户或需要辅助功能的用户更好地使用设备。但它的功能远不止于此,开发者可以利用它实现自动化操作、界面监控和交互等功能。

核心功能:
  • 界面内容访问:获取屏幕上的UI元素信息
  • 自动化操作:模拟点击、滑动等用户操作
  • 事件监控:监听窗口变化、通知、焦点改变等系统事件
  • 增强交互:为特定应用提供定制化辅助功能

1.2 基本原理与架构

用户操作 目标应用 无障碍服务 系统框架 触发界面变化 更新界面状态 发送AccessibilityEvent 处理事件 可选: 执行操作(点击/滑动等) 执行请求的操作 用户操作 目标应用 无障碍服务 系统框架

1.3 开发环境配置

所需工具:
  • Android Studio最新版
  • 安卓设备或模拟器(API 16+)
  • ADB调试工具
关键依赖:
gradle 复制代码
dependencies {
    implementation 'androidx.core:core-ktx:1.7.0'
    implementation 'androidx.appcompat:appcompat:1.4.1'
}

第二部分:创建基础无障碍服务

2.1 服务声明配置

AndroidManifest.xml中添加服务声明:

xml 复制代码
<service
    android:name=".MyAccessibilityService"
    android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
    android:exported="true">
    <intent-filter>
        <action android:name="android.accessibilityservice.AccessibilityService" />
    </intent-filter>
    <meta-data
        android:name="android.accessibilityservice"
        android:resource="@xml/service_config" />
</service>

2.2 服务配置文件

创建res/xml/service_config.xml

xml 复制代码
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:description="@string/accessibility_service_description"
    android:accessibilityEventTypes="typeAllMask"
    android:accessibilityFlags="flagDefault|flagRetrieveInteractiveWindows"
    android:canRetrieveWindowContent="true"
    android:settingsActivity="com.example.android.accessibility.ServiceSettingsActivity"
    android:canRequestFilterKeyEvents="true"
    android:canPerformGestures="true"
    android:notificationTimeout="100"
    android:packageNames="com.example.targetapp" />
关键属性说明:
  • accessibilityEventTypes:监听的事件类型
  • packageNames:指定监控的应用包名(可选)
  • canPerformGestures:允许执行手势操作(API 24+)

2.3 实现服务类

创建基础服务类MyAccessibilityService.kt

kotlin 复制代码
class MyAccessibilityService : AccessibilityService() {

    override fun onServiceConnected() {
        Log.d("A11yService", "无障碍服务已连接")
        // 可以在此处进行服务配置更新
        val info = AccessibilityServiceInfo().apply {
            eventTypes = AccessibilityEvent.TYPE_VIEW_CLICKED or 
                       AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
            feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC
            notificationTimeout = 100
            flags = AccessibilityServiceInfo.DEFAULT or
                   AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS
        }
        this.serviceInfo = info
    }

    override fun onAccessibilityEvent(event: AccessibilityEvent?) {
        event ?: return
        when (event.eventType) {
            AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED -> {
                handleWindowChange(event)
            }
            AccessibilityEvent.TYPE_VIEW_CLICKED -> {
                handleViewClick(event)
            }
        }
    }

    override fun onInterrupt() {
        Log.w("A11yService", "无障碍服务被中断")
    }

    private fun handleWindowChange(event: AccessibilityEvent) {
        val rootNode = rootInActiveWindow ?: return
        Log.d("A11yService", "窗口变化: ${event.packageName}")
        // 遍历视图树
        traverseNode(rootNode)
    }

    private fun traverseNode(node: AccessibilityNodeInfo, depth: Int = 0) {
        if (node.childCount == 0) {
            Log.d("A11yTree", "${" ".repeat(depth)}${node.viewIdResourceName}")
            return
        }
        
        for (i in 0 until node.childCount) {
            node.getChild(i)?.let { child ->
                traverseNode(child, depth + 1)
                child.recycle()
            }
        }
    }
}

第三部分:高级功能实现

3.1 节点查找与操作

常用查找方法:
kotlin 复制代码
fun findNodes(root: AccessibilityNodeInfo) {
    // 通过文本查找
    val byText = root.findAccessibilityNodeInfosByText("搜索")
    
    // 通过View ID查找(全限定ID)
    val byId = root.findAccessibilityNodeInfosByViewId("com.example.app:id/btnSubmit")
    
    // 通过类名查找
    val editTexts = mutableListOf<AccessibilityNodeInfo>()
    val queue: Queue<AccessibilityNodeInfo> = LinkedList()
    queue.add(root)
    
    while (queue.isNotEmpty()) {
        val current = queue.poll()
        if (current.className == "android.widget.EditText") {
            editTexts.add(current)
        }
        for (i in 0 until current.childCount) {
            current.getChild(i)?.let { queue.add(it) }
        }
    }
}
节点操作示例:
kotlin 复制代码
fun performActions(node: AccessibilityNodeInfo) {
    // 点击操作
    if (node.isClickable) {
        node.performAction(AccessibilityNodeInfo.ACTION_CLICK)
    }
    
    // 文本输入
    val arguments = Bundle().apply {
        putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, "Hello")
    }
    node.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, arguments)
    
    // 焦点控制
    node.performAction(AccessibilityNodeInfo.ACTION_FOCUS)
    
    // 滚动操作
    node.performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD)
}

3.2 手势模拟

Android支持通过无障碍服务模拟复杂手势:

kotlin 复制代码
fun performGesture(service: AccessibilityService) {
    val path = Path().apply {
        moveTo(100f, 100f)  // 起点
        lineTo(500f, 100f)   // 移动到右侧
        lineTo(500f, 500f)   // 向下移动
        lineTo(100f, 500f)   // 向左移动
        close()              // 闭合路径
    }
    
    val gestureBuilder = GestureDescription.Builder()
        .addStroke(GestureDescription.StrokeDescription(
            path, 
            0L,  // 开始时间
            1000L,  // 持续时间(毫秒)
            false  // 是否持续
        ))
    
    service.dispatchGesture(gestureBuilder.build(), object : AccessibilityService.GestureResultCallback() {
        override fun onCompleted(gestureDescription: GestureDescription?) {
            Log.d("Gesture", "手势完成")
        }
        
        override onCancelled(gestureDescription: GestureDescription?) {
            Log.w("Gesture", "手势取消")
        }
    }, null)
}

3.3 全局事件监听

监听系统级事件:

kotlin 复制代码
override fun onAccessibilityEvent(event: AccessibilityEvent) {
    when (event.eventType) {
        AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED -> {
            val notificationText = event.text.joinToString()
            Log.d("Notification", "新通知: $notificationText")
        }
        AccessibilityEvent.TYPE_ANNOUNCEMENT -> {
            Log.d("Announcement", "系统公告: ${event.text}")
        }
        AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_START -> {
            Log.d("Touch", "触摸探索开始")
        }
    }
}

第四部分:实战案例开发

4.1 自动填写表单

kotlin 复制代码
class FormFillerService : AccessibilityService() {

    private val formData = mapOf(
        "username" to "testuser",
        "password" to "secure123",
        "email" to "test@example.com"
    )

    override fun onAccessibilityEvent(event: AccessibilityEvent) {
        if (event.eventType != AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) return
        
        val rootNode = rootInActiveWindow ?: return
        
        formData.forEach { (fieldName, value) ->
            val nodes = rootNode.findAccessibilityNodeInfosByViewId("com.example.app:id/$fieldName")
            nodes.firstOrNull()?.let { field ->
                if (field.className == "android.widget.EditText") {
                    val args = Bundle().apply {
                        putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, value)
                    }
                    field.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args)
                }
            }
        }
        
        // 自动提交表单
        rootNode.findAccessibilityNodeInfosByViewId("com.example.app:id/submit")
            .firstOrNull()
            ?.performAction(AccessibilityNodeInfo.ACTION_CLICK)
    }
}

4.2 消息自动回复

kotlin 复制代码
class AutoReplyService : AccessibilityService() {

    private val replyMessages = listOf(
        "我正在开会,稍后回复您",
        "好的,收到",
        "谢谢通知"
    )

    override fun onAccessibilityEvent(event: AccessibilityEvent) {
        if (event.packageName != "com.whatsapp") return
        
        when (event.eventType) {
            AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED -> {
                // 处理通知事件
                val messages = event.text.filter { it.contains("发来消息") }
                if (messages.isNotEmpty()) {
                    replyToLatestMessage()
                }
            }
            AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED -> {
                // 处理界面文本变化
                if (isChatOpen()) {
                    autoReplyInChat()
                }
            }
        }
    }
    
    private fun replyToLatestMessage() {
        // 实现打开聊天界面并回复的逻辑
    }
    
    private fun isChatOpen(): Boolean {
        // 检测当前是否在聊天界面
    }
    
    private fun autoReplyInChat() {
        val root = rootInActiveWindow ?: return
        val messageNodes = root.findAccessibilityNodeInfosByViewId("com.whatsapp:id/message_text")
        
        // 获取最后一条消息
        val lastMessage = messageNodes.lastOrNull()?.text ?: return
        
        // 随机选择回复内容
        val randomReply = replyMessages.random()
        
        // 找到输入框并发送
        root.findAccessibilityNodeInfosByViewId("com.whatsapp:id/entry")
            .firstOrNull()
            ?.let { input ->
                val args = Bundle().apply {
                    putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, randomReply)
                }
                input.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args)
                
                // 发送消息
                root.findAccessibilityNodeInfosByViewId("com.whatsapp:id/send")
                    .firstOrNull()
                    ?.performAction(AccessibilityNodeInfo.ACTION_CLICK)
            }
    }
}

4.3 游戏自动化辅助

kotlin 复制代码
class GameHelperService : AccessibilityService() {

    private var isRunning = false
    private val handler = Handler(Looper.getMainLooper())
    private val clickRunnable = object : Runnable {
        override fun run() {
            performAutoClick()
            if (isRunning) {
                handler.postDelayed(this, 1000) // 每秒点击一次
            }
        }
    }

    override fun onServiceConnected() {
        val info = AccessibilityServiceInfo().apply {
            eventTypes = AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
            feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC
            flags = AccessibilityServiceInfo.FLAG_REPORT_VIEW_IDS
        }
        serviceInfo = info
    }

    override fun onAccessibilityEvent(event: AccessibilityEvent) {
        if (event.packageName != "com.game.package") return
        
        when (event.eventType) {
            AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED -> {
                checkGameState()
            }
        }
    }
    
    private fun checkGameState() {
        val root = rootInActiveWindow ?: return
        val battleNode = root.findAccessibilityNodeInfosByViewId("com.game.package:id/battle_indicator")
        
        if (battleNode.isNotEmpty()) {
            startAutoClicking()
        } else {
            stopAutoClicking()
        }
    }
    
    private fun startAutoClicking() {
        if (!isRunning) {
            isRunning = true
            handler.post(clickRunnable)
        }
    }
    
    private fun stopAutoClicking() {
        isRunning = false
        handler.removeCallbacks(clickRunnable)
    }
    
    private fun performAutoClick() {
        val root = rootInActiveWindow ?: return
        val attackBtn = root.findAccessibilityNodeInfosByViewId("com.game.package:id/attack_button")
            .firstOrNull()
        
        attackBtn?.performAction(AccessibilityNodeInfo.ACTION_CLICK)
        
        // 随机位置点击,避免被检测为机器人
        if (Math.random() < 0.3) {
            val randomX = (100..500).random()
            val randomY = (200..800).random()
            dispatchGesture(createClickGesture(randomX, randomY), null, null)
        }
    }
    
    private fun createClickGesture(x: Int, y: Int): GestureDescription {
        val clickPath = Path().apply {
            moveTo(x.toFloat(), y.toFloat())
        }
        
        return GestureDescription.Builder()
            .addStroke(GestureDescription.StrokeDescription(
                clickPath, 0, 50))
            .build()
    }
}

第五部分:调试与优化

5.1 调试技巧

ADB调试命令:
bash 复制代码
# 查看已启用的无障碍服务
adb shell settings get secure enabled_accessibility_services

# 启用服务
adb shell settings put secure enabled_accessibility_services com.example.pkg/.MyAccessibilityService

# 查看无障碍事件日志
adb shell logcat -s AccessibilityEvent
日志记录最佳实践:
kotlin 复制代码
fun logNodeInfo(node: AccessibilityNodeInfo) {
    val sb = StringBuilder().apply {
        append("View ID: ${node.viewIdResourceName}\n")
        append("Text: ${node.text}\n")
        append("Class: ${node.className}\n")
        append("Bounds: ${node.boundsInScreen}\n")
        append("Actions: ${node.actionList.joinToString()}\n")
        append("ChildCount: ${node.childCount}\n")
    }
    Log.d("NodeInfo", sb.toString())
}

5.2 性能优化

优化建议:
  1. 减少遍历深度:只查找必要的节点层级
  2. 及时回收节点 :调用recycle()释放资源
  3. 事件过滤:只监听必要的事件类型
  4. 延迟处理:对频繁事件使用防抖
  5. 后台处理:将耗时操作移到工作线程
优化示例:
kotlin 复制代码
class OptimizedService : AccessibilityService() {

    private val eventQueue = LinkedBlockingQueue<AccessibilityEvent>()
    private val workerThread = HandlerThread("EventProcessor").apply { start() }
    private val workerHandler = Handler(workerThread.looper)
    
    private val eventProcessor = object : Runnable {
        override fun run() {
            while (true) {
                val event = eventQueue.take()
                processEvent(event)
            }
        }
    }
    
    override fun onCreate() {
        super.onCreate()
        workerHandler.post(eventProcessor)
    }
    
    override fun onAccessibilityEvent(event: AccessibilityEvent) {
        // 快速将事件加入队列,避免阻塞主线程
        eventQueue.put(event)
    }
    
    private fun processEvent(event: AccessibilityEvent) {
        // 实际处理逻辑
    }
    
    override fun onDestroy() {
        workerThread.quitSafely()
        super.onDestroy()
    }
}

第六部分:发布与安全

6.1 权限与隐私

必要权限声明:
xml 复制代码
<uses-permission android:name="android.permission.BIND_ACCESSIBILITY_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
隐私注意事项:
  1. 明确告知用户:在隐私政策中说明数据收集范围
  2. 最小权限原则:只请求必要的权限
  3. 敏感数据处理:避免收集密码等敏感信息
  4. 数据加密:对存储的日志和数据进行加密

6.2 发布流程

  1. 测试阶段

    • 在不同安卓版本上测试
    • 在各种品牌设备上测试(特别是国产ROM)
    • 测试电池消耗情况
  2. 应用商店要求

    • 明确说明是无障碍辅助工具
    • 提供详细的使用说明视频
    • 如果是自动化工具,需遵守各商店政策
  3. 持续更新

    • 定期适配新安卓版本
    • 针对流行应用的特殊适配
    • 根据用户反馈优化功能

第七部分:高级主题

7.1 与其他技术的结合

与Tasker集成:
kotlin 复制代码
// 接收Tasker的广播意图
private val taskerReceiver = object : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        if (intent.action == "net.dinglisch.android.tasker.ACTION_TRIGGER") {
            val task = intent.getStringExtra("task")
            when (task) {
                "start_automation" -> startAutomation()
                "stop_automation" -> stopAutomation()
            }
        }
    }
}

override fun onCreate() {
    super.onCreate()
    registerReceiver(taskerReceiver, IntentFilter("net.dinglisch.android.tasker.ACTION_TRIGGER"))
}
使用机器学习:
kotlin 复制代码
// 使用ML Kit识别屏幕内容
fun detectTextFromScreen(bitmap: Bitmap): String {
    val recognizer = TextRecognition.getClient(TextRecognizerOptions.DEFAULT_OPTIONS)
    val image = InputImage.fromBitmap(bitmap, 0)
    
    return try {
        val result = recognizer.process(image).await()
        result.text
    } catch (e: Exception) {
        Log.e("ML", "识别失败", e)
        ""
    }
}

// 截图并处理
fun captureAndAnalyze() {
    val projection = MediaProjectionManager.createScreenCaptureIntent()
    // 需要先获取用户授权...
    
    val imageReader = ImageReader.newInstance(
        screenWidth, screenHeight, 
        PixelFormat.RGBA_8888, 2
    )
    
    imageReader.setOnImageAvailableListener({ reader ->
        val image = reader.acquireLatestImage()
        // 转换为Bitmap并传递给识别器
        val text = detectTextFromScreen(convertImageToBitmap(image))
        Log.d("ScreenText", "识别结果: $text")
        image.close()
    }, handler)
}

7.2 跨版本兼容性处理

版本差异处理表:
功能 API 16-22 API 23-28 API 29+
节点信息获取 基本支持 增强支持 受限
手势模拟 不支持 部分支持 完全支持
隐私限制 部分 严格
后台服务 允许 限制 严格限制
兼容性代码示例:
kotlin 复制代码
fun performActionCompat(node: AccessibilityNodeInfo, action: Int, args: Bundle? = null): Boolean {
    return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        node.performAction(action, args)
    } else {
        node.performAction(action)
    }
}

fun getNodeTextCompat(node: AccessibilityNodeInfo): String {
    return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        node.text?.toString() ?: ""
    } else {
        node.text ?: ""
    }
}
相关推荐
安卓理事人21 小时前
安卓LinkedBlockingQueue消息队列
android
万能的小裴同学1 天前
Android M3U8视频播放器
android·音视频
q***57741 天前
MySql的慢查询(慢日志)
android·mysql·adb
JavaNoober1 天前
Android 前台服务 "Bad Notification" 崩溃机制分析文档
android
城东米粉儿1 天前
关于ObjectAnimator
android
zhangphil1 天前
Android渲染线程Render Thread的RenderNode与DisplayList,引用Bitmap及Open GL纹理上传GPU
android
火柴就是我1 天前
从头写一个自己的app
android·前端·flutter
lichong9511 天前
XLog debug 开启打印日志,release 关闭打印日志
android·java·前端
用户69371750013841 天前
14.Kotlin 类:类的形态(一):抽象类 (Abstract Class)
android·后端·kotlin
火柴就是我1 天前
NekoBoxForAndroid 编译libcore.aar
android