package com.hikoncont.service.modules import android.accessibilityservice.AccessibilityService import android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_BACK import android.content.Context import android.content.Intent import android.net.Uri import android.os.Build import android.provider.Settings import android.util.Log import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityNodeInfo import com.hikoncont.service.AccessibilityRemoteService import kotlinx.coroutines.* /** * WRITE_SETTINGS权限管理器 * * 主要职责: * 1. 检查WRITE_SETTINGS权限状态 * 2. 自动跳转到系统设置页面 * 3. 自动查找并点击开启按钮 * 4. 处理权限申请完成后的流程 */ class WriteSettingsPermissionManager( private val service: AccessibilityRemoteService, private val context: Context ) { companion object { private const val TAG = "WriteSettingsPermissionManager" private const val PERMISSION_CHECK_INTERVAL = 2000L // 2秒检查一次 private const val MAX_AUTO_CLICK_ATTEMPTS = 8 // 增加最大自动点击尝试次数 private const val SETTINGS_PAGE_TIMEOUT = 45000L // 增加到45秒设置页面超时 private const val CLICK_DELAY = 1000L // 点击后等待时间 } /** * 设备策略类型枚举 */ private enum class DeviceStrategy { TEXT_BASED_CLICK, // 文本相对定位策略(优先使用) INTELLIGENT_DETECTION // 智能检测策略(备用方案) } // 协程作用域 private var permissionScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) // 权限申请状态 @Volatile private var isRequestingWriteSettings = false @Volatile private var isInSettingsPage = false @Volatile private var autoClickAttempts = 0 @Volatile private var permissionRequestStartTime = 0L @Volatile private var lastEventTime = 0L @Volatile private var lastDetectedPackage = "" @Volatile private var packageStableCount = 0 // 🔥 新增:设备策略相关字段 @Volatile private var deviceStrategy: DeviceStrategy = DeviceStrategy.TEXT_BASED_CLICK @Volatile private var textSearchFailCount = 0 // 文本搜索连续失败次数 // 🔥 优化:防止重复处理的标志 @Volatile private var permissionGrantedProcessed = false // 权限申请监听Job private var permissionMonitorJob: Job? = null private var autoClickJob: Job? = null // 节点管理 - 用于避免重复回收 private val managedNodes = mutableSetOf() // 错误控件记录 - 记录已经尝试过但失败的控件,避免重复点击 private val failedControlsHistory = mutableSetOf() /** * 检查WRITE_SETTINGS权限是否已授权 */ fun hasWriteSettingsPermission(): Boolean { return try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { val hasPermission = Settings.System.canWrite(context) Log.d(TAG, "🔍 WRITE_SETTINGS权限状态: $hasPermission") hasPermission } else { // Android 6.0以下版本不需要运行时权限 Log.d(TAG, "🔍 Android 6.0以下版本,WRITE_SETTINGS权限默认已授权") true } } catch (e: Exception) { Log.e(TAG, "❌ 检查WRITE_SETTINGS权限失败", e) false } } /** * 开始WRITE_SETTINGS权限申请流程 */ fun startWriteSettingsPermissionRequest() { if (isRequestingWriteSettings) { Log.w(TAG, "⚠️ WRITE_SETTINGS权限申请已在进行中") return } if (hasWriteSettingsPermission()) { Log.i(TAG, "✅ WRITE_SETTINGS权限已授权,跳过申请") onWriteSettingsPermissionGranted() return } Log.i(TAG, "🚀 开始WRITE_SETTINGS权限申请流程") Log.i( TAG, "📱 设备信息: 品牌=${android.os.Build.BRAND}, 型号=${android.os.Build.MODEL}, SDK=${android.os.Build.VERSION.SDK_INT}" ) // 🔥 修复:首先停止之前的权限申请 stopPermissionRequest() // 🔥 修复:确保协程作用域处于活跃状态 if (!permissionScope.isActive) { Log.w(TAG, "⚠️ 协程作用域不活跃,重新创建") permissionScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) } // 重置并设置状态 resetDetectionState() isRequestingWriteSettings = true permissionRequestStartTime = System.currentTimeMillis() Log.i(TAG, "🚀 协程作用域状态: isActive=${permissionScope.isActive}") // 跳转到系统设置页面 openWriteSettingsPage() // 启动权限监听 startPermissionMonitoring() // 根据策略启动不同的检测机制 when (deviceStrategy) { DeviceStrategy.TEXT_BASED_CLICK -> startTextBasedClickStrategy() DeviceStrategy.INTELLIGENT_DETECTION -> startIntelligentDetectionStrategy() } } /** * 🔥 新增:启动文本相对定位策略 */ private fun startTextBasedClickStrategy() { Log.i(TAG, "🎯 启动文本相对定位策略") // 启动权限监听(通用) // 文本定位策略主要通过定时执行文本定位点击 startCoordinateClickDetection() } /** * 🔥 优化:启动智能检测策略 */ private fun startIntelligentDetectionStrategy() { Log.i(TAG, "🎯 启动智能检测策略 (通用设备)") // 启动定时轮询检测机制(原有逻辑) startPeriodicDetection() } /** * 🔥 优化:坐标点击检测机制 */ private fun startCoordinateClickDetection() { Log.i(TAG, "🔄 启动坐标点击检测机制") try { permissionScope.launch { Log.i(TAG, "✅ 坐标点击检测协程已启动") var detectionCount = 0 val maxDetections = 10 // 坐标点击策略检测次数较少 while (isRequestingWriteSettings && isActive && detectionCount < maxDetections) { delay(1000) // 每2秒检测一次,给页面更多时间稳定 detectionCount++ Log.i(TAG, "🔍 坐标点击检测第${detectionCount}次(共${maxDetections}次)") // 🔥 CRITICAL: 首先检查权限状态 - 这是最高优先级 if (hasWriteSettingsPermission()) { Log.i(TAG, "✅ 坐标点击检测发现权限已授权") onWriteSettingsPermissionGranted() break } // 检查页面稳定性 try { val rootNode = service.rootInActiveWindow if (rootNode != null) { val currentPackage = rootNode.packageName?.toString() ?: "" // 检查页面稳定性 if (currentPackage == lastDetectedPackage) { packageStableCount++ } else { packageStableCount = 1 lastDetectedPackage = currentPackage } Log.i( TAG, "🔍 坐标点击检测当前页面: $currentPackage (稳定计数: $packageStableCount)" ) // 只有页面稳定2次以上才执行坐标点击 if (packageStableCount >= 2) { if (isSettingsPackage(currentPackage) || isExpectedPermissionPage( currentPackage ) ) { Log.i(TAG, "🎯 页面稳定且在权限页面,执行坐标点击") val coordinateClickSuccess = attemptCoordinateClick() if (coordinateClickSuccess) { Log.i(TAG, "✅ 坐标点击成功") break // 点击成功后退出循环 } } else if (currentPackage == context.packageName) { Log.w(TAG, "⚠️ 坐标点击检测发现应用返回主应用,重新打开设置页面") safeRecycleNode(rootNode) openWriteSettingsPage() delay(3000) // 等待页面加载后继续检测 continue } } safeRecycleNode(rootNode) } } catch (e: Exception) { Log.e(TAG, "❌ 坐标点击检测失败", e) } } if (detectionCount >= maxDetections && isRequestingWriteSettings) { Log.w(TAG, "⚠️ 坐标点击检测次数达到上限,尝试备用方案") // 坐标点击失败后,可以尝试智能检测作为备用 Log.i(TAG, "🔄 切换到智能检测策略作为备用方案") deviceStrategy = DeviceStrategy.INTELLIGENT_DETECTION startIntelligentDetectionStrategy() } Log.i(TAG, "🏁 坐标点击检测协程结束") } } catch (e: Exception) { Log.e(TAG, "❌ 启动坐标点击检测失败", e) } } /** * 打开WRITE_SETTINGS权限设置页面 */ fun openWriteSettingsPage() { try { Log.i(TAG, "📱 打开WRITE_SETTINGS权限设置页面") Log.i(TAG, "📦 应用包名: ${context.packageName}") val intent = Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS).apply { data = Uri.parse("package:${context.packageName}") addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } // 检查Intent是否可以被处理 val resolveInfo = context.packageManager.resolveActivity(intent, 0) if (resolveInfo != null) { Log.i(TAG, "✅ 找到处理Intent的Activity: ${resolveInfo.activityInfo.packageName}") context.startActivity(intent) isInSettingsPage = true Log.i(TAG, "✅ 已启动WRITE_SETTINGS权限设置页面") } else { Log.w(TAG, "⚠️ 系统不支持ACTION_MANAGE_WRITE_SETTINGS,尝试备用方案") openGeneralSettingsPage() } } catch (e: Exception) { Log.e(TAG, "❌ 打开WRITE_SETTINGS权限设置页面失败", e) // 如果无法打开特定页面,尝试打开通用设置页面 openGeneralSettingsPage() } } /** * 打开通用设置页面(备用方案) */ private fun openGeneralSettingsPage() { try { Log.i(TAG, "📱 尝试打开通用设置页面(备用方案)") val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { data = Uri.parse("package:${context.packageName}") addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } val resolveInfo = context.packageManager.resolveActivity(intent, 0) if (resolveInfo != null) { Log.i(TAG, "✅ 找到通用设置Activity: ${resolveInfo.activityInfo.packageName}") context.startActivity(intent) isInSettingsPage = true Log.i(TAG, "✅ 已启动通用设置页面") } else { Log.e(TAG, "❌ 系统不支持应用设置页面") onWriteSettingsPermissionFailed("系统不支持设置页面") } } catch (e: Exception) { Log.e(TAG, "❌ 打开通用设置页面也失败", e) onWriteSettingsPermissionFailed("无法打开设置页面") } } /** * 启动定时轮询检测机制 */ private fun startPeriodicDetection() { Log.i(TAG, "🔄 准备启动定时检测机制,协程作用域状态: ${permissionScope.isActive}") try { permissionScope.launch { Log.i(TAG, "✅ 定时检测协程已启动") var detectionCount = 0 val maxDetections = 15 // 最多检测15次(45秒) while (isRequestingWriteSettings && isActive && detectionCount < maxDetections) { delay(1500) // 每1.5秒检测一次 detectionCount++ Log.i(TAG, "🔍 定时检测第${detectionCount}次(共${maxDetections}次)") // 🔥 CRITICAL: 首先检查权限状态 - 这是最高优先级 if (hasWriteSettingsPermission()) { Log.i(TAG, "✅ 定时检测发现权限已授权") onWriteSettingsPermissionGranted() break } // 执行控件检测 try { val rootNode = service.rootInActiveWindow if (rootNode != null) { val currentPackage = rootNode.packageName?.toString() ?: "" // 🔥 优化:增加更详细的页面状态日志 Log.d( TAG, "🔍 获取到根节点: 类名=${rootNode.className}, 包名=$currentPackage" ) // 检查页面稳定性 if (currentPackage == lastDetectedPackage) { packageStableCount++ } else { packageStableCount = 1 lastDetectedPackage = currentPackage Log.i(TAG, "📄 页面发生变化: $lastDetectedPackage → $currentPackage") } Log.i( TAG, "🔍 定时检测当前页面: $currentPackage (稳定计数: $packageStableCount)" ) // 🔥 优化:降低页面稳定性要求,提高检测灵活性 if (packageStableCount >= 1 || detectionCount >= 3) { // 🔥 优化:如果检测次数较多,即使页面不稳定也要尝试检测 if (detectionCount >= 3) { Log.i( TAG, "🔍 检测次数较多(${detectionCount}),即使页面不稳定也执行检测" ) } // 🔥 CRITICAL: 检查是否在正确的权限页面 if (!isSettingsPackage(currentPackage) && !isExpectedPermissionPage( currentPackage ) ) { Log.w( TAG, "🔍 页面稳定,但不在权限页面,跳过检测: $currentPackage" ) // 🔥 CRITICAL: 特别检查是否返回主应用 if (currentPackage == context.packageName) { Log.w(TAG, "⚠️ 定时检测发现应用返回主应用,重新打开设置页面") safeRecycleNode(rootNode) // 重新打开权限设置页面 openWriteSettingsPage() // 等待页面加载后继续检测 delay(1000) continue } safeRecycleNode(rootNode) continue } // 🔥 CRITICAL: 检查是否是正确的WRITE_SETTINGS页面 managedNodes.add(rootNode) if (!isCorrectWriteSettingsPage()) { Log.w(TAG, "🔍 页面稳定,但不是正确的WRITE_SETTINGS页面,跳过检测") safeRecycleNode(rootNode) continue } // 🔥 优化:智能检测策略,直接执行多策略查找 if (isSettingsPackage(currentPackage) || currentPackage.contains( "settings", true ) ) { Log.i(TAG, "🎯 页面稳定,发现设置页面,执行查找") val enableButton = findEnableButtonWithMultipleStrategies(rootNode) if (enableButton != null) { Log.i(TAG, "✅ 定时检测找到按钮,执行点击") performClickSafe(enableButton) break // 找到并点击后退出循环 } else { Log.d(TAG, "🔍 定时检测未找到按钮") } } else { // 即使不是设置页面,也尝试检测(可能是特殊的权限页面) Log.i( TAG, "🔍 页面稳定,尝试在非设置页面查找控件: $currentPackage" ) val enableButton = findEnableButtonWithMultipleStrategies(rootNode) if (enableButton != null) { Log.i(TAG, "✅ 在非设置页面找到按钮,执行点击") performClickSafe(enableButton) break } } } else { Log.d(TAG, "🔍 页面还不稳定,等待下次检测") } // 🔥 优化:确保根节点始终被正确回收 safeRecycleNode(rootNode) } else { // 🔥 优化:无法获取根节点时的日志 Log.w(TAG, "⚠️ 定时检测第${detectionCount}次:无法获取根节点") } } catch (e: Exception) { Log.e(TAG, "❌ 定时检测失败 (第${detectionCount}次)", e) // 🔥 优化:增加异常详细信息和恢复机制 Log.e(TAG, "❌ 异常详情: ${e.javaClass.simpleName} - ${e.message}") Log.e( TAG, "❌ 当前状态: packageStableCount=$packageStableCount, lastDetectedPackage=$lastDetectedPackage" ) // 🔥 优化:异常后继续检测,不要停止整个流程 try { // 等待一段时间后继续 delay(1000) Log.i(TAG, "🔄 异常恢复:继续下一次检测") } catch (recoveryException: Exception) { Log.e(TAG, "❌ 异常恢复失败", recoveryException) } } } if (detectionCount >= maxDetections && isRequestingWriteSettings) { Log.w(TAG, "⚠️ 定时检测次数达到上限,检查设备策略") // 🔥 优化:根据设备策略执行不同的备用方案 when (deviceStrategy) { DeviceStrategy.TEXT_BASED_CLICK -> { Log.w(TAG, "⚠️ 文本定位策略超时,尝试最后一次文本定位") val lastAttempt = attemptCoordinateClick() if (!lastAttempt) { Log.e(TAG, "❌ 坐标点击策略最终失败") onWriteSettingsPermissionFailed("坐标点击策略超时失败") } } DeviceStrategy.INTELLIGENT_DETECTION -> { Log.w(TAG, "⚠️ 智能检测策略超时,尝试备用方案") openGeneralSettingsPage() } } } Log.i(TAG, "🏁 定时检测协程结束") } } catch (e: Exception) { Log.e(TAG, "❌ 启动定时检测失败", e) } } /** * 启动权限监听 */ private fun startPermissionMonitoring() { Log.i(TAG, "🔄 准备启动权限监听") permissionMonitorJob?.cancel() try { permissionMonitorJob = permissionScope.launch { try { Log.i(TAG, "🔍 启动权限监听") while (isRequestingWriteSettings && isActive) { // 检查权限是否已获取 if (hasWriteSettingsPermission()) { Log.i(TAG, "✅ 检测到WRITE_SETTINGS权限已授权") onWriteSettingsPermissionGranted() break } // 检查是否超时 val elapsed = System.currentTimeMillis() - permissionRequestStartTime if (elapsed > SETTINGS_PAGE_TIMEOUT) { Log.w(TAG, "⚠️ WRITE_SETTINGS权限申请超时") onWriteSettingsPermissionTimeout() break } delay(PERMISSION_CHECK_INTERVAL) } } catch (e: Exception) { Log.e(TAG, "❌ 权限监听异常", e) onWriteSettingsPermissionFailed("权限监听异常: ${e.message}") } } } catch (e: Exception) { Log.e(TAG, "❌ 启动权限监听失败", e) } } /** * 处理无障碍事件,用于自动点击 */ fun handleAccessibilityEvent(event: AccessibilityEvent) { if (!isRequestingWriteSettings || !isInSettingsPage) { return } // 避免重复处理相同事件,降低处理频率 val currentTime = System.currentTimeMillis() if (currentTime - lastEventTime < 2000) { // 改为2秒间隔 return } lastEventTime = currentTime try { when (event.eventType) { AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED -> { val packageName = event.packageName?.toString() ?: "" // 检查是否在设置页面 if (isSettingsPackage(packageName)) { Log.d(TAG, "🔍 检测到设置页面: $packageName") // 取消之前的自动点击任务 autoClickJob?.cancel() // 延长等待时间,让设置页面完全加载 autoClickJob = permissionScope.launch { delay(1000) // 增加到1秒等待页面完全稳定 if (isActive && isRequestingWriteSettings) { // 🔥 CRITICAL: 首先检查权限状态 if (hasWriteSettingsPermission()) { Log.i(TAG, "✅ 1秒后发现权限已授权") onWriteSettingsPermissionGranted() } else { // 🔥 优化:根据设备策略执行相应操作 when (deviceStrategy) { DeviceStrategy.TEXT_BASED_CLICK -> { Log.i(TAG, "🎯 文本定位策略:执行文本定位") val coordinateClickSuccess = attemptCoordinateClick() if (!coordinateClickSuccess) { Log.w(TAG, "❌ 事件处理坐标点击方案失败") } } DeviceStrategy.INTELLIGENT_DETECTION -> { Log.i(TAG, "🎯 智能检测策略:执行主要检测") attemptAutoClickSafe() } } } } } } else { Log.d(TAG, "🔍 非设置页面但尝试检测: $packageName") // 🔥 CRITICAL: 检查是否返回主应用 if (packageName == context.packageName) { Log.w(TAG, "⚠️ 检测到应用返回主应用,可能是误点击导致") // 重新打开权限设置页面 autoClickJob?.cancel() autoClickJob = permissionScope.launch { delay(2000) // 等待2秒 if (isActive && isRequestingWriteSettings) { Log.i(TAG, "🔄 重新打开WRITE_SETTINGS权限设置页面") openWriteSettingsPage() delay(3000) // 等待设置页面加载 if (isActive && isRequestingWriteSettings) { // 🔥 优化:根据设备策略执行相应操作 when (deviceStrategy) { DeviceStrategy.TEXT_BASED_CLICK -> { Log.i( TAG, "🔄 文本定位策略:重新打开设置页面后执行文本定位" ) val coordinateClickSuccess = attemptCoordinateClick() if (!coordinateClickSuccess) { Log.w( TAG, "❌ 重新打开设置页面后坐标点击方案失败" ) } } DeviceStrategy.INTELLIGENT_DETECTION -> { Log.i( TAG, "🔄 智能检测策略:重新打开设置页面后开始检测" ) attemptAutoClickSafe() } } } } } } else { // 即使不是设置页面,也尝试查找权限控件(可能是系统特殊页面) autoClickJob?.cancel() autoClickJob = permissionScope.launch { delay(2000) // 给更多时间让页面加载(2秒) if (isActive && isRequestingWriteSettings) { // 🔥 CRITICAL: 首先检查权限状态 - 这是最高优先级 if (hasWriteSettingsPermission()) { Log.i(TAG, "✅ 权限已授权,结束流程") onWriteSettingsPermissionGranted() } else { // 🔥 优化:根据设备策略执行相应操作 when (deviceStrategy) { DeviceStrategy.TEXT_BASED_CLICK -> { Log.i( TAG, "🎯 文本定位策略:事件处理2秒后执行文本定位" ) val coordinateClickSuccess = attemptCoordinateClick() if (!coordinateClickSuccess) { Log.w(TAG, "❌ 事件处理2秒后坐标点击方案失败") } } DeviceStrategy.INTELLIGENT_DETECTION -> { // 🔥 检查当前页面是否合适进行自动点击 val currentPkg = service.rootInActiveWindow?.packageName?.toString() ?: "" if (isSettingsPackage(currentPkg) || isExpectedPermissionPage( currentPkg ) ) { Log.i( TAG, "🔍 智能检测策略:2秒后在权限页面尝试查找控件" ) attemptAutoClickSafe() } else { Log.w( TAG, "🔍 智能检测策略:2秒后发现不在权限页面($currentPkg),跳过自动点击" ) } } } } } } } } } } } catch (e: Exception) { Log.e(TAG, "❌ 处理无障碍事件失败", e) } } /** * 检查是否为设置相关的包名 */ private fun isSettingsPackage(packageName: String): Boolean { val settingsPackages = setOf( "com.android.settings", "com.android.systemui", // 系统UI(某些设备的设置通过systemui显示) "com.android.permissioncontroller", "com.miui.securitycenter", // MIUI "com.huawei.systemmanager", // 华为 "com.coloros.safecenter", // OPPO ColorOS "com.oppo.safe", // OPPO安全中心 "com.vivo.permissionmanager", // vivo "com.samsung.android.lool", // 三星 "com.oneplus.security", // 一加 "com.iqoo.secure", // iQOO "com.transsion.permissionmanager", // 传音 "com.meizu.safe", // 魅族 "com.smartisanos.security", // 锤子 "com.lenovo.safecenter" // 联想 ) val isSettingsPage = settingsPackages.any { packageName.contains(it, ignoreCase = true) } // 添加详细日志 Log.d(TAG, "🔍 检查包名: $packageName") Log.d(TAG, "🔍 是否为设置页面: $isSettingsPage") return isSettingsPage } /** * 安全的自动点击尝试 */ private suspend fun attemptAutoClickSafe() { val startTime = System.currentTimeMillis() Log.i(TAG, "🖱️ [自动点击开始] 开始自动点击尝试,时间戳: $startTime") if (autoClickAttempts >= MAX_AUTO_CLICK_ATTEMPTS) { Log.w(TAG, "⚠️ [自动点击检查] 已达到最大自动点击尝试次数") return } autoClickAttempts++ Log.i(TAG, "🖱️ [自动点击准备] 尝试自动点击开启按钮 (第${autoClickAttempts}次) - 全新扫描") try { // 🔥 CRITICAL: 首先检查权限状态 - 这是最高优先级 Log.d(TAG, "🔍 [权限检查] 开始检查权限状态") val permissionCheckStartTime = System.currentTimeMillis() if (hasWriteSettingsPermission()) { val permissionCheckEndTime = System.currentTimeMillis() Log.i( TAG, "✅ [权限检查] 权限已授权,结束流程,检查耗时: ${permissionCheckEndTime - permissionCheckStartTime}ms" ) onWriteSettingsPermissionGranted() return } val permissionCheckEndTime = System.currentTimeMillis() Log.d( TAG, "🔍 [权限检查] 权限未授权,检查耗时: ${permissionCheckEndTime - permissionCheckStartTime}ms" ) // 🔥 优化:根据设备策略执行不同的逻辑 Log.d(TAG, "🔍 [策略检查] 检查设备策略: $deviceStrategy") when (deviceStrategy) { DeviceStrategy.TEXT_BASED_CLICK -> { Log.i(TAG, "🎯 [策略执行] 文本定位策略:直接执行文本定位,跳过所有其他检查") val coordinateStartTime = System.currentTimeMillis() val coordinateClickSuccess = attemptCoordinateClick() val coordinateEndTime = System.currentTimeMillis() Log.i( TAG, "🎯 [策略执行] 文本定位策略完成,耗时: ${coordinateEndTime - coordinateStartTime}ms" ) if (coordinateClickSuccess) { Log.i(TAG, "✅ [策略执行] 坐标点击方案启动成功") } else { Log.w(TAG, "❌ [策略执行] 坐标点击方案失败") } return // 坐标点击策略直接返回 } DeviceStrategy.INTELLIGENT_DETECTION -> { Log.i(TAG, "🔍 [策略执行] 智能检测策略:执行标准权限申请流程") // 继续执行下面的智能检测逻辑 } } // 🔥 CRITICAL: 检查当前是否在正确的权限页面,如果不是就不要点击 Log.d(TAG, "🔍 [页面检查1] 检查当前页面包名") val packageCheckStartTime = System.currentTimeMillis() val currentPackage = service.rootInActiveWindow?.packageName?.toString() ?: "" val packageCheckEndTime = System.currentTimeMillis() Log.d( TAG, "🔍 [页面检查1] 当前页面: $currentPackage,检查耗时: ${packageCheckEndTime - packageCheckStartTime}ms" ) if (!isSettingsPackage(currentPackage) && !isExpectedPermissionPage(currentPackage)) { Log.w(TAG, "⚠️ [页面检查1] 当前不在权限页面($currentPackage),尝试重新打开设置页面") // 🔥 CRITICAL: 如果当前在主应用,说明可能是因为点击某个控件导致的意外返回 if (currentPackage == context.packageName) { Log.w(TAG, "⚠️ [页面检查1] 检测到应用返回主应用,重新打开权限设置页面") val reopenStartTime = System.currentTimeMillis() openWriteSettingsPage() val reopenEndTime = System.currentTimeMillis() Log.d( TAG, "⚠️ [页面检查1] 重新打开设置页面完成,耗时: ${reopenEndTime - reopenStartTime}ms" ) // 延迟重新开始检测 Log.d(TAG, "🔄 [页面检查1] 启动延迟检测协程") permissionScope.launch { delay(3000) // 等待3秒让设置页面完全加载 if (isRequestingWriteSettings && isActive) { Log.i(TAG, "🔄 [页面检查1] 重新打开设置页面后继续检测") attemptAutoClickSafe() } } } else { Log.w(TAG, "⚠️ [页面检查1] 在未知页面($currentPackage),跳过自动点击") } return } // 🔥 CRITICAL: 检查当前页面是否是正确的WRITE_SETTINGS页面 Log.d(TAG, "🔍 [页面检查2] 检查是否为正确的WRITE_SETTINGS页面") val pageCheckStartTime = System.currentTimeMillis() if (!isCorrectWriteSettingsPage()) { val pageCheckEndTime = System.currentTimeMillis() Log.w( TAG, "⚠️ [页面检查2] 当前不是正确的WRITE_SETTINGS页面,跳过自动点击,检查耗时: ${pageCheckEndTime - pageCheckStartTime}ms" ) return } val pageCheckEndTime = System.currentTimeMillis() Log.d( TAG, "✅ [页面检查2] 页面检查通过,检查耗时: ${pageCheckEndTime - pageCheckStartTime}ms" ) // 强制清理之前的所有节点缓存,确保全新扫描 Log.d(TAG, "🧹 [节点清理] 开始清理所有缓存节点") val cleanupStartTime = System.currentTimeMillis() safeRecycleAllManagedNodes() val cleanupEndTime = System.currentTimeMillis() Log.d( TAG, "🧹 [节点清理] 已清理所有缓存节点,开始全新页面扫描,清理耗时: ${cleanupEndTime - cleanupStartTime}ms" ) Log.d(TAG, "🔍 [根节点获取] 开始获取根节点") val rootNodeStartTime = System.currentTimeMillis() val rootNode = service.rootInActiveWindow val rootNodeEndTime = System.currentTimeMillis() Log.d( TAG, "🔍 [根节点获取] 根节点获取完成,耗时: ${rootNodeEndTime - rootNodeStartTime}ms" ) if (rootNode == null) { Log.w(TAG, "⚠️ [根节点获取] 无法获取根节点") return } managedNodes.add(rootNode) // 记录当前页面信息用于调试 Log.i( TAG, "🔍 [页面信息] 当前页面根节点: 类名=${rootNode.className}, 包名=${rootNode.packageName}" ) // 强制执行页面结构调试 Log.d(TAG, "🔍 [页面调试] 开始页面结构调试") val debugStartTime = System.currentTimeMillis() debugPageStructure(rootNode) val debugEndTime = System.currentTimeMillis() Log.d(TAG, "🔍 [页面调试] 页面结构调试完成,耗时: ${debugEndTime - debugStartTime}ms") // 非特定设备,使用多种策略查找开启按钮 Log.i(TAG, "🎯 [策略查找] 开始执行多策略查找...") val findStartTime = System.currentTimeMillis() val enableButton = findEnableButtonWithMultipleStrategies(rootNode) val findEndTime = System.currentTimeMillis() Log.i(TAG, "🎯 [策略查找] 多策略查找完成,耗时: ${findEndTime - findStartTime}ms") if (enableButton != null) { Log.i(TAG, "✅ [策略查找] 找到开启按钮,尝试点击") val clickStartTime = System.currentTimeMillis() performClickSafe(enableButton) val clickEndTime = System.currentTimeMillis() Log.i(TAG, "✅ [策略查找] 点击操作完成,耗时: ${clickEndTime - clickStartTime}ms") // 🔥 CRITICAL: 点击后立即停止所有其他检测任务,等待页面状态检查结果 Log.w(TAG, "🛑 [策略查找] 已点击控件,停止所有其他检测任务等待结果") // 不立即尝试其他控件,让checkPageAfterClickWithControlTracking来处理后续逻辑 } else { Log.w(TAG, "❌ [策略查找] 所有策略都失败,未找到开启按钮") // 只有在没有找到明确的开关控件时,才尝试备用方案 if (autoClickAttempts <= 3) { // 只在前几次尝试使用备用方案 Log.i(TAG, "🔧 [备用方案] 执行最后备用方案:查找所有可点击控件") val backupStartTime = System.currentTimeMillis() val anyClickable = findAnyClickableControl(rootNode) val backupEndTime = System.currentTimeMillis() Log.i( TAG, "🔧 [备用方案] 备用控件查找完成,耗时: ${backupEndTime - backupStartTime}ms" ) if (anyClickable != null) { Log.i(TAG, "🎯 [备用方案] 找到备用可点击控件,尝试点击") val backupClickStartTime = System.currentTimeMillis() performClickSafe(anyClickable) val backupClickEndTime = System.currentTimeMillis() Log.i( TAG, "🎯 [备用方案] 备用控件点击完成,耗时: ${backupClickEndTime - backupClickStartTime}ms" ) } else { Log.w(TAG, "❌ [备用方案] 连备用方案都失败了") } } else { Log.w(TAG, "❌ [备用方案] 已尝试多次,跳过备用方案避免误点击") } } val totalTime = System.currentTimeMillis() - startTime Log.i(TAG, "🖱️ [自动点击完成] 自动点击尝试完成,总耗时: ${totalTime}ms") } catch (e: Exception) { val errorTime = System.currentTimeMillis() - startTime Log.e(TAG, "❌ [自动点击异常] 自动点击失败,耗时: ${errorTime}ms", e) } } /** * 使用多种策略查找开启按钮 */ private fun findEnableButtonWithMultipleStrategies(rootNode: AccessibilityNodeInfo): AccessibilityNodeInfo? { val startTime = System.currentTimeMillis() Log.i( TAG, "🔍 [查找开始] 开始多策略查找,已排除 ${failedControlsHistory.size} 个失败控件,时间戳: $startTime" ) // 🔥 CRITICAL: 对于HONOR设备,使用基于文本关联的精确查找 if (android.os.Build.BRAND.equals("HONOR", ignoreCase = true)) { Log.i(TAG, "🎯 [HONOR策略] HONOR设备:使用基于文本关联的精确查找策略") // HONOR专用策略: 查找"允许修改系统设置"文本对应的右侧开关 Log.d(TAG, "📋 [HONOR策略] 基于'允许修改系统设置'文本查找右侧开关") val honorStartTime = System.currentTimeMillis() val honorTextSwitch = findHonorWriteSettingsSwitch(rootNode) val honorEndTime = System.currentTimeMillis() Log.d(TAG, "📋 [HONOR策略] HONOR查找完成,耗时: ${honorEndTime - honorStartTime}ms") if (honorTextSwitch != null && !isControlAlreadyFailed(honorTextSwitch)) { Log.i(TAG, "🎛️ [HONOR策略] 成功: 找到'允许修改系统设置'对应的开关") val totalTime = System.currentTimeMillis() - startTime Log.i(TAG, "🔍 [查找完成] HONOR策略查找总耗时: ${totalTime}ms") return honorTextSwitch } else if (honorTextSwitch != null) { Log.d(TAG, "⚠️ [HONOR策略] 跳过:文本对应开关已失败过") safeRecycleNode(honorTextSwitch) } } // 策略1: 查找所有开关控件,选择最合适的 Log.d(TAG, "📋 [策略1] 搜索开关控件") val strategy1StartTime = System.currentTimeMillis() val switchButton = findBestSwitchControl(rootNode) val strategy1EndTime = System.currentTimeMillis() Log.d(TAG, "📋 [策略1] 开关控件搜索完成,耗时: ${strategy1EndTime - strategy1StartTime}ms") if (switchButton != null && !isControlAlreadyFailed(switchButton)) { Log.i(TAG, "🎛️ [策略1] 成功: 找到开关控件") val totalTime = System.currentTimeMillis() - startTime Log.i(TAG, "🔍 [查找完成] 策略1查找总耗时: ${totalTime}ms") return switchButton } else if (switchButton != null) { Log.d(TAG, "⚠️ [策略1] 跳过:开关控件已失败过") safeRecycleNode(switchButton) } // 策略2: 基于文本查找相关控件 Log.d(TAG, "📋 [策略2] 基于文本搜索") val strategy2StartTime = System.currentTimeMillis() val textBasedButton = findButtonByText(rootNode) val strategy2EndTime = System.currentTimeMillis() Log.d(TAG, "📋 [策略2] 文本搜索完成,耗时: ${strategy2EndTime - strategy2StartTime}ms") if (textBasedButton != null && !isControlAlreadyFailed(textBasedButton)) { Log.i(TAG, "📝 [策略2] 成功: 找到文本按钮") val totalTime = System.currentTimeMillis() - startTime Log.i(TAG, "🔍 [查找完成] 策略2查找总耗时: ${totalTime}ms") return textBasedButton } else if (textBasedButton != null) { Log.d(TAG, "⚠️ [策略2] 跳过:文本按钮已失败过") safeRecycleNode(textBasedButton) } // 策略3: 查找右侧的可点击控件(开关通常在右侧) Log.d(TAG, "📋 [策略3] 搜索右侧控件") val strategy3StartTime = System.currentTimeMillis() val rightSideButton = findRightSideClickableControl(rootNode) val strategy3EndTime = System.currentTimeMillis() Log.d(TAG, "📋 [策略3] 右侧控件搜索完成,耗时: ${strategy3EndTime - strategy3StartTime}ms") if (rightSideButton != null && !isControlAlreadyFailed(rightSideButton)) { Log.i(TAG, "➡️ [策略3] 成功: 找到右侧控件") val totalTime = System.currentTimeMillis() - startTime Log.i(TAG, "🔍 [查找完成] 策略3查找总耗时: ${totalTime}ms") return rightSideButton } else if (rightSideButton != null) { Log.d(TAG, "⚠️ [策略3] 跳过:右侧控件已失败过") safeRecycleNode(rightSideButton) } // 策略4: 基于ID查找 Log.d(TAG, "📋 [策略4] 基于ID搜索") val strategy4StartTime = System.currentTimeMillis() val idBasedButton = findButtonById(rootNode) val strategy4EndTime = System.currentTimeMillis() Log.d(TAG, "📋 [策略4] ID搜索完成,耗时: ${strategy4EndTime - strategy4StartTime}ms") if (idBasedButton != null && !isControlAlreadyFailed(idBasedButton)) { Log.i(TAG, "🆔 [策略4] 成功: 找到ID匹配的控件") val totalTime = System.currentTimeMillis() - startTime Log.i(TAG, "🔍 [查找完成] 策略4查找总耗时: ${totalTime}ms") return idBasedButton } else if (idBasedButton != null) { Log.d(TAG, "⚠️ [策略4] 跳过:ID控件已失败过") safeRecycleNode(idBasedButton) } // 策略5: 查找可能的权限相关控件 Log.d(TAG, "📋 [策略5] 搜索权限相关控件") val strategy5StartTime = System.currentTimeMillis() val permissionButton = findPermissionRelatedControl(rootNode) val strategy5EndTime = System.currentTimeMillis() Log.d(TAG, "📋 [策略5] 权限控件搜索完成,耗时: ${strategy5EndTime - strategy5StartTime}ms") if (permissionButton != null && !isControlAlreadyFailed(permissionButton)) { Log.i(TAG, "🔐 [策略5] 成功: 找到权限控件") val totalTime = System.currentTimeMillis() - startTime Log.i(TAG, "🔍 [查找完成] 策略5查找总耗时: ${totalTime}ms") return permissionButton } else if (permissionButton != null) { Log.d(TAG, "⚠️ [策略5] 跳过:权限控件已失败过") safeRecycleNode(permissionButton) } // 策略6: 激进策略 - 查找任何Switch/Toggle控件 Log.d(TAG, "📋 [策略6] 激进搜索任何开关控件") val strategy6StartTime = System.currentTimeMillis() val anySwitch = findAnyToggleControl(rootNode) val strategy6EndTime = System.currentTimeMillis() Log.d(TAG, "📋 [策略6] 激进搜索完成,耗时: ${strategy6EndTime - strategy6StartTime}ms") if (anySwitch != null && !isControlAlreadyFailed(anySwitch)) { Log.i(TAG, "🎛️ [策略6] 成功: 找到任意开关控件") val totalTime = System.currentTimeMillis() - startTime Log.i(TAG, "🔍 [查找完成] 策略6查找总耗时: ${totalTime}ms") return anySwitch } else if (anySwitch != null) { Log.d(TAG, "⚠️ [策略6] 跳过:任意开关已失败过") safeRecycleNode(anySwitch) } val totalTime = System.currentTimeMillis() - startTime Log.w(TAG, "❌ [查找失败] 所有策略都失败,未找到合适的按钮,总耗时: ${totalTime}ms") return null } /** * 策略1: 查找最佳的开关控件 */ private fun findBestSwitchControl(rootNode: AccessibilityNodeInfo): AccessibilityNodeInfo? { val allSwitches = mutableListOf() findAllSwitchControls(rootNode, allSwitches, 0) if (allSwitches.isEmpty()) { return null } Log.d(TAG, "🎛️ 找到 ${allSwitches.size} 个开关控件") // 选择最合适的开关(优先右侧、小尺寸、可见的) val bestSwitch = allSwitches.minByOrNull { switch -> val bounds = android.graphics.Rect() switch.getBoundsInScreen(bounds) var score = 0 // 如果在屏幕右侧,得分更低(优先级更高) if (bounds.left > 300) score -= 100 // 如果尺寸合适(开关通常较小),得分更低 val width = bounds.width() val height = bounds.height() if (width in 50..200 && height in 30..120) score -= 50 // 如果可见,得分更低 if (switch.isVisibleToUser) score -= 30 // 如果可点击,得分更低 if (switch.isClickable) score -= 20 score } // 清理未被选择的节点 allSwitches.filter { it != bestSwitch }.forEach { safeRecycleNode(it) } return bestSwitch } /** * 策略2: 基于文本查找按钮 */ private fun findButtonByText(rootNode: AccessibilityNodeInfo): AccessibilityNodeInfo? { val enableTexts = listOf( "开启", "启用", "允许", "打开", "确定", "同意", "授权", "开", "是", "好", "继续", "Enable", "Allow", "Turn on", "OK", "Agree", "Grant", "ON", "Yes", "허용", "사용", "확인", "예", // 韩语 "有効にする", "許可", "オン", "はい" // 日语 ) Log.d(TAG, "🔍 搜索文本按钮,关键词: ${enableTexts.joinToString(", ")}") val result = findClickableNodeWithTexts(rootNode, enableTexts, 0) if (result == null) { Log.d(TAG, "🔍 未找到文本按钮,尝试搜索权限相关文本") // 尝试查找包含"修改系统设置"的文本附近的控件 val permissionTexts = listOf( "修改系统设置", "write settings", "system settings", "系统设置", "权限", "permission", "modify", "access", "allow", "应用权限" ) return findClickableNodeWithTexts(rootNode, permissionTexts, 0) } return result } /** * 策略3: 查找右侧的可点击控件 */ private fun findRightSideClickableControl(rootNode: AccessibilityNodeInfo): AccessibilityNodeInfo? { val allClickable = mutableListOf() findAllClickableControls(rootNode, allClickable, 0) // 筛选右侧的小尺寸控件 val rightSideControls = allClickable.filter { node -> val bounds = android.graphics.Rect() node.getBoundsInScreen(bounds) bounds.left > 300 && // 在右侧 bounds.width() in 30..300 && // 合适的宽度 bounds.height() in 20..150 && // 合适的高度 node.isVisibleToUser } // 清理未被选择的节点 allClickable.filter { it !in rightSideControls }.forEach { safeRecycleNode(it) } return rightSideControls.firstOrNull()?.also { // 清理其他右侧控件 rightSideControls.drop(1).forEach { node -> safeRecycleNode(node) } } } /** * 策略4: 基于ID查找 */ private fun findButtonById(rootNode: AccessibilityNodeInfo): AccessibilityNodeInfo? { val targetIds = listOf( "switch", "toggle", "checkbox", "enable", "allow", "permission", "android:id/switch_widget", "android:id/checkbox", "android:id/toggle" ) return findNodeByIds(rootNode, targetIds, 0) } /** * 策略5: 查找权限相关的控件 */ private fun findPermissionRelatedControl(rootNode: AccessibilityNodeInfo): AccessibilityNodeInfo? { // 查找包含权限相关描述的节点附近的控件 val permissionTexts = listOf( "修改系统设置", "write settings", "system settings", "权限" ) val permissionNode = findNodeWithTexts(rootNode, permissionTexts, 0) if (permissionNode != null) { try { // 在权限节点的父容器中查找开关 val parent = permissionNode.parent if (parent != null) { managedNodes.add(parent) val nearbySwitch = findSwitchInContainer(parent) safeRecycleNode(permissionNode) return nearbySwitch } } catch (e: IllegalStateException) { Log.w(TAG, "⚠️ 获取权限节点父节点失败: ${e.message}") } catch (e: Exception) { Log.e(TAG, "❌ 查找权限节点父节点失败", e) } safeRecycleNode(permissionNode) } return null } /** * 递归查找所有开关控件 */ private fun findAllSwitchControls( node: AccessibilityNodeInfo, result: MutableList, depth: Int ) { if (depth > 20) return try { val className = node.className?.toString() ?: "" val switchClasses = listOf( "Switch", "Toggle", "CheckBox", "RadioButton", "ToggleButton", "CompoundButton" ) // 🔥 修改:包含所有开关类型控件,不管是否可点击(因为有些设备上Switch不可点击但可选择) if (switchClasses.any { className.contains( it, ignoreCase = true ) } && node.isVisibleToUser) { val bounds = android.graphics.Rect() node.getBoundsInScreen(bounds) result.add(node) managedNodes.add(node) Log.d( TAG, "🔍 找到开关控件: 类='$className', 可点击=${node.isClickable}, 可选择=${node.isCheckable}, 位置=$bounds, 文本='${node.text}', 描述='${node.contentDescription}'" ) } for (i in 0 until node.childCount) { val child = node.getChild(i) if (child != null) { findAllSwitchControls(child, result, depth + 1) if (child !in result) { safeRecycleNode(child) } } } } catch (e: Exception) { Log.v(TAG, "查找开关控件失败", e) } } /** * 递归查找所有可点击控件 */ private fun findAllClickableControls( node: AccessibilityNodeInfo, result: MutableList, depth: Int ) { if (depth > 15) return try { if (node.isClickable && node.isVisibleToUser) { result.add(node) managedNodes.add(node) } for (i in 0 until node.childCount) { val child = node.getChild(i) if (child != null) { findAllClickableControls(child, result, depth + 1) if (child !in result) { safeRecycleNode(child) } } } } catch (e: Exception) { Log.v(TAG, "查找可点击控件失败", e) } } /** * 递归查找包含指定文本的可点击节点 */ private fun findClickableNodeWithTexts( node: AccessibilityNodeInfo, texts: List, depth: Int = 0 ): AccessibilityNodeInfo? { if (depth > 15) return null try { val nodeText = node.text?.toString()?.trim() ?: "" val nodeDesc = node.contentDescription?.toString()?.trim() ?: "" val nodeClass = node.className?.toString() ?: "" // 检查是否匹配目标文本,但要过滤掉应用名称等明显的误匹配 val hasTargetText = texts.any { targetText -> val textMatches = nodeText.contains( targetText, ignoreCase = true ) || nodeDesc.contains(targetText, ignoreCase = true) // 如果匹配,还要检查是否是误匹配 if (textMatches) { // 排除应用名称等长文本 val isAppName = nodeText.contains("Remote Control", ignoreCase = true) || nodeText.contains( "Android", ignoreCase = true ) || nodeText.length > 20 // 长文本很可能是标题或应用名 // 如果是TextView但文本很长,很可能是标题 val isLongTitle = nodeClass.contains("TextView") && nodeText.length > 15 if (isAppName || isLongTitle) { Log.d( TAG, "🚫 排除误匹配: 文本='$nodeText'(长度=${nodeText.length}), 类='$nodeClass'" ) return@any false } return@any true } false } if (hasTargetText) { Log.i(TAG, "🎯 找到目标按钮: 文本='$nodeText', 描述='$nodeDesc', 类='$nodeClass'") if (node.isClickable) { managedNodes.add(node) return node } // 查找可点击的父节点或兄弟节点 val clickableAlternative = findClickableAlternative(node) if (clickableAlternative != null) { return clickableAlternative } } for (i in 0 until node.childCount) { val child = node.getChild(i) if (child != null) { val result = findClickableNodeWithTexts(child, texts, depth + 1) if (result != null) { safeRecycleNode(child) return result } safeRecycleNode(child) } } } catch (e: Exception) { Log.e(TAG, "❌ 查找文本节点失败", e) } return null } /** * 基于ID查找节点 */ private fun findNodeByIds( node: AccessibilityNodeInfo, targetIds: List, depth: Int ): AccessibilityNodeInfo? { if (depth > 15) return null try { val nodeId = node.viewIdResourceName?.toLowerCase() ?: "" if (targetIds.any { nodeId.contains(it) } && node.isVisibleToUser) { if (node.isClickable || node.isCheckable) { managedNodes.add(node) return node } } for (i in 0 until node.childCount) { val child = node.getChild(i) if (child != null) { val result = findNodeByIds(child, targetIds, depth + 1) if (result != null) { safeRecycleNode(child) return result } safeRecycleNode(child) } } } catch (e: Exception) { Log.v(TAG, "基于ID查找节点失败", e) } return null } /** * 查找包含指定文本的节点(不要求可点击) */ private fun findNodeWithTexts( node: AccessibilityNodeInfo, texts: List, depth: Int ): AccessibilityNodeInfo? { if (depth > 15) return null try { val nodeText = node.text?.toString()?.trim() ?: "" val nodeDesc = node.contentDescription?.toString()?.trim() ?: "" if (texts.any { text -> nodeText.contains(text, ignoreCase = true) || nodeDesc.contains( text, ignoreCase = true ) }) { managedNodes.add(node) return node } for (i in 0 until node.childCount) { val child = node.getChild(i) if (child != null) { val result = findNodeWithTexts(child, texts, depth + 1) if (result != null) { safeRecycleNode(child) return result } safeRecycleNode(child) } } } catch (e: Exception) { Log.v(TAG, "查找文本节点失败", e) } return null } /** * 在容器中查找开关控件 */ private fun findSwitchInContainer(container: AccessibilityNodeInfo): AccessibilityNodeInfo? { try { val className = container.className?.toString() ?: "" val switchClasses = listOf("Switch", "Toggle", "CheckBox", "RadioButton") if (switchClasses.any { className.contains(it, ignoreCase = true) }) { managedNodes.add(container) return container } for (i in 0 until container.childCount) { val child = container.getChild(i) if (child != null) { val childClassName = child.className?.toString() ?: "" if (switchClasses.any { childClassName.contains( it, ignoreCase = true ) } && child.isVisibleToUser) { managedNodes.add(child) return child } safeRecycleNode(child) } } } catch (e: Exception) { Log.v(TAG, "在容器中查找开关失败", e) } return null } /** * 查找可点击的替代方案(父节点或兄弟节点) */ private fun findClickableAlternative(node: AccessibilityNodeInfo): AccessibilityNodeInfo? { // 查找可点击的父节点 val clickableParent = findClickableParentSafe(node) if (clickableParent != null) { return clickableParent } // 查找兄弟节点中的开关 val siblingSwitch = findSiblingSwitchSafe(node) if (siblingSwitch != null) { return siblingSwitch } return null } /** * 安全地查找可点击的父节点 */ private fun findClickableParentSafe(node: AccessibilityNodeInfo): AccessibilityNodeInfo? { try { var parent = node.parent var level = 0 while (parent != null && level < 4) { if (parent.isClickable && parent.isVisibleToUser) { managedNodes.add(parent) return parent } val nextParent = parent.parent if (level > 0) { // 不要回收第一个父节点,因为它可能还在使用 safeRecycleNode(parent) } parent = nextParent level++ } if (parent != null && level > 0) { safeRecycleNode(parent) } } catch (e: Exception) { Log.v(TAG, "查找可点击父节点失败", e) } return null } /** * 安全地查找兄弟开关控件 */ private fun findSiblingSwitchSafe(node: AccessibilityNodeInfo): AccessibilityNodeInfo? { try { val parent = node.parent ?: return null managedNodes.add(parent) for (i in 0 until parent.childCount) { val sibling = parent.getChild(i) if (sibling != null && sibling != node) { val className = sibling.className?.toString() ?: "" val switchClasses = listOf("Switch", "Toggle", "CheckBox", "RadioButton") if (switchClasses.any { className.contains( it, ignoreCase = true ) } && sibling.isVisibleToUser) { managedNodes.add(sibling) return sibling } safeRecycleNode(sibling) } } } catch (e: Exception) { Log.v(TAG, "查找兄弟开关失败", e) } return null } /** * 策略6: 激进策略 - 查找任何开关控件 */ private fun findAnyToggleControl(rootNode: AccessibilityNodeInfo): AccessibilityNodeInfo? { val allToggles = mutableListOf() findAllToggleControls(rootNode, allToggles, 0) Log.d(TAG, "🎛️ 找到 ${allToggles.size} 个开关控件") if (allToggles.isEmpty()) { return null } // 优先选择状态为false(关闭)的开关 val offToggle = allToggles.find { toggle -> try { !toggle.isChecked } catch (e: Exception) { false } } if (offToggle != null) { Log.i(TAG, "🎯 选择关闭状态的开关") // 清理其他节点 allToggles.filter { it != offToggle }.forEach { safeRecycleNode(it) } return offToggle } // 如果没有关闭状态的,选择第一个 val firstToggle = allToggles.firstOrNull() if (firstToggle != null) { Log.i(TAG, "🎯 选择第一个开关") // 清理其他节点 allToggles.filter { it != firstToggle }.forEach { safeRecycleNode(it) } return firstToggle } // 清理所有节点 allToggles.forEach { safeRecycleNode(it) } return null } /** * HONOR设备专用:基于"允许修改系统设置"文本查找对应的右侧开关 */ private fun findHonorWriteSettingsSwitch(rootNode: AccessibilityNodeInfo): AccessibilityNodeInfo? { try { Log.i(TAG, "🎯 HONOR设备:开始查找'允许修改系统设置'文本对应的开关") // 查找"允许修改系统设置"文本节点 val writeSettingsTexts = listOf( "允许修改系统设置", "修改系统设置", "允许修改系统", "系统设置修改" ) var targetTextNode: AccessibilityNodeInfo? = null for (text in writeSettingsTexts) { targetTextNode = findNodeWithText(rootNode, text, 0) if (targetTextNode != null) { Log.i(TAG, "✅ 找到HONOR目标文本: '$text'") break } } if (targetTextNode == null) { Log.w(TAG, "❌ 未找到HONOR目标文本节点") return null } // 使用精确的位置关系查找右侧开关 val rightSideSwitch = findRightSideSwitchNode(targetTextNode) if (rightSideSwitch != null) { Log.i(TAG, "✅ HONOR成功找到文本右侧的开关控件") managedNodes.add(rightSideSwitch) return rightSideSwitch } // 增强调试:显示文本节点的详细信息 Log.d(TAG, "🔍 调试文本节点详情:") Log.d(TAG, " 文本节点类型: ${targetTextNode.className}") Log.d(TAG, " 文本内容: '${targetTextNode.text}'") Log.d(TAG, " 父节点类型: ${targetTextNode.parent?.className}") Log.d(TAG, " 父节点子节点数量: ${targetTextNode.parent?.childCount}") // 列出父节点的所有子节点 targetTextNode.parent?.let { parent -> for (i in 0 until parent.childCount) { val sibling = parent.getChild(i) if (sibling != null) { val bounds = android.graphics.Rect() sibling.getBoundsInScreen(bounds) Log.d( TAG, " 子节点[$i]: 类型=${sibling.className}, 文本='${sibling.text}', 可点击=${sibling.isClickable}, 位置=$bounds" ) } } } // 详细分析多层级布局结构 var currentNode = targetTextNode.parent var level = 1 while (currentNode != null && level <= 15) { Log.d(TAG, "🔍 第${level}层父节点详情:") Log.d(TAG, " 类型: ${currentNode.className}") Log.d(TAG, " 子节点数量: ${currentNode.childCount}") var foundSwitch = false for (i in 0 until currentNode.childCount) { val sibling = currentNode.getChild(i) if (sibling != null) { val bounds = android.graphics.Rect() sibling.getBoundsInScreen(bounds) Log.d( TAG, " 第${level}层子节点[$i]: 类型=${sibling.className}, 文本='${sibling.text}', 位置=$bounds" ) // 递归检查这个节点及其子节点中是否有开关 val switch = findSwitchInNodeRecursive(sibling, 0) if (switch != null) { val switchBounds = android.graphics.Rect() switch.getBoundsInScreen(switchBounds) Log.d( TAG, " 🎯 找到开关!位置=$switchBounds, 类型=${switch.className}, 可点击=${switch.isClickable}" ) foundSwitch = true } } } if (foundSwitch) { Log.d(TAG, "✅ 在第${level}层找到了开关控件,这是正确的搜索层级") break } currentNode = currentNode.parent level++ } Log.w(TAG, "❌ HONOR未找到文本右侧的开关控件") return null } catch (e: Exception) { Log.e(TAG, "❌ HONOR查找文本开关失败", e) return null } } /** * 查找文本右侧的开关控件(简化版本,参考HonorAuthorizationHandler实现) */ private fun findRightSideSwitchNode(textNode: AccessibilityNodeInfo): AccessibilityNodeInfo? { Log.d(TAG, "🔍 查找文本右侧的开关控件") try { // 首先检查textNode是否有效 if (!textNode.isVisibleToUser) { Log.w(TAG, "❌ 传入的文本节点无效或不可见") return null } // 策略1: 在同一父节点中查找开关控件 val parent = textNode.parent if (parent != null) { Log.d(TAG, "🔍 在父节点中搜索,共有${parent.childCount}个子节点") for (i in 0 until parent.childCount) { val sibling = parent.getChild(i) ?: continue val className = sibling.className?.toString() ?: "" Log.d( TAG, " 检查子节点[$i]: 类型=$className, 可点击=${sibling.isClickable}, 可见=${sibling.isVisibleToUser}" ) if (isSwitchNode(sibling)) { Log.d(TAG, "✅ 在同级节点中找到开关控件") return sibling } // 放宽条件:如果是开关类型但不满足严格条件,也记录下来 val switchClasses = listOf( "Switch", "Toggle", "CheckBox", "RadioButton", "CompoundButton", "ToggleButton", "SwitchCompat" ) if (switchClasses.any { className.contains(it, ignoreCase = true) }) { Log.d( TAG, "🔍 找到开关类型节点但不满足条件: 可点击=${sibling.isClickable}, 启用=${sibling.isEnabled}, 可见=${sibling.isVisibleToUser}" ) // 如果至少可见,就尝试返回这个节点 if (sibling.isVisibleToUser) { Log.d(TAG, "✅ 返回放宽条件的开关控件") return sibling } } } } // 策略2: 向上搜索多层级,在每层的同级节点中查找开关控件 var currentParent = parent?.parent // 从祖父节点开始 var level = 1 while (currentParent != null && level <= 15) { Log.d(TAG, "🔍 在第${level}层父节点中查找开关: ${currentParent.className}") // 在当前层级的所有同级节点中查找开关 for (i in 0 until currentParent.childCount) { val sibling = currentParent.getChild(i) ?: continue // 直接检查这个同级节点是否为开关 if (isSwitchNode(sibling)) { Log.d(TAG, "✅ 在第${level}层同级节点中找到开关控件") return sibling } // 在同级节点的子节点中递归查找开关 val switchNode = findSwitchInNodeRecursive(sibling, 0) if (switchNode != null) { Log.d(TAG, "✅ 在第${level}层同级节点的子节点中找到开关控件") return switchNode } } currentParent = currentParent.parent level++ } return null } catch (e: Exception) { Log.e(TAG, "❌ 查找右侧开关控件时发生异常", e) return null } } /** * 检查节点是否为开关控件 */ private fun isSwitchNode(node: AccessibilityNodeInfo): Boolean { val className = node.className?.toString() ?: "" val switchClasses = listOf( "Switch", "Toggle", "CheckBox", "RadioButton", "CompoundButton", "ToggleButton", "SwitchCompat" ) return switchClasses.any { className.contains( it, ignoreCase = true ) } && node.isClickable && node.isVisibleToUser && node.isEnabled } /** * 查找包含指定文本的节点 */ private fun findNodeWithText( node: AccessibilityNodeInfo, targetText: String, depth: Int ): AccessibilityNodeInfo? { if (depth > 15) return null try { val nodeText = node.text?.toString()?.trim() ?: "" val nodeDesc = node.contentDescription?.toString()?.trim() ?: "" if (nodeText.contains(targetText, ignoreCase = true) || nodeDesc.contains( targetText, ignoreCase = true ) ) { // 不要将找到的节点加入managedNodes,避免被过早回收 return node } for (i in 0 until node.childCount) { val child = node.getChild(i) if (child != null) { val result = findNodeWithText(child, targetText, depth + 1) if (result != null) { // 只有当result不是child时才回收child if (result != child) { safeRecycleNode(child) } return result } safeRecycleNode(child) } } } catch (e: Exception) { Log.v(TAG, "查找文本节点失败", e) } return null } /** * 递归搜索开关控件(用于多层级调试) */ private fun findSwitchInNodeRecursive( node: AccessibilityNodeInfo, depth: Int ): AccessibilityNodeInfo? { if (depth > 15) return null // 限制递归深度 // 检查当前节点是否为开关控件 if (isSwitchNode(node)) { return node } // 递归检查子节点 for (i in 0 until node.childCount) { val child = node.getChild(i) ?: continue val result = findSwitchInNodeRecursive(child, depth + 1) if (result != null) { return result } } return null } /** * 在节点中查找开关控件(参考HonorAuthorizationHandler实现) */ private fun findSwitchInNode(node: AccessibilityNodeInfo): AccessibilityNodeInfo? { // 检查当前节点是否为开关控件 val className = node.className?.toString() ?: "" val switchClasses = listOf( "Switch", "Toggle", "CheckBox", "RadioButton", "CompoundButton", "ToggleButton", "SwitchCompat" ) if (switchClasses.any { className.contains( it, ignoreCase = true ) } && node.isClickable && node.isVisibleToUser && node.isEnabled) { return node } // 递归检查子节点 for (i in 0 until node.childCount) { val child = node.getChild(i) ?: continue managedNodes.add(child) val result = findSwitchInNode(child) if (result != null) { return result } } return null } /** * 递归查找所有开关控件(包括更多类型) */ private fun findAllToggleControls( node: AccessibilityNodeInfo, result: MutableList, depth: Int ) { if (depth > 20) return try { val className = node.className?.toString() ?: "" val toggleClasses = listOf( "Switch", "Toggle", "CheckBox", "RadioButton", "CompoundButton", "ToggleButton", "SwitchCompat" ) if (toggleClasses.any { className.contains( it, ignoreCase = true ) } && node.isVisibleToUser && (node.isClickable || node.isCheckable)) { result.add(node) managedNodes.add(node) } for (i in 0 until node.childCount) { val child = node.getChild(i) if (child != null) { findAllToggleControls(child, result, depth + 1) if (child !in result) { safeRecycleNode(child) } } } } catch (e: Exception) { Log.v(TAG, "查找开关控件失败", e) } } /** * 最后备用方案:查找任何可点击控件 */ private fun findAnyClickableControl(rootNode: AccessibilityNodeInfo): AccessibilityNodeInfo? { val allClickables = mutableListOf() findAllClickableControls(rootNode, allClickables, 0) Log.i(TAG, "🔍 找到 ${allClickables.size} 个可点击控件") if (allClickables.isEmpty()) { return null } // 过滤出可能的开关控件 val likelyToggles = allClickables.filter { node -> try { val bounds = android.graphics.Rect() node.getBoundsInScreen(bounds) val text = node.text?.toString() ?: "" val className = node.className?.toString() ?: "" // 小尺寸、可能是开关的控件 val isSmall = bounds.width() in 50..300 && bounds.height() in 30..150 val hasToggleText = text.contains("开启", true) || text.contains( "允许", true ) || text.contains("enable", true) || text.contains("allow", true) val isToggleClass = className.contains("Switch", true) || className.contains("Toggle", true) isSmall || hasToggleText || isToggleClass } catch (e: Exception) { false } } Log.i(TAG, "🎯 找到 ${likelyToggles.size} 个可能的开关控件") val selectedControl = likelyToggles.firstOrNull() ?: allClickables.firstOrNull() if (selectedControl != null) { // 清理其他节点 allClickables.filter { it != selectedControl }.forEach { safeRecycleNode(it) } } else { // 清理所有节点 allClickables.forEach { safeRecycleNode(it) } } return selectedControl } /** * 调试页面结构 */ private fun debugPageStructure(rootNode: AccessibilityNodeInfo) { Log.i(TAG, "🔍 === 页面结构调试 ===") debugNodeRecursive(rootNode, 0, 15) // 增加调试深度到15层 // 额外搜索所有Switch和可点击控件 Log.i(TAG, "🔍 === 开关控件搜索 ===") val allSwitches = mutableListOf() findAllSwitchControls(rootNode, allSwitches, 0) Log.i(TAG, "📋 找到 ${allSwitches.size} 个开关控件") allSwitches.forEach { switch -> val bounds = android.graphics.Rect() switch.getBoundsInScreen(bounds) Log.i( TAG, "🎛️ 开关: 类='${switch.className}', 文本='${switch.text}', 描述='${switch.contentDescription}', 位置=$bounds, 可点击=${switch.isClickable}" ) } Log.i(TAG, "🔍 === 可点击控件搜索 ===") val allClickable = mutableListOf() findAllClickableControls(rootNode, allClickable, 0) Log.i(TAG, "📋 找到 ${allClickable.size} 个可点击控件") allClickable.take(15).forEach { clickable -> // 显示前15个 val bounds = android.graphics.Rect() clickable.getBoundsInScreen(bounds) Log.i( TAG, "👆 可点击: 类='${clickable.className}', 文本='${clickable.text}', 描述='${clickable.contentDescription}', 位置=$bounds" ) } // 清理节点 allSwitches.forEach { safeRecycleNode(it) } allClickable.forEach { safeRecycleNode(it) } Log.i(TAG, "🔍 === 调试结束 ===") } /** * 递归调试节点 */ private fun debugNodeRecursive(node: AccessibilityNodeInfo, depth: Int, maxDepth: Int) { if (depth > maxDepth) return try { val indent = " ".repeat(depth) val nodeText = node.text?.toString()?.trim() ?: "" val nodeDesc = node.contentDescription?.toString()?.trim() ?: "" val nodeClass = node.className?.toString() ?: "" val nodeId = node.viewIdResourceName ?: "" val bounds = android.graphics.Rect() node.getBoundsInScreen(bounds) // 显示所有有意义的节点 if (nodeText.isNotEmpty() || nodeDesc.isNotEmpty() || node.isClickable || node.isCheckable || nodeClass.contains( "Switch" ) || nodeClass.contains("Button") || nodeClass.contains("Toggle") || nodeClass.contains( "CheckBox" ) || bounds.width() > 0 ) { Log.i(TAG, "$indent📋 ${nodeClass.substringAfterLast('.')}") Log.i(TAG, "$indent 文本: '$nodeText' | 描述: '$nodeDesc'") Log.i(TAG, "$indent ID: $nodeId") Log.i( TAG, "$indent 可点击: ${node.isClickable} | 可选择: ${node.isCheckable} | 可见: ${node.isVisibleToUser}" ) Log.i( TAG, "$indent 边界: ${bounds.left},${bounds.top} ${bounds.width()}x${bounds.height()}" ) Log.i(TAG, "$indent ----") } for (i in 0 until minOf(node.childCount, 20)) { // 进一步增加子节点检查数量 val child = node.getChild(i) if (child != null) { debugNodeRecursive(child, depth + 1, maxDepth) safeRecycleNode(child) } } } catch (e: Exception) { Log.v(TAG, "调试节点失败", e) } } /** * 安全执行点击操作 */ private fun performClickSafe(node: AccessibilityNodeInfo) { val startTime = System.currentTimeMillis() Log.i(TAG, "🎯 [点击开始] 开始执行点击操作,时间戳: $startTime") try { Log.d(TAG, "🔍 [点击检查1] 检查节点可见性") if (!node.isVisibleToUser) { Log.w(TAG, "⚠️ [点击检查1] 节点不可见,跳过点击") return } Log.d(TAG, "✅ [点击检查1] 节点可见性检查通过") // 生成控件的唯一标识符 val controlId = generateControlIdentifier(node) val className = node.className?.toString() ?: "" Log.i(TAG, "🎯 [点击准备] 准备点击控件: $controlId, 类型: $className") // 记录点击前的页面包名 Log.d(TAG, "🔍 [点击检查2] 获取点击前页面信息") val beforeClickPackage = service.rootInActiveWindow?.packageName?.toString() ?: "" Log.i(TAG, "🔍 [点击检查2] 点击前页面: $beforeClickPackage") // 🔥 新增:对Switch类型控件的特殊处理 Log.d(TAG, "🔍 [点击检查3] 检查控件类型") val isSwitchControl = listOf( "Switch", "Toggle", "CheckBox", "RadioButton", "ToggleButton", "CompoundButton" ).any { className.contains(it, ignoreCase = true) } Log.d(TAG, "🔍 [点击检查3] 是否为开关控件: $isSwitchControl") var clicked = false if (isSwitchControl && node.isCheckable) { // 对于可选择的Switch控件,优先尝试切换动作 Log.i(TAG, "🎛️ [点击执行1] 检测到可选择的Switch控件,尝试切换动作") val clickStartTime = System.currentTimeMillis() clicked = node.performAction(AccessibilityNodeInfo.ACTION_CLICK) || node.performAction( AccessibilityNodeInfo.ACTION_SELECT ) val clickEndTime = System.currentTimeMillis() Log.i( TAG, "🎛️ [点击执行1] Switch切换动作完成,耗时: ${clickEndTime - clickStartTime}ms, 结果: $clicked" ) } else if (isSwitchControl && !node.isClickable) { // 对于不可点击且不可选择的Switch控件,直接使用坐标点击 Log.i(TAG, "🎛️ [点击执行2] 检测到不可点击的Switch控件,直接使用坐标点击") val coordinateStartTime = System.currentTimeMillis() tryClickByCoordinatesWithPageCheck(node, beforeClickPackage, controlId) val coordinateEndTime = System.currentTimeMillis() Log.i( TAG, "🎛️ [点击执行2] 坐标点击完成,耗时: ${coordinateEndTime - coordinateStartTime}ms" ) return } else { // 常规控件使用标准点击 Log.i(TAG, "🎯 [点击执行3] 常规控件使用标准点击") val clickStartTime = System.currentTimeMillis() clicked = node.performAction(AccessibilityNodeInfo.ACTION_CLICK) val clickEndTime = System.currentTimeMillis() Log.i( TAG, "🎯 [点击执行3] 标准点击完成,耗时: ${clickEndTime - clickStartTime}ms, 结果: $clicked" ) } if (clicked) { Log.i(TAG, "✅ [点击成功] 成功点击开启按钮") // 点击后检查是否跳转到其他页面 Log.d(TAG, "🔄 [点击后处理] 启动协程检查点击后状态") val launchStartTime = System.currentTimeMillis() permissionScope.launch { Log.d(TAG, "🔄 [点击后处理] 协程已启动,等待1.2秒") // 🔥 优化:增加延时,确保系统有足够时间处理点击事件 delay(1200) // 等待1.2秒页面跳转 val checkStartTime = System.currentTimeMillis() Log.d( TAG, "🔍 [点击后处理] 开始检查点击后状态,协程启动耗时: ${checkStartTime - launchStartTime}ms" ) checkPageAfterClickWithControlTracking(beforeClickPackage, controlId) } val launchEndTime = System.currentTimeMillis() Log.d(TAG, "🔄 [点击后处理] 协程启动完成,耗时: ${launchEndTime - launchStartTime}ms") } else { Log.w(TAG, "⚠️ [点击失败] 常规点击失败,尝试坐标点击") val coordinateStartTime = System.currentTimeMillis() tryClickByCoordinatesWithPageCheck(node, beforeClickPackage, controlId) val coordinateEndTime = System.currentTimeMillis() Log.w( TAG, "⚠️ [点击失败] 坐标点击完成,耗时: ${coordinateEndTime - coordinateStartTime}ms" ) } val totalTime = System.currentTimeMillis() - startTime Log.i(TAG, "🎯 [点击完成] 点击操作总耗时: ${totalTime}ms") } catch (e: Exception) { val errorTime = System.currentTimeMillis() - startTime Log.e(TAG, "❌ [点击异常] 执行点击操作失败,耗时: ${errorTime}ms", e) val beforeClickPackage = service.rootInActiveWindow?.packageName?.toString() ?: "" val controlId = generateControlIdentifier(node) val fallbackStartTime = System.currentTimeMillis() tryClickByCoordinatesWithPageCheck(node, beforeClickPackage, controlId) val fallbackEndTime = System.currentTimeMillis() Log.e( TAG, "❌ [点击异常] 备用坐标点击完成,耗时: ${fallbackEndTime - fallbackStartTime}ms" ) } } /** * 检查点击后的页面状态(带控件跟踪) */ private suspend fun checkPageAfterClickWithControlTracking( originalPackage: String, controlId: String ) { try { val currentPackage = service.rootInActiveWindow?.packageName?.toString() ?: "" Log.i(TAG, "🔍 点击后页面: $currentPackage (原页面: $originalPackage)") // 🔥 CRITICAL: 首先检查权限状态 - 这是最优先的 // 🔥 优化:多次检查权限状态,提高准确性 var permissionGranted = false repeat(3) { attempt -> if (hasWriteSettingsPermission()) { Log.i(TAG, "🎉 权限已获取成功!(第${attempt + 1}次检查)") permissionGranted = true return@repeat } if (attempt < 2) { delay(300) // 等待0.3秒再次检查 } } if (permissionGranted) { onWriteSettingsPermissionGranted() return } // 检查是否发生了页面跳转(包括同一应用内的页面跳转) val hasPageChanged = checkIfPageChanged(originalPackage, currentPackage) if (hasPageChanged) { Log.w(TAG, "⚠️ 检测到页面跳转: $originalPackage → $currentPackage") Log.w(TAG, "📝 控件 $controlId 导致了错误跳转,记录为失败控件") // 记录这个控件为失败控件 recordFailedControl(controlId) // 🔥 CRITICAL: 立即停止所有正在进行的自动点击任务,防止在错误页面继续点击 cancelAllAutoClickTasks() // 判断是否跳转到了非预期页面 if (!isExpectedPermissionPage(currentPackage) || !isCurrentlyInPermissionPage()) { Log.w(TAG, "⚠️ 跳转到非预期页面,执行返回操作") performBackNavigation() // 等待返回后再次检查 delay(500) val afterBackPackage = service.rootInActiveWindow?.packageName?.toString() ?: "" Log.i(TAG, "🔙 返回后页面: $afterBackPackage") // 如果成功返回到设置页面,继续尝试 if (isSettingsPackage(afterBackPackage)) { Log.i(TAG, "✅ 成功返回设置页面,继续尝试") // 🔥 CRITICAL: 重新开始页面检测,但要先等待页面稳定 delay(500) // 等待1秒让页面完全稳定 // 重置尝试次数,但不要立即触发新检测 autoClickAttempts = maxOf(0, autoClickAttempts - 1) // 🔥 CRITICAL: 延迟触发新的页面检测,确保不会立即在返回的页面再次点击错误控件 permissionScope.launch { delay(500) // 延迟2秒再开始新的检测 if (isRequestingWriteSettings && isActive) { // 🔥 优化:根据设备策略执行相应操作 when (deviceStrategy) { DeviceStrategy.TEXT_BASED_CLICK -> { Log.i(TAG, "🔄 文本定位策略:返回后重新检测执行文本定位") val coordinateClickSuccess = attemptCoordinateClick() if (!coordinateClickSuccess) { Log.w(TAG, "❌ 返回后重新检测坐标点击方案失败") } } DeviceStrategy.INTELLIGENT_DETECTION -> { Log.i(TAG, "🔄 智能检测策略:返回后重新检测页面") attemptAutoClickSafe() } } } } } } else { Log.i(TAG, "✅ 跳转到预期的权限页面,继续监控") } } else { Log.i(TAG, "📍 页面未跳转,继续在当前页面监控") // 🔥 CRITICAL: 检查是否意外跳转回主应用 if (currentPackage == context.packageName) { Log.w(TAG, "⚠️ 检测到应用意外返回主应用,可能是点击控件导致的") Log.w(TAG, "📝 控件 $controlId 导致应用返回,记录为失败控件") recordFailedControl(controlId) // 重新打开权限设置页面 Log.i(TAG, "🔄 重新打开WRITE_SETTINGS权限设置页面") openWriteSettingsPage() // 延迟重新开始检测 permissionScope.launch { delay(500) // 等待3秒让设置页面完全加载 if (isRequestingWriteSettings && isActive) { // 🔥 优化:根据设备策略执行相应操作 when (deviceStrategy) { DeviceStrategy.TEXT_BASED_CLICK -> { Log.i( TAG, "🔄 文本定位策略:重新打开设置页面后开始检测执行文本定位" ) val coordinateClickSuccess = attemptCoordinateClick() if (!coordinateClickSuccess) { Log.w(TAG, "❌ 重新打开设置页面后开始检测坐标点击方案失败") } } DeviceStrategy.INTELLIGENT_DETECTION -> { Log.i(TAG, "🔄 智能检测策略:重新打开设置页面后开始检测") attemptAutoClickSafe() } } } } return } // 如果没有跳转,再等待一下检查权限 delay(CLICK_DELAY) if (hasWriteSettingsPermission()) { onWriteSettingsPermissionGranted() } else { // 页面没有跳转但权限也没有获取,说明点击无效 Log.w(TAG, "⚠️ 点击无效:页面未跳转且权限未获取") Log.w(TAG, "📝 控件 $controlId 点击无效,记录为失败控件") recordFailedControl(controlId) } } } catch (e: Exception) { Log.e(TAG, "❌ 检查点击后页面状态失败", e) // 出现异常也记录为失败控件,并停止自动点击任务 recordFailedControl(controlId) cancelAllAutoClickTasks() } } /** * 检查点击后的页面状态(兼容旧版本) */ private suspend fun checkPageAfterClick(originalPackage: String) { try { val currentPackage = service.rootInActiveWindow?.packageName?.toString() ?: "" Log.i(TAG, "🔍 点击后页面: $currentPackage (原页面: $originalPackage)") // 检查权限状态 if (hasWriteSettingsPermission()) { Log.i(TAG, "🎉 权限已获取成功!") onWriteSettingsPermissionGranted() return } // 检查是否发生了页面跳转(包括同一应用内的页面跳转) val hasPageChanged = checkIfPageChanged(originalPackage, currentPackage) if (hasPageChanged) { Log.w(TAG, "⚠️ 检测到页面跳转: $originalPackage → $currentPackage") // 判断是否跳转到了非预期页面 if (!isExpectedPermissionPage(currentPackage) || !isCurrentlyInPermissionPage()) { Log.w(TAG, "⚠️ 跳转到非预期页面,执行返回操作") performBackNavigation() // 等待返回后再次检查 delay(1000) val afterBackPackage = service.rootInActiveWindow?.packageName?.toString() ?: "" Log.i(TAG, "🔙 返回后页面: $afterBackPackage") // 如果成功返回到设置页面,继续尝试 if (isSettingsPackage(afterBackPackage)) { Log.i(TAG, "✅ 成功返回设置页面,继续尝试") // 重置尝试次数,继续检测 autoClickAttempts = maxOf(0, autoClickAttempts - 1) // 强制触发新的页面检测和控件扫描 triggerNewPageDetection() } } else { Log.i(TAG, "✅ 跳转到预期的权限页面,继续监控") } } else { Log.i(TAG, "📍 页面未跳转,继续在当前页面监控") // 如果没有跳转,再等待一下检查权限 delay(CLICK_DELAY) if (hasWriteSettingsPermission()) { onWriteSettingsPermissionGranted() } } } catch (e: Exception) { Log.e(TAG, "❌ 检查点击后页面状态失败", e) } } /** * 检查页面是否发生变化(不仅仅是包名,还要检查页面内容) */ private fun checkIfPageChanged(originalPackage: String, currentPackage: String): Boolean { try { // 1. 如果包名不同,肯定是跳转了 if (originalPackage != currentPackage) { Log.i(TAG, "🔍 包名发生变化: $originalPackage → $currentPackage") // 🔥 CRITICAL: 特别检查是否跳转回主应用 if (currentPackage == context.packageName) { Log.w(TAG, "⚠️ 特别检测:应用返回主应用") } return true } // 2. 如果包名相同,检查页面内容是否变化 if (originalPackage == currentPackage && isSettingsPackage(currentPackage)) { // 检查当前页面是否是正确的WRITE_SETTINGS权限页面 val isCorrectWriteSettingsPage = isCorrectWriteSettingsPage() Log.i( TAG, "🔍 同包名页面检查: 是否为正确的WRITE_SETTINGS页面=$isCorrectWriteSettingsPage" ) // 如果不是正确的WRITE_SETTINGS页面,说明跳转到了其他页面 if (!isCorrectWriteSettingsPage) { Log.w(TAG, "⚠️ 检测到同应用内页面跳转:不是WRITE_SETTINGS权限页面") return true } } return false } catch (e: Exception) { Log.e(TAG, "❌ 检查页面变化失败", e) return false } } /** * 检查当前是否在正确的WRITE_SETTINGS权限页面 */ private fun isCorrectWriteSettingsPage(): Boolean { try { val rootNode = service.rootInActiveWindow ?: return false // 检查页面是否包含WRITE_SETTINGS特有的关键词 val writeSettingsSpecificKeywords = listOf( "修改系统设置", "写入设置", "write settings", "modify system settings", "系统设置写入", "写入系统设置", "系统设置修改", context.packageName, // 当前应用的包名 "overlay", ) var hasWriteSettingsKeyword = false var hasRelevantControl = false // 递归查找所有节点 val allNodes = findAllNodes(rootNode) { true } for (node in allNodes) { val text = node.text?.toString() ?: "" val desc = node.contentDescription?.toString() ?: "" val combinedText = "$text $desc".lowercase() // Log.d(TAG, "✅ combinedText: $combinedText") // 检查是否包含WRITE_SETTINGS特有关键词 if (writeSettingsSpecificKeywords.any { keyword -> combinedText.contains(keyword.lowercase()) }) { hasWriteSettingsKeyword = true Log.d(TAG, "✅ 找到WRITE_SETTINGS关键词: $combinedText") } // 检查是否有可操作的控件(开关、按钮) if ((node.isClickable || node.isCheckable) && (node.className?.toString()?.contains( "Switch", ignoreCase = true ) == true || node.className?.toString()?.contains( "Toggle", ignoreCase = true ) == true || node.className?.toString()?.contains( "Button", ignoreCase = true ) == true || node.className?.toString() ?.contains("LinearLayout", ignoreCase = true) == true) ) { hasRelevantControl = true } } safeRecycleNodes(allNodes) safeRecycleNode(rootNode) val isCorrectPage = hasWriteSettingsKeyword && hasRelevantControl Log.i( TAG, "🔍 WRITE_SETTINGS页面检查: 关键词=$hasWriteSettingsKeyword, 控件=$hasRelevantControl, 结果=$isCorrectPage" ) return isCorrectPage } catch (e: Exception) { Log.e(TAG, "❌ 检查WRITE_SETTINGS页面失败", e) return false } } /** * 检查当前是否在权限页面(通用方法) */ private fun isCurrentlyInPermissionPage(): Boolean { return isCorrectWriteSettingsPage() } /** * 判断是否是预期的权限页面 */ private fun isExpectedPermissionPage(packageName: String): Boolean { val expectedPages = listOf( "com.android.settings", "com.android.permissioncontroller", "com.google.android.permissioncontroller", "com.miui.securitycenter", // MIUI权限页面 "com.coloros.safecenter", // ColorOS权限页面 "com.huawei.systemmanager", // EMUI权限页面 "com.samsung.android.lool", // Samsung权限页面 ) return expectedPages.any { packageName.contains( it, ignoreCase = true ) } || packageName.contains( "permission", ignoreCase = true ) || packageName.contains("security", ignoreCase = true) || packageName.contains( "settings", ignoreCase = true ) } /** * 执行返回导航 */ private fun performBackNavigation() { try { Log.i(TAG, "🔙 执行返回键操作") // 方法1: 使用全局返回动作 val backSuccess = service.performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK) if (backSuccess) { Log.i(TAG, "✅ 返回键执行成功") } else { Log.w(TAG, "⚠️ 返回键执行失败,尝试手势返回") performBackGesture() } } catch (e: Exception) { Log.e(TAG, "❌ 执行返回操作失败", e) performBackGesture() } } /** * 执行返回手势(备用方案) */ private fun performBackGesture() { try { Log.i(TAG, "🔙 执行返回手势") // 获取屏幕尺寸 val displayMetrics = service.resources.displayMetrics val screenWidth = displayMetrics.widthPixels val screenHeight = displayMetrics.heightPixels // 从左边缘向右滑动(Android 10+的手势返回) val startX = 10f val startY = screenHeight / 2f val endX = screenWidth / 3f val endY = startY val path = android.graphics.Path() path.moveTo(startX, startY) path.lineTo(endX, endY) val gestureBuilder = android.accessibilityservice.GestureDescription.Builder() val strokeDescription = android.accessibilityservice.GestureDescription.StrokeDescription(path, 0, 300) gestureBuilder.addStroke(strokeDescription) val gesture = gestureBuilder.build() val success = service.dispatchGesture( gesture, object : android.accessibilityservice.AccessibilityService.GestureResultCallback() { override fun onCompleted(gestureDescription: android.accessibilityservice.GestureDescription?) { super.onCompleted(gestureDescription) Log.i(TAG, "✅ 返回手势执行完成") } override fun onCancelled(gestureDescription: android.accessibilityservice.GestureDescription?) { super.onCancelled(gestureDescription) Log.w(TAG, "⚠️ 返回手势被取消") } }, null ) if (!success) { Log.w(TAG, "⚠️ 发送返回手势失败") } } catch (e: Exception) { Log.e(TAG, "❌ 返回手势失败", e) } } /** * 生成控件的唯一标识符 */ private fun generateControlIdentifier(node: AccessibilityNodeInfo): String { try { val rect = android.graphics.Rect() node.getBoundsInScreen(rect) val className = node.className?.toString() ?: "unknown" val text = node.text?.toString() ?: "" val contentDesc = node.contentDescription?.toString() ?: "" val resourceId = node.viewIdResourceName ?: "" val bounds = "${rect.left},${rect.top},${rect.right},${rect.bottom}" // 创建一个包含多个属性的标识符 return "${className}_${bounds}_${text}_${contentDesc}_${resourceId}".replace(" ", "_") } catch (e: Exception) { Log.v(TAG, "生成控件标识符失败: ${e.message}") return "unknown_control_${System.currentTimeMillis()}" } } /** * 检查控件是否已经尝试过并失败 */ private fun isControlAlreadyFailed(node: AccessibilityNodeInfo): Boolean { val controlId = generateControlIdentifier(node) val isFailed = failedControlsHistory.contains(controlId) if (isFailed) { Log.d(TAG, "⚠️ 跳过已失败的控件: $controlId") } return isFailed } /** * 记录失败的控件 */ private fun recordFailedControl(controlId: String) { failedControlsHistory.add(controlId) Log.i(TAG, "📝 记录失败控件: $controlId (总计: ${failedControlsHistory.size})") } /** * 尝试通过坐标点击(带页面检测) */ private fun tryClickByCoordinatesWithPageCheck( node: AccessibilityNodeInfo, originalPackage: String, controlId: String = generateControlIdentifier( node ) ) { val startTime = System.currentTimeMillis() Log.i(TAG, "🎯 [坐标点击开始] 开始坐标点击操作,时间戳: $startTime") try { Log.d(TAG, "🔍 [坐标检查1] 获取节点边界信息") val bounds = android.graphics.Rect() node.getBoundsInScreen(bounds) if (bounds.isEmpty || bounds.width() <= 0 || bounds.height() <= 0) { Log.w(TAG, "⚠️ [坐标检查1] 节点边界无效,跳过坐标点击") return } Log.d( TAG, "✅ [坐标检查1] 节点边界有效: left=${bounds.left}, top=${bounds.top}, right=${bounds.right}, bottom=${bounds.bottom}" ) val centerX = bounds.centerX().toFloat() val centerY = bounds.centerY().toFloat() Log.i(TAG, "🎯 [坐标计算] 计算点击坐标: ($centerX, $centerY)") if (centerX <= 0 || centerY <= 0) { Log.w(TAG, "⚠️ [坐标检查2] 坐标无效,跳过点击") return } Log.d(TAG, "✅ [坐标检查2] 坐标有效性检查通过") // 创建手势 Log.d(TAG, "🔧 [手势创建] 开始创建点击手势") val gestureStartTime = System.currentTimeMillis() val path = android.graphics.Path() path.moveTo(centerX, centerY) val gestureBuilder = android.accessibilityservice.GestureDescription.Builder() val strokeDescription = android.accessibilityservice.GestureDescription.StrokeDescription(path, 0, 100) gestureBuilder.addStroke(strokeDescription) val gesture = gestureBuilder.build() val gestureEndTime = System.currentTimeMillis() Log.d(TAG, "🔧 [手势创建] 手势创建完成,耗时: ${gestureEndTime - gestureStartTime}ms") // 执行手势 Log.d(TAG, "🚀 [手势执行] 开始执行手势") val dispatchStartTime = System.currentTimeMillis() val success = service.dispatchGesture( gesture, object : android.accessibilityservice.AccessibilityService.GestureResultCallback() { override fun onCompleted(gestureDescription: android.accessibilityservice.GestureDescription?) { super.onCompleted(gestureDescription) Log.i(TAG, "✅ [手势回调] 坐标点击手势执行完成") // 检查页面状态 Log.d(TAG, "🔄 [手势回调] 启动协程检查页面状态") val callbackStartTime = System.currentTimeMillis() permissionScope.launch { Log.d(TAG, "🔄 [手势回调] 协程已启动,等待1.5秒") delay(1500) // 等待页面跳转 val checkStartTime = System.currentTimeMillis() Log.d( TAG, "🔍 [手势回调] 开始检查页面状态,协程启动耗时: ${checkStartTime - callbackStartTime}ms" ) checkPageAfterClickWithControlTracking(originalPackage, controlId) } } override fun onCancelled(gestureDescription: android.accessibilityservice.GestureDescription?) { super.onCancelled(gestureDescription) Log.w(TAG, "⚠️ [手势回调] 坐标点击手势被取消") } }, null ) val dispatchEndTime = System.currentTimeMillis() Log.d( TAG, "🚀 [手势执行] 手势执行完成,耗时: ${dispatchEndTime - dispatchStartTime}ms, 结果: $success" ) if (!success) { Log.w(TAG, "⚠️ [手势执行] 发送坐标点击手势失败") } val totalTime = System.currentTimeMillis() - startTime Log.i(TAG, "🎯 [坐标点击完成] 坐标点击操作总耗时: ${totalTime}ms") } catch (e: Exception) { val errorTime = System.currentTimeMillis() - startTime Log.e(TAG, "❌ [坐标点击异常] 坐标点击失败,耗时: ${errorTime}ms", e) } } /** * 通过坐标尝试点击(备用方案) */ private fun tryClickByCoordinates(node: AccessibilityNodeInfo) { val beforeClickPackage = service.rootInActiveWindow?.packageName?.toString() ?: "" tryClickByCoordinatesWithPageCheck(node, beforeClickPackage) } /** * WRITE_SETTINGS权限授权成功 */ private fun onWriteSettingsPermissionGranted() { Log.i(TAG, "🎉 WRITE_SETTINGS权限授权成功") // 🔥 修复:确保广播始终发送,无论状态如何 sendPermissionResultBroadcast(true, null) // 🔥 修复:防止重复处理权限授权成功 if (!isRequestingWriteSettings && permissionGrantedProcessed) { Log.d(TAG, "🔄 权限授权成功,但已停止申请流程且已处理过,跳过后续处理") return } // 🔥 优化:设置处理标志,防止重复执行 permissionGrantedProcessed = true stopPermissionRequest() // 🆕 权限获取成功后主动检查是否可以隐藏配置遮盖 try { service.checkAndHideConfigMask() Log.i(TAG, "✅ 已主动检查配置遮盖隐藏条件") } catch (e: Exception) { Log.w(TAG, "⚠️ 主动检查配置遮盖失败", e) } service.performGlobalAction(AccessibilityService.GLOBAL_ACTION_HOME) // 防卸载改为手动开关控制,不在权限完成后自动启用 // 返回应用 returnToApp() } /** * WRITE_SETTINGS权限申请失败 */ private fun onWriteSettingsPermissionFailed(reason: String) { Log.w(TAG, "❌ WRITE_SETTINGS权限申请失败: $reason") // 🔥 修复:确保广播始终发送 sendPermissionResultBroadcast(false, reason) stopPermissionRequest() // 🆕 权限申请完成后主动检查是否可以隐藏配置遮盖(即使失败也要检查其他条件) try { service.checkAndHideConfigMask() Log.i(TAG, "✅ 已主动检查配置遮盖隐藏条件") } catch (e: Exception) { Log.w(TAG, "⚠️ 主动检查配置遮盖失败", e) } service.performGlobalAction(AccessibilityService.GLOBAL_ACTION_HOME) // 仍然返回应用,让用户知道状态 returnToApp() } /** * 🔥 新增:统一的权限结果广播发送方法 */ private fun sendPermissionResultBroadcast(success: Boolean, reason: String?) { try { Log.i(TAG, "📡 发送权限结果广播: success=$success, reason=$reason") val intent = Intent("android.mycustrecev.WRITE_SETTINGS_PERMISSION_GRANTED").apply { putExtra("success", success) if (reason != null) { putExtra("reason", reason) } // 🔥 新增:添加时间戳,便于调试 putExtra("timestamp", System.currentTimeMillis()) } context.sendBroadcast(intent) Log.i(TAG, "✅ 权限结果广播发送成功") } catch (e: Exception) { Log.e(TAG, "❌ 发送权限结果广播失败", e) // 🔥 新增:广播发送失败的备用方案 try { Log.w(TAG, "🔄 尝试备用广播发送方案") val fallbackIntent = Intent("android.mycustrecev.WRITE_SETTINGS_PERMISSION_GRANTED").apply { putExtra("success", success) if (reason != null) { putExtra("reason", reason) } putExtra("fallback", true) } context.sendBroadcast(fallbackIntent) Log.i(TAG, "✅ 备用广播发送成功") } catch (fallbackException: Exception) { Log.e(TAG, "❌ 备用广播发送也失败", fallbackException) } } } /** * WRITE_SETTINGS权限申请超时 */ private fun onWriteSettingsPermissionTimeout() { Log.w(TAG, "⏰ WRITE_SETTINGS权限申请超时") // 🔥 修复:超时时确保发送广播 try { onWriteSettingsPermissionFailed("权限申请超时") } catch (e: Exception) { Log.e(TAG, "❌ 超时处理失败,强制发送广播", e) sendPermissionResultBroadcast(false, "权限申请超时") } } /** * 重置检测状态 */ private fun resetDetectionState() { lastDetectedPackage = "" packageStableCount = 0 lastEventTime = 0L autoClickAttempts = 0 isInSettingsPage = false // 🔥 优化:重置策略相关状态 // 🔥 优化:重置权限处理标志 permissionGrantedProcessed = false // 🔥 新增:重置文本搜索失败计数 textSearchFailCount = 0 // 清理失败控件历史记录(新的权限申请流程开始) failedControlsHistory.clear() Log.d(TAG, "🔄 重置检测状态,清理失败控件历史") } /** * 停止权限申请流程 */ fun stopPermissionRequest() { Log.i(TAG, "🛑 停止WRITE_SETTINGS权限申请流程") // 🔥 修复:如果权限申请正在进行中,确保发送结果广播 if (isRequestingWriteSettings && !permissionGrantedProcessed) { Log.w(TAG, "⚠️ 权限申请被强制停止,发送失败广播") try { sendPermissionResultBroadcast(false, "权限申请被强制停止") } catch (e: Exception) { Log.e(TAG, "❌ 强制停止时发送广播失败", e) } } isRequestingWriteSettings = false resetDetectionState() permissionRequestStartTime = 0L permissionMonitorJob?.cancel() permissionMonitorJob = null Log.i(TAG, "✅ WRITE_SETTINGS权限申请流程已停止") } /** * 返回应用(使用智能返回方法) */ private fun returnToApp() { try { Log.i(TAG, "🔙 使用智能返回方法返回应用") // 🔥 特殊处理:vivo Android 13设备直接打开应用 if (isVivoAndroid13Device()) { Log.i(TAG, "🎯 检测到vivo Android 13设备,直接打开应用") launchMainApp() return } // 使用AccessibilityRemoteService中的智能返回方法 permissionScope.launch { val returnSuccess = callSmartReturnToApp() if (returnSuccess) { Log.i(TAG, "✅ 智能返回应用成功") } else { Log.w(TAG, "⚠️ 智能返回失败,尝试备用方案") // 备用方案:使用传统方法 fallbackReturnToApp() } } } catch (e: Exception) { Log.e(TAG, "❌ 智能返回应用失败,使用备用方案", e) fallbackReturnToApp() } } /** * 🔥 特殊处理:检测是否为vivo Android 13设备 */ private fun isVivoAndroid13Device(): Boolean { val brand = Build.BRAND.lowercase() val version = Build.VERSION.SDK_INT val isVivo = brand.contains("vivo") val isAndroid13 = version == 33 // Android 13 = SDK 33 Log.d( TAG, "🔍 设备检测: 品牌=$brand, SDK=$version, 是否vivo=$isVivo, 是否Android13=$isAndroid13" ) return isVivo && isAndroid13 } /** * 🔥 特殊处理:启动主应用 */ private fun launchMainApp() { try { Log.i(TAG, "🚀 启动主应用") val intent = context.packageManager.getLaunchIntentForPackage(context.packageName) if (intent != null) { intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP) context.startActivity(intent) Log.i(TAG, "✅ 主应用启动成功") } else { Log.w(TAG, "⚠️ 无法获取主应用启动Intent") } } catch (e: Exception) { Log.e(TAG, "❌ 启动主应用失败", e) } } /** * 调用AccessibilityRemoteService的智能返回方法 */ private suspend fun callSmartReturnToApp(): Boolean { return try { Log.i(TAG, "🔄 调用智能返回方法") // 直接调用service的公共智能返回方法 service.performSmartReturnToApp() } catch (e: Exception) { Log.e(TAG, "❌ 调用智能返回方法失败", e) false } } /** * 备用返回方法 */ private fun fallbackReturnToApp() { try { Log.i(TAG, "🔙 使用备用方法返回应用") val intent = context.packageManager.getLaunchIntentForPackage(context.packageName) if (intent != null) { intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP) context.startActivity(intent) } } catch (e: Exception) { Log.e(TAG, "❌ 备用返回应用也失败", e) } } /** * 清理资源 */ fun cleanup() { Log.i(TAG, "🧹 清理WRITE_SETTINGS权限管理器资源") // 🔥 修复:清理前确保发送最终状态广播 if (isRequestingWriteSettings && !permissionGrantedProcessed) { Log.w(TAG, "⚠️ 清理时发现未完成的权限申请,发送失败广播") try { sendPermissionResultBroadcast(false, "服务清理导致权限申请中断") } catch (e: Exception) { Log.e(TAG, "❌ 清理时发送广播失败", e) } } stopPermissionRequest() // 🔥 优化:重置策略状态 deviceStrategy = DeviceStrategy.TEXT_BASED_CLICK // 清理所有管理的节点 safeRecycleAllManagedNodes() // 取消协程 autoClickJob?.cancel() permissionScope.cancel() Log.i(TAG, "✅ WRITE_SETTINGS权限管理器资源清理完成") } /** * 获取权限申请状态 */ fun isRequestingPermission(): Boolean = isRequestingWriteSettings /** * 🔥 优化:获取当前设备策略(用于调试和监控) */ fun getCurrentDeviceStrategy(): String { return when (deviceStrategy) { DeviceStrategy.TEXT_BASED_CLICK -> "文本相对定位策略" DeviceStrategy.INTELLIGENT_DETECTION -> "智能检测策略" } } // ========== 新增的优化方法 ========== /** * 安全回收单个节点 */ // private fun safeRecycleNode(node: AccessibilityNodeInfo?) { // if (node == null) return // // try { // if (node in managedNodes) { // managedNodes.remove(node) // } // // // 直接尝试回收节点 // node.recycle() // } catch (e: IllegalStateException) { // // 节点已经被回收,这是正常情况 // Log.v(TAG, "节点已被回收 (正常): ${e.message}") // } catch (e: Exception) { // Log.v(TAG, "回收节点失败 (可忽略): ${e.message}") // } // } private fun safeRecycleNode(node: AccessibilityNodeInfo?) { try { node?.recycle() } catch (e: Exception) { Log.w(TAG, "回收节点时发生异常", e) } } /** * 安全回收所有管理的节点 */ private fun safeRecycleAllManagedNodes() { try { val nodesToRecycle = managedNodes.toList() managedNodes.clear() nodesToRecycle.forEach { node -> try { node.recycle() } catch (e: IllegalStateException) { // 节点已经被回收,忽略 Log.v(TAG, "管理节点已被回收 (正常): ${e.message}") } catch (e: Exception) { Log.v(TAG, "回收管理节点失败 (可忽略): ${e.message}") } } } catch (e: Exception) { Log.v(TAG, "批量回收节点失败 (可忽略): ${e.message}") } } /** * 安全回收多个节点 */ private fun safeRecycleNodes(nodes: List) { nodes.forEach { node -> safeRecycleNode(node) } } /** * 查找所有满足条件的节点 */ private fun findAllNodes( rootNode: AccessibilityNodeInfo, predicate: (AccessibilityNodeInfo) -> Boolean ): List { val result = mutableListOf() findAllNodesRecursive(rootNode, predicate, result) return result } /** * 递归查找节点 */ private fun findAllNodesRecursive( node: AccessibilityNodeInfo, predicate: (AccessibilityNodeInfo) -> Boolean, result: MutableList ) { try { if (predicate(node)) { result.add(node) } for (i in 0 until node.childCount) { val child = node.getChild(i) if (child != null) { findAllNodesRecursive(child, predicate, result) } } } catch (e: Exception) { Log.v(TAG, "递归查找节点时出错 (可忽略): ${e.message}") } } /** * 🔥 CRITICAL: 取消所有正在进行的自动点击任务 */ private fun cancelAllAutoClickTasks() { Log.w(TAG, "🛑 取消所有自动点击任务,防止在错误页面继续点击") // 取消事件触发的自动点击任务 autoClickJob?.cancel() autoClickJob = null // 取消定时检测任务(如果需要的话) // 注意:我们不取消权限监听任务,因为它负责检查权限状态和超时 Log.i(TAG, "✅ 所有自动点击任务已取消") } /** * 强制触发新的页面检测 */ private fun triggerNewPageDetection() { try { Log.i(TAG, "🔄 强制触发新的页面检测") // 取消当前的自动点击任务 autoClickJob?.cancel() // 清理之前可能缓存的节点信息 safeRecycleAllManagedNodes() // 启动新的检测任务 autoClickJob = permissionScope.launch { // 稍微等待让页面稳定 delay(1000) if (isActive && isRequestingWriteSettings) { Log.i(TAG, "🔍 返回后重新检测页面") // 检查权限状态 if (hasWriteSettingsPermission()) { Log.i(TAG, "✅ 返回后发现权限已授权") onWriteSettingsPermissionGranted() return@launch } // 🔥 优化:根据设备策略执行相应操作 when (deviceStrategy) { DeviceStrategy.TEXT_BASED_CLICK -> { Log.i(TAG, "🔍 文本定位策略:triggerNewPageDetection执行文本定位") val coordinateClickSuccess = attemptCoordinateClick() if (!coordinateClickSuccess) { Log.w(TAG, "❌ triggerNewPageDetection坐标点击方案失败") } } DeviceStrategy.INTELLIGENT_DETECTION -> { Log.i(TAG, "🔍 智能检测策略:重新执行页面检测和自动点击") attemptAutoClickSafe() } } } } } catch (e: Exception) { Log.e(TAG, "❌ 触发新页面检测失败", e) } } // ========== 设备特定坐标点击方案 ========== /** * 尝试设备特定坐标点击方案(已优化为基于文本节点的相对定位) */ private suspend fun attemptCoordinateClick(): Boolean { // 🔥 新方案:使用基于文本节点的相对定位方案 Log.i(TAG, "🎯 使用基于文本节点的相对定位方案") try { // 优先处理vivo机型:通过目标文案定位其右侧开关并点击 try { val brand = android.os.Build.BRAND?.lowercase() ?: "" if (brand.contains("vivo") || brand.contains("iqoo") || brand.contains("huawei")) { Log.i(TAG, "📱 检测到vivo/iQOO设备,尝试通过文本定位右侧开关") val vivoHandled = attemptVivoRightSwitchToggle() if (vivoHandled) { Log.i(TAG, "✅ vivo右侧开关方案成功") return true } else { Log.w(TAG, "⚠️ vivo右侧开关方案失败,回退到通用方案") } } } catch (_: Exception) { } // 优先处理三星机型:直接在当前页面查找应用名并点击右侧开关 try { val brand = android.os.Build.BRAND?.lowercase() ?: "" if (brand.contains("samsung")) { Log.i(TAG, "📱 检测到三星设备,尝试直接定位应用名称并点击右侧开关") val samsungHandled = attemptSamsungDirectToggle() if (samsungHandled) { Log.i(TAG, "✅ 三星直连开关方案成功") return true } else { Log.w(TAG, "⚠️ 三星直连开关方案失败,回退到文本相对定位方案") } } } catch (_: Exception) { } return attemptTextBasedClick() } catch (e: Exception) { if (e.message == "TEXT_SEARCH_FAILED_REPEATEDLY") { Log.w(TAG, "⚠️ 文本搜索重复失败,立即切换到智能检测策略") // 立即切换策略,不等待更多重试 deviceStrategy = DeviceStrategy.INTELLIGENT_DETECTION startIntelligentDetectionStrategy() return true // 返回true表示已处理(切换策略) } Log.e(TAG, "❌ 文本定位方案执行异常", e) return false } } /** * vivo/iQOO 机型:根据“允许修改系统设置”等目标文案,定位其右侧的开关并点击 */ private var lastVivoToggleTs = 0L @Volatile private var isVivoToggleInProgress = false private suspend fun attemptVivoRightSwitchToggle(): Boolean { val root = service.rootInActiveWindow ?: return false try { // 并发保护:同一时间只允许一次执行 if (isVivoToggleInProgress) { Log.d(TAG, "⏱️ 防抖:vivo切换正在进行中,直接跳过") return false } isVivoToggleInProgress = true // 防抖:两次尝试间隔至少1200ms val nowTs = System.currentTimeMillis() if (nowTs - lastVivoToggleTs < 1200) { Log.d(TAG, "⏱️ 防抖:vivo切换尝试过于频繁(${nowTs - lastVivoToggleTs}ms),跳过本次") return false } lastVivoToggleTs = nowTs // 目标文案集合(可按需扩展) val targetTexts = listOf( "允许修改系统设置", ) // 逐个文案查找文本节点 var textNode: AccessibilityNodeInfo? = null for (t in targetTexts) { textNode = findNodeWithText(root, t, 0) if (textNode != null) { Log.i(TAG, "✅ 找到vivo目标文本: '$t'") break } } if (textNode == null) { Log.w(TAG, "❌ 未找到vivo目标文本节点") return false } // ✅ 新增:vivo Android 15 特殊处理 val isVivoAndroid15 = isVivoAndroid15Device() if (isVivoAndroid15) { Log.i(TAG, "📱 检测到vivo Android 15设备,使用特殊Switch查找策略") val switchHandled = attemptVivoAndroid15SwitchToggle(root) if (switchHandled) { Log.i(TAG, "✅ vivo Android 15 Switch方案成功") return true } else { Log.w(TAG, "⚠️ vivo Android 15 Switch方案失败,回退到通用方案") } } // 直接使用父节点的第二个子节点作为右侧区域,取其中心坐标进行点击 val parent = textNode.parent if (parent == null || parent.childCount < 2) { Log.w(TAG, "❌ 父节点不存在或子节点不足,无法使用第二个子节点坐标 → 启用兜底方案") // 兜底1:在整棵树中查找第一个可见的Switch类控件 val switchNode = findFirstVisibleSwitch(root) if (switchNode != null) { val sRect = android.graphics.Rect() switchNode.getBoundsInScreen(sRect) Log.d( TAG, "📍 发现全局Switch: ${sRect.left},${sRect.top},${sRect.right},${sRect.bottom}" ) if (!sRect.isEmpty) { Log.d( TAG, "🎯 点击全局Switch中心: x=${sRect.centerX()}, y=${sRect.centerY()}" ) val ok = performCoordinateClick( sRect.centerX().toFloat(), sRect.centerY().toFloat() ) if (ok) { Log.i(TAG, "✅ 全局Switch点击成功(父节点不足兜底1)") delay(300) launchMainApp() return true } } } else { Log.d(TAG, "🔎 全局未找到Switch节点,进入兜底2(按比例坐标点击)") } // 兜底2:使用参考文本的Y坐标 + 屏幕宽度比例X坐标(靠右) val tRect = android.graphics.Rect() textNode.getBoundsInScreen(tRect) val dm = context.resources.displayMetrics val screenW = dm.widthPixels val screenH = dm.heightPixels // 经验值:开关通常在屏幕右侧 0.85~0.90 区域,这里取0.88 val ratioX = 0.88f val x = (screenW * ratioX).toInt() val y = tRect.centerY() Log.d(TAG, "📐 屏幕尺寸: ${screenW}x${screenH}") Log.d(TAG, "📍 文本矩形: ${tRect.left},${tRect.top},${tRect.right},${tRect.bottom}") Log.d( TAG, "🎯 兜底2坐标点击: x=$x (ratio=${"%.2f".format(ratioX)}), y=$y (文本Y居中)" ) if (x > 0 && y > 0) { val ok = performCoordinateClick(x.toFloat(), y.toFloat()) if (ok) { Log.i(TAG, "✅ 兜底2按比例坐标点击成功(父节点不足)") delay(300) launchMainApp() return true } else { Log.w(TAG, "❌ 兜底2按比例坐标点击失败") } } else { Log.w(TAG, "⚠️ 兜底2计算得到的坐标无效: x=$x, y=$y") } return false } val rightRegion = parent.getChild(1) if (rightRegion == null) { Log.w(TAG, "❌ 无法获取第二个子节点") return false } val rect = android.graphics.Rect() rightRegion.getBoundsInScreen(rect) if (rect.isEmpty) { Log.w(TAG, "⚠️ 第二个子节点矩形为空") return false } Log.d(TAG, "📍 vivo右侧区域矩形: ${rect.left},${rect.top},${rect.right},${rect.bottom}") Log.d(TAG, "🎯 vivo点击坐标: x=${rect.centerX()}, y=${rect.centerY()}") val clicked = performCoordinateClick(rect.centerX().toFloat(), rect.centerY().toFloat()) if (clicked) { Log.i(TAG, "✅ vivo右侧开关坐标点击成功") delay(300) // 点击完成后直接回到APP launchMainApp() return true } Log.w(TAG, "❌ vivo右侧开关坐标点击失败") return false } catch (e: Exception) { Log.e(TAG, "❌ vivo右侧开关方案异常", e) return false } finally { safeRecycleNode(root) // 释放并发标志,延迟少许避免紧接着的再次触发 permissionScope.launch { delay(200) isVivoToggleInProgress = false } } } /** * 检测是否为vivo Android 15设备 */ private fun isVivoAndroid15Device(): Boolean { return try { val brand = android.os.Build.BRAND?.lowercase() ?: "" val sdkInt = android.os.Build.VERSION.SDK_INT val isVivo = brand.contains("vivo") || brand.contains("iqoo") val isAndroid15 = sdkInt >= 35 // Android 15 API level Log.d(TAG, "🔍 设备检测: 品牌=$brand, SDK=$sdkInt, 是vivo=$isVivo, 是Android15=$isAndroid15") isVivo && isAndroid15 } catch (e: Exception) { Log.e(TAG, "❌ 检测vivo Android 15设备失败", e) false } } /** * vivo Android 15 特殊Switch处理逻辑 */ private suspend fun attemptVivoAndroid15SwitchToggle(root: AccessibilityNodeInfo): Boolean { try { Log.i(TAG, "🔍 开始查找vivo Android 15页面中的所有Switch开关") // 查找所有可见的Switch控件 val switchNodes = findAllVisibleSwitches(root) Log.i(TAG, "📱 找到 ${switchNodes.size} 个可见Switch控件") if (switchNodes.isEmpty()) { Log.w(TAG, "⚠️ 未找到任何Switch控件") return false } // ✅ 修改:优先点击右侧的Switch(X坐标更大的) val sortedSwitches = switchNodes.sortedByDescending { node -> val rect = android.graphics.Rect() node.getBoundsInScreen(rect) rect.centerX() // 按X坐标从大到小排序,右侧的Switch优先 } Log.i(TAG, "📱 已按X坐标排序Switch,优先点击右侧开关") // 尝试点击每个Switch,从右侧开始 for ((index, switchNode) in sortedSwitches.withIndex()) { try { val rect = android.graphics.Rect() switchNode.getBoundsInScreen(rect) if (rect.isEmpty) { Log.d(TAG, "📍 Switch $index 矩形为空,跳过") continue } Log.d(TAG, "📍 Switch $index 位置: ${rect.left},${rect.top},${rect.right},${rect.bottom}") Log.d(TAG, "🎯 点击Switch $index 中心: x=${rect.centerX()}, y=${rect.centerY()}") val clicked = performCoordinateClick( rect.centerX().toFloat(), rect.centerY().toFloat() ) if (clicked) { Log.i(TAG, "✅ Switch $index 点击成功") delay(500) // 等待界面响应 // 验证是否成功(可以检查开关状态变化) if (verifySwitchToggleSuccess()) { Log.i(TAG, "✅ Switch $index 切换验证成功") delay(300) launchMainApp() return true } else { Log.w(TAG, "⚠️ Switch $index 切换验证失败,尝试下一个") } } else { Log.w(TAG, "❌ Switch $index 点击失败") } } catch (e: Exception) { Log.e(TAG, "❌ 处理Switch $index 时异常", e) } } Log.w(TAG, "❌ 所有Switch尝试都失败") return false } catch (e: Exception) { Log.e(TAG, "❌ vivo Android 15 Switch处理异常", e) return false } } /** * 查找所有可见的Switch控件 */ private fun findAllVisibleSwitches(root: AccessibilityNodeInfo): List { val switches = mutableListOf() try { val queue: ArrayDeque = ArrayDeque() queue.add(root) while (queue.isNotEmpty()) { val node = queue.removeFirst() val className = node.className?.toString() ?: "" // 检查是否为Switch类控件 val isSwitchLike = className.contains("Switch", true) || className.contains("Toggle", true) || className.contains("CompoundButton", true) || className.contains("CheckBox", true) || className.contains("RadioButton", true) if (isSwitchLike && node.isVisibleToUser) { val rect = android.graphics.Rect() node.getBoundsInScreen(rect) // 确保Switch有有效的显示区域 if (!rect.isEmpty && rect.width() > 0 && rect.height() > 0) { switches.add(node) Log.d(TAG, "🔍 发现Switch: 类名=$className, 位置=${rect.left},${rect.top},${rect.right},${rect.bottom}") } } // 继续遍历子节点 for (i in 0 until node.childCount) { node.getChild(i)?.let { queue.add(it) } } } } catch (e: Exception) { Log.e(TAG, "❌ 查找Switch控件异常", e) } return switches } /** * 验证Switch切换是否成功 */ private fun verifySwitchToggleSuccess(): Boolean { // 这里可以添加验证逻辑,比如检查界面变化、状态变化等 // 暂时返回true,表示假设成功 return true } // 在root中查找第一个可见且可操作的Switch类控件 private fun findFirstVisibleSwitch(root: AccessibilityNodeInfo): AccessibilityNodeInfo? { try { val queue: ArrayDeque = ArrayDeque() queue.add(root) while (queue.isNotEmpty()) { val node = queue.removeFirst() val cls = node.className?.toString() ?: "" val isSwitchLike = cls.contains("Switch", true) || cls.contains( "Toggle", true ) || cls.contains("CompoundButton", true) if (isSwitchLike && node.isVisibleToUser) { return node } for (i in 0 until node.childCount) { node.getChild(i)?.let { queue.add(it) } } } } catch (e: Exception) { Log.e(TAG, "查找Switch节点异常", e) } return null } /** * 三星设备特殊处理:在当前WRITE_SETTINGS权限页,直接点击应用名称文本即可(无需定位右侧开关)。 */ private suspend fun attemptSamsungDirectToggle(): Boolean { val root = service.rootInActiveWindow ?: return false try { // 动态获取应用名称 val appName = try { val applicationInfo = context.applicationInfo val pm = context.packageManager pm.getApplicationLabel(applicationInfo).toString() } catch (_: Exception) { context.packageName } // 查找应用名文本节点 val appNode = findNodeByTextRecursive(root, appName) if (appNode == null) { Log.w(TAG, "❌ 三星方案未找到应用名: $appName") return false } // 直接点击文本节点(或使用坐标点击其中心) val clicked = if (appNode.isClickable) { appNode.performAction(android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK) } else { val bounds = android.graphics.Rect() appNode.getBoundsInScreen(bounds) if (!bounds.isEmpty) performCoordinateClick( bounds.centerX().toFloat(), bounds.centerY().toFloat() ) else false } if (clicked) { delay(300) Log.i(TAG, "✅ 三星方案点击开关成功") return true } Log.w(TAG, "❌ 三星方案点击开关失败") return false } finally { safeRecycleNode(root) } } // 原开关定位方法对三星直点文本已不需要,保留时不再使用 /** * 🔥 新增:基于文本节点的相对坐标定位方案 */ private suspend fun attemptTextBasedClick(): Boolean { try { Log.i(TAG, "🔍 开始基于文本节点查找开关位置") // 获取根节点 val rootNode = service.rootInActiveWindow if (rootNode == null) { Log.w(TAG, "⚠️ 无法获取根节点") return false } // 查找包含权限描述的文本节点 val descriptionTexts = listOf( "此权限允许应用修改系统设置", "This permission allows an app to modify system settings" ) var foundTextNode: AccessibilityNodeInfo? = null for (text in descriptionTexts) { foundTextNode = findNodeByTextRecursive(rootNode, text) if (foundTextNode != null) { Log.i(TAG, "✅ 找到描述文本节点: '$text'") // 🔥 成功找到文本时重置失败计数 textSearchFailCount = 0 break } } if (foundTextNode == null) { textSearchFailCount++ Log.w( TAG, "⚠️ 未找到权限描述文本,无法进行相对定位 (失败次数: $textSearchFailCount)" ) safeRecycleNode(rootNode) // 🔥 优化:连续失败3次后立即放弃文本定位方案 if (textSearchFailCount >= 3) { Log.w(TAG, "⚠️ 文本搜索连续失败${textSearchFailCount}次,立即切换到智能检测策略") // 通过抛出特殊异常通知调用方切换策略 throw Exception("TEXT_SEARCH_FAILED_REPEATEDLY") } return false } // 获取文本节点的屏幕坐标 val textBounds = android.graphics.Rect() foundTextNode.getBoundsInScreen(textBounds) if (textBounds.isEmpty) { Log.w(TAG, "⚠️ 文本节点边界为空") safeRecycleNode(foundTextNode) safeRecycleNode(rootNode) return false } Log.i( TAG, "📍 文本节点位置: left=${textBounds.left}, top=${textBounds.top}, right=${textBounds.right}, bottom=${textBounds.bottom}" ) // 🔥 核心算法:基于文本位置和屏幕宽度计算开关的相对位置 // 根据日志分析和用户反馈,开关X坐标应该使用屏幕宽度减去固定值 // 获取屏幕宽度 val displayMetrics = context.resources.displayMetrics val screenWidth = displayMetrics.widthPixels Log.i(TAG, "📏 屏幕信息: 宽度=${screenWidth}px, 文本右边界=${textBounds.right}px") // 尝试多个可能的开关位置(使用屏幕宽度计算X坐标) val possiblePositions = listOf( // 主要位置:屏幕宽度减去100-180之间的值,Y坐标基于文本上方 Pair(screenWidth - 150, textBounds.top - 110), Pair(screenWidth - 160, textBounds.top - 120), Pair(screenWidth - 140, textBounds.top - 100), // 备用位置:调整Y坐标 Pair(screenWidth - 130, textBounds.top - 90), Pair(screenWidth - 110, textBounds.top - 70), // 边界位置:100和180的边界值 Pair(screenWidth - 120, textBounds.top - 80), Pair(screenWidth - 170, textBounds.top - 130), //这三个是为了适配google模拟器1080*2400 sdk36 Pair(screenWidth - 70, textBounds.top - 180), Pair(screenWidth - 70, textBounds.top - 200), Pair(screenWidth - 70, textBounds.top - 210), ) // 记录点击前的页面包名 val beforeClickPackage = service.rootInActiveWindow?.packageName?.toString() ?: "" for (i in possiblePositions.indices) { val (x, y) = possiblePositions[i] // 确保坐标有效 if (x <= 0 || y <= 0) { Log.w(TAG, "⚠️ 计算出的坐标无效: ($x, $y)") continue } Log.i(TAG, "🎯 尝试相对定位点击 ${i + 1}/${possiblePositions.size}: ($x, $y)") // 执行坐标点击 val clickSuccess = performCoordinateClick(x.toFloat(), y.toFloat()) if (!clickSuccess) { Log.w(TAG, "⚠️ 相对定位点击 ${i + 1} 执行失败") continue } // 等待点击生效 delay(200) // 检查权限状态 if (hasWriteSettingsPermission()) { Log.i(TAG, "🎉 相对定位点击 ${i + 1} 成功获取权限!") performBackNavigation() performBackNavigation() safeRecycleNode(foundTextNode) safeRecycleNode(rootNode) onWriteSettingsPermissionGranted() return true } // 检查是否页面跳转了 val currentPackage = service.rootInActiveWindow?.packageName?.toString() ?: "" if (checkIfPageChanged(beforeClickPackage, currentPackage)) { Log.w(TAG, "⚠️ 相对定位点击 ${i + 1} 导致页面跳转,执行返回") performBackNavigation() delay(500) // 等待返回 } Log.d(TAG, "🔍 相对定位点击 ${i + 1} 未获取权限,继续尝试下一个位置") } Log.w(TAG, "❌ 所有相对定位点击都未成功获取权限") safeRecycleNode(foundTextNode) safeRecycleNode(rootNode) return false } catch (e: Exception) { Log.e(TAG, "❌ 基于文本节点的定位方案执行失败", e) return false } } /** * 🔥 新增:递归查找包含指定文本的节点 */ private fun findNodeByTextRecursive( node: AccessibilityNodeInfo?, targetText: String ): AccessibilityNodeInfo? { if (node == null) return null try { // 检查当前节点的文本 val nodeText = node.text?.toString() if (!nodeText.isNullOrEmpty() && nodeText.contains(targetText, ignoreCase = true)) { Log.d(TAG, "🔍 找到匹配文本: '$nodeText'") return node } // 检查内容描述 val contentDesc = node.contentDescription?.toString() if (!contentDesc.isNullOrEmpty() && contentDesc.contains( targetText, ignoreCase = true ) ) { Log.d(TAG, "🔍 找到匹配内容描述: '$contentDesc'") return node } // 递归检查子节点 for (i in 0 until node.childCount) { val child = node.getChild(i) val result = findNodeByTextRecursive(child, targetText) if (result != null) { return result } safeRecycleNode(child) } } catch (e: Exception) { Log.e(TAG, "❌ 递归查找文本节点失败", e) } return null } /** * 执行单个坐标点击 */ private suspend fun performCoordinateClick(x: Float, y: Float): Boolean { return try { Log.i(TAG, "🖱️ 执行坐标点击: ($x, $y)") // 创建手势路径 val path = android.graphics.Path() path.moveTo(x, y) // 构建手势描述 val gestureBuilder = android.accessibilityservice.GestureDescription.Builder() val strokeDescription = android.accessibilityservice.GestureDescription.StrokeDescription(path, 0, 100) gestureBuilder.addStroke(strokeDescription) val gesture = gestureBuilder.build() // 使用协程来处理手势完成回调 var gestureCompleted = false var gestureSuccess = false val success = service.dispatchGesture( gesture, object : android.accessibilityservice.AccessibilityService.GestureResultCallback() { override fun onCompleted(gestureDescription: android.accessibilityservice.GestureDescription?) { super.onCompleted(gestureDescription) Log.i(TAG, "✅ 坐标点击手势执行完成") gestureCompleted = true gestureSuccess = true } override fun onCancelled(gestureDescription: android.accessibilityservice.GestureDescription?) { super.onCancelled(gestureDescription) Log.w(TAG, "⚠️ 坐标点击手势被取消") gestureCompleted = true gestureSuccess = false } }, null ) if (!success) { Log.w(TAG, "⚠️ 发送坐标点击手势失败") return false } // 等待手势完成,最多等待1秒 var waitTime = 0 while (!gestureCompleted && waitTime < 1000) { delay(50) waitTime += 50 } gestureSuccess } catch (e: Exception) { Log.e(TAG, "❌ 执行坐标点击失败", e) false } } }