Files
android/app/src/main/java/com/hikoncont/service/modules/WriteSettingsPermissionManager.kt

4060 lines
168 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<AccessibilityNodeInfo>()
// 错误控件记录 - 记录已经尝试过但失败的控件,避免重复点击
private val failedControlsHistory = mutableSetOf<String>()
/**
* 检查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<AccessibilityNodeInfo>()
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<AccessibilityNodeInfo>()
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<AccessibilityNodeInfo>, 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<AccessibilityNodeInfo>, 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<String>, 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<String>, 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<String>, 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<AccessibilityNodeInfo>()
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<AccessibilityNodeInfo>, 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<AccessibilityNodeInfo>()
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<AccessibilityNodeInfo>()
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<AccessibilityNodeInfo>()
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<AccessibilityNodeInfo>) {
nodes.forEach { node ->
safeRecycleNode(node)
}
}
/**
* 查找所有满足条件的节点
*/
private fun findAllNodes(
rootNode: AccessibilityNodeInfo, predicate: (AccessibilityNodeInfo) -> Boolean
): List<AccessibilityNodeInfo> {
val result = mutableListOf<AccessibilityNodeInfo>()
findAllNodesRecursive(rootNode, predicate, result)
return result
}
/**
* 递归查找节点
*/
private fun findAllNodesRecursive(
node: AccessibilityNodeInfo,
predicate: (AccessibilityNodeInfo) -> Boolean,
result: MutableList<AccessibilityNodeInfo>
) {
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
}
// ✅ 修改优先点击右侧的SwitchX坐标更大的
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<AccessibilityNodeInfo> {
val switches = mutableListOf<AccessibilityNodeInfo>()
try {
val queue: ArrayDeque<AccessibilityNodeInfo> = 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<AccessibilityNodeInfo> = 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
}
}
}