feat: ###
在应用程序中重构MainActivity和MainViewModel逻辑。 - 重构`MainActivity.kt`以提高代码可读性并删除不必要的导入 - 更新`MainViewModel.kt`以在`launchTargetApp`函数中传递额外参数到`findAndLaunchApp`方法调用
This commit is contained in:
parent
956ca015a0
commit
718915962d
8
.idea/deploymentTargetSelector.xml
generated
8
.idea/deploymentTargetSelector.xml
generated
@ -4,6 +4,14 @@
|
||||
<selectionStates>
|
||||
<SelectionState runConfigName="app">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DropdownSelection timestamp="2025-03-23T13:13:04.988067200Z">
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="LocalEmulator" identifier="path=/home/ls/.android/avd/Pixel_6_API_31.avd" />
|
||||
</handle>
|
||||
</Target>
|
||||
</DropdownSelection>
|
||||
<DialogSelection />
|
||||
</SelectionState>
|
||||
</selectionStates>
|
||||
</component>
|
||||
|
@ -3,13 +3,10 @@ package io.sixminutes.ridicule
|
||||
import android.app.ActivityManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
@ -141,7 +138,8 @@ class MainActivity : AppCompatActivity() {
|
||||
windowManager.removeView(it)
|
||||
isFloatingWindowVisible = false
|
||||
}
|
||||
|
||||
// 停止后台自动点击服务
|
||||
MyAccessibilityService.getInstance()?.stop()
|
||||
// 强制将 MainActivity 带到前台
|
||||
bringMainActivityToFront()
|
||||
|
||||
@ -158,6 +156,7 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试将应用的主Activity任务栈带到前台
|
||||
private fun bringMainActivityToFront() {
|
||||
val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
|
||||
val tasks = activityManager.getRunningTasks(10) // 获取最近的任务列表
|
||||
|
@ -64,7 +64,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
viewModelScope.launch {
|
||||
delay(1000) // 延迟 1000 毫秒,确保桌面已经显示
|
||||
try {
|
||||
MyAccessibilityService.getInstance()?.findAndLaunchApp("breakingnews")
|
||||
MyAccessibilityService.getInstance()?.findAndLaunchApp("breakingnews", "io.sixminutes.breakingnews", "Show Inter Ad 1: 3689d2816239b64e")
|
||||
} catch (e: Exception) {
|
||||
Log.e("ViewModel", "Launch app failed", e)
|
||||
updateState { it.copy(errorMessage = "Failed to launch app") }
|
||||
|
@ -1,59 +1,72 @@
|
||||
package io.sixminutes.ridicule
|
||||
|
||||
import android.accessibilityservice.AccessibilityService
|
||||
import android.accessibilityservice.GestureDescription
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.Path
|
||||
import android.graphics.Rect
|
||||
import android.os.Binder
|
||||
import android.os.Build
|
||||
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 kotlinx.coroutines.delay
|
||||
import java.util.LinkedList
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
class MyAccessibilityService : AccessibilityService() {
|
||||
// region 伴生对象与实例管理
|
||||
companion object {
|
||||
private const val TAG = "AccessibilityService"
|
||||
@Volatile private var instance: MyAccessibilityService? = null
|
||||
private const val DEFAULT_MAX_RETRIES = 5
|
||||
private const val DEFAULT_RETRY_INTERVAL = 4000L
|
||||
|
||||
// 使用双重校验锁保证线程安全
|
||||
@Volatile
|
||||
private var instance: MyAccessibilityService? = null
|
||||
|
||||
/**
|
||||
* 获取服务实例(双重校验锁实现线程安全)
|
||||
* @return 当前服务实例或null(如果未初始化)
|
||||
*/
|
||||
fun getInstance(): MyAccessibilityService? = instance ?: synchronized(this) {
|
||||
instance ?: null
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
private val binder = MyBinder()
|
||||
private var targetAppPackageName: String? = null // 目标应用的包名
|
||||
// region 类成员与初始化
|
||||
private var targetAppName: String? = null
|
||||
private var targetAppPackageName: String? = null
|
||||
private var currentAppPackageName: String? = null
|
||||
private var currentClassName: String? = null
|
||||
private var launcherButtonText: String? = null
|
||||
private val mainHandler by lazy { Handler(Looper.getMainLooper()) }
|
||||
private var running = false
|
||||
|
||||
private val isRetrying = AtomicBoolean(false) // 防止重复执行
|
||||
// 创建一个专门用于执行后台任务的 HandlerThread
|
||||
private val backgroundHandlerThread = HandlerThread("RetryOperationThread").apply { start() }
|
||||
private val backgroundHandler = Handler(backgroundHandlerThread.looper)
|
||||
|
||||
inner class MyBinder : Binder() {
|
||||
fun getService(): MyAccessibilityService = this@MyAccessibilityService
|
||||
}
|
||||
|
||||
override fun onServiceConnected() {
|
||||
super.onServiceConnected()
|
||||
instance = this
|
||||
Log.d(TAG, "Service connected")
|
||||
startMainActivitySafely() // 注意:无障碍服务启动Activity需谨慎
|
||||
startMainActivitySafely()
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region 生命周期管理
|
||||
override fun onDestroy() {
|
||||
cleanUpResources()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onInterrupt() {
|
||||
Log.w(TAG, "Service interrupted")
|
||||
}
|
||||
|
||||
override fun onAccessibilityEvent(event: AccessibilityEvent?) {
|
||||
event?.let {
|
||||
Log.v(TAG, "Event received: ${event.eventType}")
|
||||
// 添加实际的事件处理逻辑
|
||||
processAccessibilityEvent(event)
|
||||
}
|
||||
// 释放后台线程资源
|
||||
backgroundHandlerThread.quitSafely()
|
||||
}
|
||||
|
||||
override fun onUnbind(intent: Intent?): Boolean {
|
||||
@ -61,14 +74,24 @@ class MyAccessibilityService : AccessibilityService() {
|
||||
return super.onUnbind(intent)
|
||||
}
|
||||
|
||||
override fun onInterrupt() {
|
||||
Log.w(TAG, "Service interrupted")
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region 核心功能方法
|
||||
/**
|
||||
* 模拟点击屏幕上的指定坐标
|
||||
* @param x 点击的X坐标
|
||||
* @param y 点击的Y坐标
|
||||
* 模拟点击屏幕指定坐标
|
||||
* @param x X轴坐标(像素)
|
||||
* @param y Y轴坐标(像素)
|
||||
* 实现要点:
|
||||
* 1. 复用Rect对象提升性能
|
||||
* 2. 严格回收节点资源
|
||||
* 3. 校验节点可见性和可点击性
|
||||
*/
|
||||
fun simulateTap(x: Int, y: Int) {
|
||||
var root: AccessibilityNodeInfo? = null
|
||||
val tempRect = Rect() // 复用 Rect 对象提升性能
|
||||
val tempRect = Rect()
|
||||
|
||||
try {
|
||||
root = rootInActiveWindow ?: run {
|
||||
@ -78,24 +101,17 @@ class MyAccessibilityService : AccessibilityService() {
|
||||
|
||||
val wrappedRoot = AccessibilityNodeInfoCompat.wrap(root)
|
||||
try {
|
||||
val matchingNodes = findNodesByCondition(
|
||||
root = wrappedRoot,
|
||||
predicate = { node ->
|
||||
// 正确使用 bounds 判断逻辑
|
||||
node.isClickable &&
|
||||
node.isVisibleToUser() &&
|
||||
node.getBoundsInScreen(tempRect).let {
|
||||
!tempRect.isEmpty && tempRect.contains(x, y)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
matchingNodes.firstOrNull()?.let { targetNode ->
|
||||
if (targetNode.performAction(AccessibilityNodeInfo.ACTION_CLICK)) {
|
||||
findNodesByCondition(wrappedRoot) { node ->
|
||||
Log.d(TAG, "Checking node: [ID:${node.viewIdResourceName}], bounds: [${node.getBoundsInScreen(tempRect)}], clickable: ${node.isClickable}")
|
||||
node.isClickable && node.isVisibleToUser() &&
|
||||
node.run { getBoundsInScreen(tempRect); tempRect.contains(x, y) }
|
||||
}.firstOrNull()?.apply {
|
||||
if (performAction(AccessibilityNodeInfo.ACTION_CLICK)) {
|
||||
Log.d(TAG, "Tap performed at ($x, $y)")
|
||||
} else {
|
||||
Log.w(TAG, "Failed to perform click action")
|
||||
Log.w(TAG, "Failed to perform click at ($x, $y)")
|
||||
}
|
||||
recycle()
|
||||
} ?: Log.w(TAG, "No clickable node found at ($x, $y)")
|
||||
} finally {
|
||||
wrappedRoot.recycle()
|
||||
@ -108,233 +124,426 @@ class MyAccessibilityService : AccessibilityService() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据条件查找节点
|
||||
* @param root 根节点
|
||||
* @param predicate 判断条件
|
||||
* @return 符合条件的节点列表
|
||||
* 查找并启动应用(支持后续按钮点击)
|
||||
* @param appName 应用名称(支持模糊匹配)
|
||||
* @param buttonText 可选参数,需要点击的按钮文本
|
||||
* @param maxRetries 最大重试次数(默认10次)
|
||||
* @param retryInterval 重试间隔(默认1000ms)
|
||||
*/
|
||||
fun findAndLaunchApp (
|
||||
appName: String,
|
||||
packageName: String,
|
||||
buttonText: String? = null,
|
||||
) : Boolean {
|
||||
var success = false
|
||||
safeRootOperation { root ->
|
||||
findNodesByCondition(root) { node ->
|
||||
node.textMatches(appName) || node.contentDescriptionMatches(appName)
|
||||
}.firstOrNull()?.apply {
|
||||
if (performAction(AccessibilityNodeInfo.ACTION_CLICK)) {
|
||||
Log.d(TAG, "Successfully launched: $appName")
|
||||
targetAppPackageName = packageName
|
||||
launcherButtonText = buttonText
|
||||
targetAppName = appName
|
||||
success = true
|
||||
running = true
|
||||
} else {
|
||||
Log.w(TAG, "Failed to click app: $appName")
|
||||
}
|
||||
recycle()
|
||||
} ?: Log.w(TAG, "App not found: $appName")
|
||||
}
|
||||
return success
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
running = false
|
||||
}
|
||||
|
||||
private fun isTargetAppRunning(): Boolean {
|
||||
return rootInActiveWindow.let { currentRoot ->
|
||||
currentAppPackageName = currentRoot.packageName?.toString()
|
||||
currentAppPackageName == targetAppPackageName
|
||||
}
|
||||
}
|
||||
// region 点击逻辑优化
|
||||
/**
|
||||
* 智能点击方案(综合节点属性/父容器/坐标点击)
|
||||
* @return 是否点击成功
|
||||
*/
|
||||
private fun safeClickNode(node: AccessibilityNodeInfoCompat): Boolean {
|
||||
// 优先使用标准点击方式
|
||||
if (tryStandardClick(node)) return true
|
||||
|
||||
// 备用方案1:尝试点击可点击的父容器
|
||||
findClickableParent(node)?.let { parent ->
|
||||
if (parent.performAction(AccessibilityNodeInfo.ACTION_CLICK)) {
|
||||
Log.d(TAG, "Clicked via parent [ID:${parent.viewIdResourceName}]")
|
||||
parent.recycle()
|
||||
return true
|
||||
}
|
||||
parent.recycle()
|
||||
}
|
||||
|
||||
// 备用方案2:使用坐标点击
|
||||
return tryCoordinateClick(node)
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试标准点击方式
|
||||
* @return 点击是否成功
|
||||
*/
|
||||
private fun tryStandardClick(node: AccessibilityNodeInfoCompat): Boolean {
|
||||
return if (isNodeTrulyClickable(node)) {
|
||||
Log.d(TAG, "Attempting standard click")
|
||||
node.performAction(AccessibilityNodeInfo.ACTION_CLICK)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 增强的节点可点击性检查
|
||||
*/
|
||||
private fun isNodeTrulyClickable(node: AccessibilityNodeInfoCompat): Boolean {
|
||||
// 需要同时满足多个条件(不同Android版本可能有差异)
|
||||
return node.isClickable &&
|
||||
node.isVisibleToUser &&
|
||||
node.isEnabled &&
|
||||
(Build.VERSION.SDK_INT < Build.VERSION_CODES.P || node.isFocusable)
|
||||
}
|
||||
|
||||
/**
|
||||
* 深度查找可点击父容器(最多向上查找5层)
|
||||
*/
|
||||
private fun findClickableParent(node: AccessibilityNodeInfoCompat): AccessibilityNodeInfoCompat? {
|
||||
var current = node.parent
|
||||
var depth = 0
|
||||
val maxDepth = 5
|
||||
|
||||
while (current != null && depth < maxDepth) {
|
||||
if (isNodeTrulyClickable(current)) {
|
||||
return AccessibilityNodeInfoCompat.obtain(current)
|
||||
}
|
||||
current = current.parent
|
||||
depth++
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 坐标点击(带边界校验)
|
||||
*/
|
||||
private fun tryCoordinateClick(node: AccessibilityNodeInfoCompat): Boolean {
|
||||
val rect = Rect().apply {
|
||||
node.getBoundsInScreen(this)
|
||||
}
|
||||
|
||||
// 边界有效性检查
|
||||
if (rect.isEmpty) {
|
||||
Log.w(TAG, "Invalid node bounds")
|
||||
return false
|
||||
}
|
||||
|
||||
val screenMetrics = getScreenMetrics()
|
||||
if (!rect.intersect(screenMetrics)) {
|
||||
Log.w(TAG, "Node out of screen bounds")
|
||||
return false
|
||||
}
|
||||
|
||||
return dispatchGesture(
|
||||
createClickGesture(rect.centerX(), rect.centerY()), null, null
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建点击手势描述
|
||||
*/
|
||||
private fun createClickGesture(x: Int, y: Int): GestureDescription {
|
||||
val path = Path().apply { moveTo(x.toFloat(), y.toFloat()) }
|
||||
return GestureDescription.Builder()
|
||||
.addStroke(
|
||||
GestureDescription.StrokeDescription(
|
||||
path,
|
||||
0L, // 开始时间
|
||||
50L // 持续时间(毫秒)
|
||||
)
|
||||
).build()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取屏幕实际显示区域
|
||||
*/
|
||||
private fun getScreenMetrics(): Rect {
|
||||
val metrics = resources.displayMetrics
|
||||
return Rect(0, 0, metrics.widthPixels, metrics.heightPixels)
|
||||
}
|
||||
// endregion
|
||||
|
||||
|
||||
private fun findAndClickButton(
|
||||
buttonText: String,
|
||||
maxRetries: Int = DEFAULT_MAX_RETRIES,
|
||||
retryInterval: Long = DEFAULT_RETRY_INTERVAL
|
||||
): Boolean {
|
||||
Log.d(TAG, "Attempting to click button: '$buttonText'")
|
||||
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}]")
|
||||
Handler(Looper.getMainLooper()).postDelayed({}, 300)
|
||||
found = safeClickNode(this)
|
||||
// 添加点击后延迟
|
||||
Handler(Looper.getMainLooper()).postDelayed({}, 300)
|
||||
recycle()
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用重试操作模板
|
||||
* @param maxRetries 最大重试次数
|
||||
* @param interval 重试间隔
|
||||
* @param checkCondition 需要检查的条件(返回Boolean表示是否成功)
|
||||
* @param onSuccess 成功回调
|
||||
* @param onFailure 失败回调
|
||||
*/
|
||||
private fun retryOperation(
|
||||
maxRetries: Int,
|
||||
interval: Long,
|
||||
checkCondition: () -> Boolean,
|
||||
onSuccess: () -> Unit,
|
||||
onFailure: () -> Unit
|
||||
) {
|
||||
var retryCount = 0
|
||||
|
||||
fun attempt() {
|
||||
when {
|
||||
checkCondition() -> onSuccess()
|
||||
retryCount < maxRetries -> {
|
||||
retryCount++
|
||||
Log.d(TAG, "Retry attempt $retryCount")
|
||||
mainHandler.postDelayed(::attempt, interval)
|
||||
}
|
||||
else -> onFailure()
|
||||
}
|
||||
}
|
||||
|
||||
attempt()
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全执行根节点操作(自动处理资源回收)
|
||||
* @param block 需要在根节点上下文中执行的操作
|
||||
*/
|
||||
private inline fun safeRootOperation(block: (AccessibilityNodeInfoCompat) -> Unit) {
|
||||
try {
|
||||
rootInActiveWindow?.let { rawRoot ->
|
||||
val wrappedRoot = AccessibilityNodeInfoCompat.wrap(rawRoot)
|
||||
try {
|
||||
block(wrappedRoot)
|
||||
} finally {
|
||||
wrappedRoot.recycle()
|
||||
}
|
||||
} ?: Log.e(TAG, "No active window available")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Root operation failed", e)
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region 节点操作工具方法
|
||||
/**
|
||||
* 递归查找符合条件的节点
|
||||
* @param root 起始节点
|
||||
* @param predicate 节点匹配条件
|
||||
* @return 匹配的节点列表(需要手动回收)
|
||||
*/
|
||||
private fun findNodesByCondition(
|
||||
root: AccessibilityNodeInfoCompat,
|
||||
predicate: (AccessibilityNodeInfoCompat) -> Boolean
|
||||
): List<AccessibilityNodeInfoCompat> {
|
||||
val result = mutableListOf<AccessibilityNodeInfoCompat>()
|
||||
val queue = LinkedList<AccessibilityNodeInfoCompat>().apply { add(root) }
|
||||
val tempRect = Rect() // 复用 Rect 对象提升性能
|
||||
|
||||
while (queue.isNotEmpty()) {
|
||||
val node = queue.poll()
|
||||
try {
|
||||
// 统一使用带参数的 bounds 获取方式
|
||||
node.getBoundsInScreen(tempRect)
|
||||
if (predicate(node)) result.add(node)
|
||||
|
||||
// 遍历子节点
|
||||
for (i in 0 until node.childCount) {
|
||||
node.getChild(i)?.let {
|
||||
queue.add(AccessibilityNodeInfoCompat.wrap(it.unwrap()))
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
node.recycle()
|
||||
fun traverse(node: AccessibilityNodeInfoCompat) {
|
||||
if (predicate(node)) result.add(AccessibilityNodeInfoCompat.obtain(node))
|
||||
(0 until node.childCount).mapNotNull { node.getChild(it) }.forEach {
|
||||
traverse(it)
|
||||
}
|
||||
}
|
||||
|
||||
traverse(root)
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找并启动指定应用
|
||||
* @param appName 应用名称
|
||||
* 文本匹配扩展方法
|
||||
* @param text 要匹配的文本
|
||||
* @param exact 是否精确匹配(默认false)
|
||||
*/
|
||||
/**
|
||||
* 查找并启动指定应用
|
||||
* @param appName 应用名称
|
||||
*/
|
||||
fun findAndLaunchApp(appName: String) {
|
||||
try {
|
||||
rootInActiveWindow?.let { rawRoot ->
|
||||
// 1. 包装成 Compat 对象
|
||||
val wrappedRoot = AccessibilityNodeInfoCompat.wrap(rawRoot)
|
||||
try {
|
||||
// 2. 显式传递参数
|
||||
val matchingNodes = findNodesByCondition(
|
||||
root = wrappedRoot,
|
||||
predicate = { node ->
|
||||
// 3. 增强文本匹配逻辑
|
||||
node.text?.toString().orEmpty().contains(appName, true) ||
|
||||
node.contentDescription?.toString().orEmpty().contains(appName, true)
|
||||
}
|
||||
)
|
||||
|
||||
// 4. 处理匹配结果
|
||||
matchingNodes.firstOrNull()?.let { targetNode ->
|
||||
if (targetNode.performAction(AccessibilityNodeInfo.ACTION_CLICK)) {
|
||||
Log.d(TAG, "Successfully launched: $appName")
|
||||
|
||||
// 启动重试机制
|
||||
retryToDetectTargetApp(10, 3000) { success ->
|
||||
if (success) {
|
||||
Log.d(TAG, "Target app package name detected: $targetAppPackageName")
|
||||
} else {
|
||||
Log.e(TAG, "Failed to detect target app after 10 retries")
|
||||
showToast("Failed to launch $appName")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "Failed to click app: $appName")
|
||||
}
|
||||
targetNode.recycle()
|
||||
} ?: Log.w(TAG, "App not found: $appName")
|
||||
} finally {
|
||||
wrappedRoot.recycle() // 确保回收资源
|
||||
}
|
||||
} ?: Log.e(TAG, "No root window available")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "findAndLaunchApp error", e)
|
||||
}
|
||||
private fun AccessibilityNodeInfoCompat.textMatches(text: String, exact: Boolean = false): Boolean {
|
||||
return this.text?.toString()?.let {
|
||||
if (exact) it.equals(text, true) else it.contains(text, true)
|
||||
} ?: false
|
||||
}
|
||||
|
||||
/**
|
||||
* 重试检测目标应用
|
||||
* @param maxRetries 最大重试次数
|
||||
* @param delayMillis 每次重试的延时(毫秒)
|
||||
* @param onResult 结果回调,true 表示成功,false 表示失败
|
||||
* 内容描述匹配扩展方法
|
||||
* @param text 要匹配的文本
|
||||
* @param exact 是否精确匹配(默认false)
|
||||
*/
|
||||
private fun retryToDetectTargetApp(maxRetries: Int, delayMillis: Long, onResult: (Boolean) -> Unit) {
|
||||
var retryCount = 0
|
||||
|
||||
val retryRunnable = object : Runnable {
|
||||
override fun run() {
|
||||
rootInActiveWindow?.let { currentRoot ->
|
||||
val packageName = currentRoot.packageName?.toString()
|
||||
if (packageName != null && packageName != targetAppPackageName) {
|
||||
targetAppPackageName = packageName
|
||||
onResult(true) // 成功检测到目标应用
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
retryCount++
|
||||
if (retryCount < maxRetries) {
|
||||
Handler(Looper.getMainLooper()).postDelayed(this, delayMillis) // 继续重试
|
||||
} else {
|
||||
onResult(false) // 重试次数用尽,失败
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 启动第一次重试
|
||||
Handler(Looper.getMainLooper()).postDelayed(retryRunnable, delayMillis)
|
||||
private fun AccessibilityNodeInfoCompat.contentDescriptionMatches(text: String, exact: Boolean = false): Boolean {
|
||||
return this.contentDescription?.toString()?.let {
|
||||
if (exact) it.equals(text, true) else it.contains(text, true)
|
||||
} ?: false
|
||||
}
|
||||
// endregion
|
||||
|
||||
/**
|
||||
* 显示 Toast 消息
|
||||
* @param message 要显示的消息
|
||||
*/
|
||||
// region 其他辅助方法
|
||||
private fun showToast(message: String) {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
Toast.makeText(this, message, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
mainHandler.post { Toast.makeText(this, message, Toast.LENGTH_LONG).show() }
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理无障碍事件
|
||||
* @param event 无障碍事件
|
||||
*/
|
||||
private fun processAccessibilityEvent(event: AccessibilityEvent) {
|
||||
// 添加具体的事件处理逻辑
|
||||
when (event.eventType) {
|
||||
AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED -> {
|
||||
Log.d(TAG, "Window state changed: ${event.packageName}")
|
||||
Log.d(TAG, "target app package name: $targetAppPackageName")
|
||||
// 检测当前活动的应用是否发生了变化
|
||||
event.packageName?.let { currentPackageName ->
|
||||
if (targetAppPackageName != null && currentPackageName != targetAppPackageName) {
|
||||
Log.d(TAG, "Detected app switch from $targetAppPackageName to $currentPackageName")
|
||||
retryBackToTargetApp(10, 1000) { success ->
|
||||
if (success) {
|
||||
Log.d(TAG, "Successfully returned to $targetAppPackageName")
|
||||
} else {
|
||||
Log.e(TAG, "Failed to return to $targetAppPackageName after 10 retries")
|
||||
showToast("Failed to return to target app")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重试返回目标应用
|
||||
* @param maxRetries 最大重试次数
|
||||
* @param delayMillis 每次重试的延时(毫秒)
|
||||
* @param onResult 结果回调,true 表示成功,false 表示失败
|
||||
*/
|
||||
private fun retryBackToTargetApp(maxRetries: Int, delayMillis: Long, onResult: (Boolean) -> Unit) {
|
||||
var retryCount = 0
|
||||
|
||||
val retryRunnable = object : Runnable {
|
||||
override fun run() {
|
||||
// 检测当前窗口是否已经返回到目标应用
|
||||
rootInActiveWindow?.let { currentRoot ->
|
||||
val packageName = currentRoot.packageName?.toString()
|
||||
if (packageName == targetAppPackageName) {
|
||||
onResult(true) // 成功返回到目标应用
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试点击返回按钮
|
||||
performGlobalAction(GLOBAL_ACTION_BACK)
|
||||
Log.d(TAG, "Performed back action, retry count: $retryCount")
|
||||
|
||||
retryCount++
|
||||
if (retryCount < maxRetries) {
|
||||
Handler(Looper.getMainLooper()).postDelayed(this, delayMillis) // 继续重试
|
||||
} else {
|
||||
onResult(false) // 重试次数用尽,失败
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 启动第一次重试
|
||||
Handler(Looper.getMainLooper()).postDelayed(retryRunnable, delayMillis)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 安全启动主Activity
|
||||
*/
|
||||
private fun startMainActivitySafely() {
|
||||
try {
|
||||
startActivity(
|
||||
Intent(this, MainActivity::class.java)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||
)
|
||||
startActivity(Intent(this, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
})
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to start MainActivity", e)
|
||||
Log.e(TAG, "Start MainActivity failed", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理资源
|
||||
*/
|
||||
private fun cleanUpResources() {
|
||||
instance = null
|
||||
targetAppPackageName = null
|
||||
}
|
||||
// endregion
|
||||
|
||||
/**
|
||||
* 遍历节点树
|
||||
* @param block 处理每个节点的函数
|
||||
*/
|
||||
private fun AccessibilityNodeInfoCompat.traverse(block: (AccessibilityNodeInfoCompat) -> Boolean) {
|
||||
if (!block(this)) return
|
||||
for (i in 0 until childCount) {
|
||||
getChild(i)?.let { child ->
|
||||
child.traverse(block)
|
||||
child.recycle() // 确保每个子节点都被回收
|
||||
// region 无障碍事件处理
|
||||
override fun onAccessibilityEvent(event: AccessibilityEvent?) {
|
||||
event?.let {
|
||||
when (event.eventType) {
|
||||
AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED -> handleWindowChange(event)
|
||||
// 可根据需要扩展其他事件类型处理
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleWindowChange(event: AccessibilityEvent) {
|
||||
if (!running) return
|
||||
|
||||
currentAppPackageName = event.packageName?.toString() ?: return
|
||||
currentClassName = event.className?.toString() ?: return
|
||||
|
||||
// 增强日志输出
|
||||
Log.d(TAG, """
|
||||
Window Changed:
|
||||
├─ Current Package : $currentAppPackageName
|
||||
├─ Target Package : $targetAppPackageName
|
||||
├─ Class Name : ${currentClassName?.substringAfterLast('.')}
|
||||
└─ Full Class Name : $currentClassName
|
||||
""".trimIndent())
|
||||
if (targetAppPackageName == null) return
|
||||
|
||||
// 当检测到切换到其他应用时
|
||||
if (currentAppPackageName != targetAppPackageName) {
|
||||
Log.w(TAG, "Detected switch to non-target app: $currentAppPackageName")
|
||||
|
||||
// 先检查 isRetrying,如果为 true 则不重复启动
|
||||
if (isRetrying.compareAndSet(false, true)) {
|
||||
backgroundHandler.post {
|
||||
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 10 attempts")
|
||||
returnToHomeAndRestart()
|
||||
}
|
||||
)
|
||||
} finally {
|
||||
isRetrying.set(false) // 任务结束后重置
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "Retry operation is already running, skipping...")
|
||||
}
|
||||
}
|
||||
checkActivity()
|
||||
}
|
||||
|
||||
private fun checkActivity() {
|
||||
|
||||
Log.d(TAG, "Current App Package: $currentAppPackageName; Current App Class: $currentClassName; targetAppPackageName: $targetAppPackageName")
|
||||
if (currentAppPackageName == targetAppPackageName) {
|
||||
when (currentClassName) {
|
||||
"io.sixminutes.breakingnews.MainActivity" -> launcherButtonText?.let {
|
||||
findAndClickButton(
|
||||
it
|
||||
)
|
||||
}
|
||||
"io.sixminutes.breakingnews.ClickTrackerActivity" -> simulateTap(300, 400)
|
||||
"com.applovin.adview.AppLovinFullscreenActivity" -> simulateTap(300, 400)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// region 返回策略实现
|
||||
/**
|
||||
* 按返回键
|
||||
*/
|
||||
private fun performReturnGlobalAction(): Boolean {
|
||||
if (performGlobalAction(GLOBAL_ACTION_BACK)) {
|
||||
Log.d(TAG, "Attempted back navigation")
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回桌面并重启应用
|
||||
*/
|
||||
private fun returnToHomeAndRestart(): Boolean {
|
||||
Log.d(TAG, "Used home+restart strategy")
|
||||
return performGlobalAction(GLOBAL_ACTION_HOME)
|
||||
&& targetAppName != null
|
||||
&& targetAppPackageName != null
|
||||
&& findAndLaunchApp(targetAppName!!, targetAppPackageName!!)
|
||||
}
|
||||
|
||||
// endregion
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user