feat: ###
优化可访问性服务功能 - 重新设计MyAccessibiltyService以提高性能和可读性 - 更新功能签名以提升清晰度和可维护性 - 在MyAccessibiltyService中实现关于边界情况的错误处理
This commit is contained in:
parent
629a5b64b8
commit
58f13dca0c
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue
Block a user