4060 lines
168 KiB
Kotlin
4060 lines
168 KiB
Kotlin
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
|
||
}
|
||
|
||
// ✅ 修改:优先点击右侧的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<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
|
||
}
|
||
}
|
||
}
|