feat: ###

优化可访问性服务功能

- 重新设计MyAccessibiltyService以提高性能和可读性
- 更新功能签名以提升清晰度和可维护性
- 在MyAccessibiltyService中实现关于边界情况的错误处理
This commit is contained in:
lvlisong 2025-03-25 15:24:28 +08:00
parent 629a5b64b8
commit 58f13dca0c

View File

@ -3,19 +3,16 @@ package io.sixminutes.ridicule
import android.accessibilityservice.AccessibilityService
import android.accessibilityservice.AccessibilityServiceInfo
import android.accessibilityservice.GestureDescription
import android.content.Intent
import android.graphics.Path
import android.graphics.Rect
import android.os.Handler
import android.os.HandlerThread
import android.os.Looper
import android.util.Log
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import android.widget.Toast
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.random.Random
class MyAccessibilityService : AccessibilityService() {
@ -47,16 +44,11 @@ class MyAccessibilityService : AccessibilityService() {
private val mainHandler by lazy { Handler(Looper.getMainLooper()) }
private var running = false
private val isCheckAppPackageName = AtomicBoolean(false) // 防止重复执行
private val isCheckActivity = AtomicBoolean(false) // 防止重复执行
private var isCheckAppPackageName = false // 防止重复执行
private var isCheckActivity = false // 防止重复执行
private lateinit var tapIndicatorView: TapIndicatorView
// 创建一个专门用于执行后台任务的 HandlerThread
private val backgroundHandlerThread = HandlerThread("RetryOperationThread").apply { start() }
private val backgroundHandler = Handler(backgroundHandlerThread.looper)
override fun onServiceConnected() {
super.onServiceConnected()
tapIndicatorView = TapIndicatorView(this)
@ -80,8 +72,6 @@ class MyAccessibilityService : AccessibilityService() {
cleanUpResources()
super.onDestroy()
// 释放后台线程资源
backgroundHandlerThread.quitSafely()
}
override fun onUnbind(intent: Intent?): Boolean {
@ -201,10 +191,7 @@ class MyAccessibilityService : AccessibilityService() {
*/
private fun isNodeTrulyClickable(node: AccessibilityNodeInfoCompat): Boolean {
// 需要同时满足多个条件不同Android版本可能有差异
return node.isClickable &&
node.isVisibleToUser &&
node.isEnabled &&
node.isFocusable
return node.isClickable && node.isVisibleToUser && node.isEnabled && node.isFocusable
}
/**
@ -255,11 +242,9 @@ class MyAccessibilityService : AccessibilityService() {
*/
private fun createClickGesture(x: Int, y: Int): GestureDescription {
val path = Path().apply { moveTo(x.toFloat(), y.toFloat()) }
return GestureDescription.Builder()
.addStroke(
return GestureDescription.Builder().addStroke(
GestureDescription.StrokeDescription(
path,
0L, // 开始时间
path, 0L, // 开始时间
50L // 持续时间(毫秒)
)
).build()
@ -284,37 +269,29 @@ class MyAccessibilityService : AccessibilityService() {
var success = false
safeRootOperation { root ->
retryOperation(
maxRetries = maxRetries,
interval = retryInterval,
checkCondition = {
// 精确查找可点击节点(包含回收保障)
var found = false
findNodesByCondition(root) { node ->
node.textMatches(buttonText, exact = true) ||
node.contentDescriptionMatches(buttonText, exact = true)
}.firstOrNull()?.apply {
Log.d(
TAG,
"Found clickable button: '$buttonText' [ID:${viewIdResourceName}]"
)
backgroundHandler.postDelayed({}, 300)
found = safeClickNode(this)
// 添加点击后延迟
backgroundHandler.postDelayed({}, 300)
}
Log.d(TAG, "$buttonText is clicked: $found")
found
},
onSuccess = {
success = true
Log.i(TAG, "Successfully clicked button: '$buttonText'")
},
onFailure = {
Log.w(TAG, "Failed to click button after $maxRetries attempts: '$buttonText'")
showToast("Failed to click $buttonText")
retryOperation(maxRetries = maxRetries, interval = retryInterval, checkCondition = {
// 精确查找可点击节点(包含回收保障)
var found = false
findNodesByCondition(root) { node ->
node.textMatches(
buttonText,
exact = true
) || node.contentDescriptionMatches(buttonText, exact = true)
}.firstOrNull()?.apply {
Log.d(
TAG, "Found clickable button: '$buttonText' [ID:${viewIdResourceName}]"
)
found = safeClickNode(this)
}
)
Log.d(TAG, "$buttonText is clicked: $found")
found
}, onSuccess = {
success = true
Log.i(TAG, "Successfully clicked button: '$buttonText'")
}, onFailure = {
Log.w(TAG, "Failed to click button after $maxRetries attempts: '$buttonText'")
showToast("Failed to click $buttonText")
})
}
return success
@ -343,7 +320,7 @@ class MyAccessibilityService : AccessibilityService() {
retryCount < maxRetries -> {
retryCount++
Log.d(TAG, "Retry attempt $retryCount")
backgroundHandler.postDelayed(::attempt, interval)
mainHandler.postDelayed(::attempt, interval)
}
else -> onFailure()
@ -379,8 +356,7 @@ class MyAccessibilityService : AccessibilityService() {
* @return 匹配的节点列表需要手动回收
*/
private fun findNodesByCondition(
root: AccessibilityNodeInfoCompat,
predicate: (AccessibilityNodeInfoCompat) -> Boolean
root: AccessibilityNodeInfoCompat, predicate: (AccessibilityNodeInfoCompat) -> Boolean
): List<AccessibilityNodeInfoCompat> {
val result = mutableListOf<AccessibilityNodeInfoCompat>()
@ -401,8 +377,7 @@ class MyAccessibilityService : AccessibilityService() {
* @param exact 是否精确匹配默认false
*/
private fun AccessibilityNodeInfoCompat.textMatches(
text: String,
exact: Boolean = false
text: String, exact: Boolean = false
): Boolean {
return this.text?.toString()?.let {
if (exact) it.equals(text, true) else it.contains(text, true)
@ -415,8 +390,7 @@ class MyAccessibilityService : AccessibilityService() {
* @param exact 是否精确匹配默认false
*/
private fun AccessibilityNodeInfoCompat.contentDescriptionMatches(
text: String,
exact: Boolean = false
text: String, exact: Boolean = false
): Boolean {
return this.contentDescription?.toString()?.let {
if (exact) it.equals(text, true) else it.contains(text, true)
@ -448,7 +422,9 @@ class MyAccessibilityService : AccessibilityService() {
// region 无障碍事件处理
override fun onAccessibilityEvent(event: AccessibilityEvent?) {
event?.let {
if (event.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) handleWindowChange(event)
if (event.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) handleWindowChange(
event
)
// 可根据需要扩展其他事件类型处理
}
}
@ -475,16 +451,18 @@ class MyAccessibilityService : AccessibilityService() {
if (currentAppPackageName != targetAppPackageName) {
// 先检查 isCheckAppPackageName如果为 true 则不重复启动
if (isCheckAppPackageName.compareAndSet(false, true)) {
backgroundHandler.postDelayed(::checkAppPackageName, DEFAULT_RETRY_INTERVAL)
if (!isCheckAppPackageName) {
isCheckAppPackageName = true
mainHandler.postDelayed(::checkAppPackageName, DEFAULT_RETRY_INTERVAL)
} else {
Log.w(TAG, "CheckAppPackageName operation is already running, skipping...")
}
} else {
// 先检查 isCheckActivity如果为 true 则不重复启动
if (isCheckActivity.compareAndSet(false, true)) {
backgroundHandler.postDelayed(::checkActivity, DEFAULT_RETRY_INTERVAL)
if (!isCheckActivity) {
isCheckActivity = true
mainHandler.postDelayed(::checkActivity, DEFAULT_RETRY_INTERVAL)
} else {
Log.w(TAG, "CheckActivity operation is already running, skipping...")
}
@ -498,25 +476,19 @@ class MyAccessibilityService : AccessibilityService() {
private fun checkAppPackageName() {
Log.w(TAG, "Detected switch to non-target app: $currentAppPackageName")
try {
retryOperation(
DEFAULT_MAX_RETRIES,
DEFAULT_RETRY_INTERVAL,
checkCondition = {
val ok = isTargetAppRunning()
ok || performReturnGlobalAction()
Log.d(TAG, "Check $targetAppPackageName is activated: $ok")
ok
},
onSuccess = {
Log.i(TAG, "Successfully returned to target app")
},
onFailure = {
Log.e(TAG, "Failed to return after $DEFAULT_MAX_RETRIES attempts")
returnToHomeAndRestart()
}
)
retryOperation(DEFAULT_MAX_RETRIES, DEFAULT_RETRY_INTERVAL, checkCondition = {
val ok = isTargetAppRunning()
ok || performReturnGlobalAction()
Log.d(TAG, "Check $targetAppPackageName is activated: $ok")
ok
}, onSuccess = {
Log.i(TAG, "Successfully returned to target app")
}, onFailure = {
Log.e(TAG, "Failed to return after $DEFAULT_MAX_RETRIES attempts")
returnToHomeAndRestart()
})
} finally {
isCheckAppPackageName.set(false) // 任务结束后重置
isCheckAppPackageName = false // 任务结束后重置
}
}
@ -529,14 +501,20 @@ class MyAccessibilityService : AccessibilityService() {
}
"io.sixminutes.breakingnews.InterstitialActivity" -> Log.d(
TAG,
"current Activity is InterstitialActivity"
TAG, "current Activity is InterstitialActivity"
)
"io.sixminutes.breakingnews.ClickTrackerActivity" -> simulateTap(Random.nextInt(0, 720), Random.nextInt(0, 1080))
"io.sixminutes.breakingnews.ClickTrackerActivity" -> simulateTap(
Random.nextInt(
0,
720
), Random.nextInt(0, 1080)
)
"com.applovin.adview.AppLovinFullscreenActivity" -> simulateTap(648, 62)
}
} finally {
isCheckActivity.set(false)
isCheckActivity = false
}
}
@ -560,10 +538,10 @@ class MyAccessibilityService : AccessibilityService() {
*/
private fun returnToHomeAndRestart(): Boolean {
Log.d(TAG, "Used home+restart strategy")
return performGlobalAction(GLOBAL_ACTION_HOME)
&& targetAppName != null
&& targetAppPackageName != null
&& findAndLaunchApp(targetAppName!!, targetAppPackageName!!)
return performGlobalAction(GLOBAL_ACTION_HOME) && targetAppName != null && targetAppPackageName != null && findAndLaunchApp(
targetAppName!!,
targetAppPackageName!!
)
}
// endregion