安卓无障碍服务(Accessibility Service)是一种特殊类型的服务,旨在帮助残障用户或需要辅助功能的用户更好地使用设备。但它的功能远不止于此,开发者可以利用它实现自动化操作、界面监控和交互等功能。
dependencies {
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.4.1'
}
在AndroidManifest.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>
创建res/xml/service_config.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+)创建基础服务类MyAccessibilityService.kt
:
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()
}
}
}
}
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) }
}
}
}
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)
}
Android支持通过无障碍服务模拟复杂手势:
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)
}
监听系统级事件:
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", "触摸探索开始")
}
}
}
class FormFillerService : AccessibilityService() {
private val formData = mapOf(
"username" to "testuser",
"password" to "secure123",
"email" to "[email protected]"
)
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)
}
}
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)
}
}
}
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()
}
}
# 查看已启用的无障碍服务
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
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())
}
recycle()
释放资源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()
}
}
<uses-permission android:name="android.permission.BIND_ACCESSIBILITY_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
测试阶段:
应用商店要求:
持续更新:
// 接收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"))
}
// 使用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)
}
功能 | API 16-22 | API 23-28 | API 29+ |
---|---|---|---|
节点信息获取 | 基本支持 | 增强支持 | 受限 |
手势模拟 | 不支持 | 部分支持 | 完全支持 |
隐私限制 | 无 | 部分 | 严格 |
后台服务 | 允许 | 限制 | 严格限制 |
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 ?: ""
}
}