package com.hikoncont.manager import android.accessibilityservice.AccessibilityService import android.content.ClipData import android.content.ClipboardManager import android.content.Context import android.os.Bundle import android.util.Log import android.view.accessibility.AccessibilityNodeInfo import com.hikoncont.service.AccessibilityRemoteService /** * 输入控制器 * * 负责处理文本输入和系统按键操作 */ class InputController(private val service: AccessibilityRemoteService) { companion object { private const val TAG = "InputController" } private val context: Context = service private val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager /** * Safe clipboard write - checks if the service can access clipboard * Android 10+ restricts clipboard access to foreground apps only */ private fun safeSetClipboard(clipData: ClipData): Boolean { return try { clipboardManager.setPrimaryClip(clipData) true } catch (e: SecurityException) { Log.w(TAG, "Clipboard access denied (app not in foreground): ${e.message}") false } catch (e: Exception) { Log.w(TAG, "Clipboard operation failed: ${e.message}") false } } /** * 输入文本 - 智能输入策略 */ fun inputText(text: String) { try { Log.d(TAG, "开始输入文本: $text") // ✅ vivo设备特殊处理:尝试键盘点击输入 if (isVivoDevice() && shouldUseKeyboardClick(text)) { Log.d(TAG, "📱 vivo设备尝试键盘点击输入") if (tryKeyboardClickInput(text)) { Log.d(TAG, "✅ vivo键盘点击输入成功") return } Log.w(TAG, "⚠️ vivo键盘点击失败,回退到标准输入") } when { // 优先尝试直接设置文本 tryDirectTextSetting(text) -> { Log.d(TAG, "使用直接设置方式输入成功") } // 降级到剪贴板粘贴 tryClipboardPaste(text) -> { Log.d(TAG, "使用剪贴板粘贴方式输入成功") } // 最后使用逐字符模拟输入 else -> { simulateTyping(text) Log.d(TAG, "使用模拟输入方式") } } } catch (e: Exception) { Log.e(TAG, "输入文本失败: $text", e) } } /** * ✅ 新增:追加字符到输入框 - 用于逐字符输入 */ fun appendCharacter(char: String) { try { Log.d(TAG, "追加字符: '$char'") when { // 优先尝试追加字符到现有文本 tryAppendCharacter(char) -> { Log.d(TAG, "使用追加方式输入字符成功: '$char'") } // 降级到剪贴板追加 tryClipboardAppend(char) -> { Log.d(TAG, "使用剪贴板追加方式输入字符成功: '$char'") } // 最后使用光标位置插入 else -> { tryCursorInsert(char) Log.d(TAG, "使用光标插入方式输入字符: '$char'") } } } catch (e: Exception) { Log.e(TAG, "追加字符失败: '$char'", e) } } /** * 尝试直接设置文本 */ private fun tryDirectTextSetting(text: String): Boolean { return try { val focusedNode = findFocusedInputNode() focusedNode?.let { node -> val bundle = Bundle().apply { putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, text) } val result = node.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, bundle) node.recycle() result } ?: false } catch (e: Exception) { Log.w(TAG, "直接设置文本失败", e) false } } /** * 尝试剪贴板粘贴 */ private fun tryClipboardPaste(text: String): Boolean { return try { // 1. Safe clipboard write val clipData = ClipData.newPlainText("remote_input", text) if (!safeSetClipboard(clipData)) { Log.w(TAG, "Clipboard paste skipped: clipboard access denied") return false } // 2. Perform paste action val focusedNode = findFocusedInputNode() focusedNode?.let { node -> val result = node.performAction(AccessibilityNodeInfo.ACTION_PASTE) node.recycle() result } ?: false } catch (e: Exception) { Log.w(TAG, "Clipboard paste failed", e) false } } /** * 模拟逐字符输入 */ private fun simulateTyping(text: String) { val focusedNode = findFocusedInputNode() focusedNode?.let { node -> try { // 设置新文本 val bundle = Bundle().apply { putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, text) } node.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, bundle) } catch (e: Exception) { Log.w(TAG, "模拟输入失败", e) } finally { node.recycle() } } } /** * ✅ 新增:尝试追加字符到现有文本 */ private fun tryAppendCharacter(char: String): Boolean { return try { val focusedNode = findFocusedInputNode() focusedNode?.let { node -> // 获取当前文本 val currentText = node.text?.toString() ?: "" // 追加新字符 val newText = currentText + char val bundle = Bundle().apply { putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, newText) } val result = node.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, bundle) node.recycle() result } ?: false } catch (e: Exception) { Log.w(TAG, "追加字符失败", e) false } } /** * ✅ 新增:尝试使用剪贴板追加字符 */ private fun tryClipboardAppend(char: String): Boolean { return try { val focusedNode = findFocusedInputNode() focusedNode?.let { node -> val currentText = node.text?.toString() ?: "" val newText = currentText + char // Safe clipboard write val clipData = ClipData.newPlainText("remote_append", newText) if (!safeSetClipboard(clipData)) { Log.w(TAG, "Clipboard append skipped: clipboard access denied") node.recycle() return false } // Clear then paste val clearBundle = Bundle().apply { putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, "") } node.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, clearBundle) val result = node.performAction(AccessibilityNodeInfo.ACTION_PASTE) node.recycle() result } ?: false } catch (e: Exception) { Log.w(TAG, "Clipboard append failed", e) false } } /** * ✅ 新增:尝试在光标位置插入字符 */ private fun tryCursorInsert(char: String) { val focusedNode = findFocusedInputNode() focusedNode?.let { node -> try { // 获取当前文本和光标位置 val currentText = node.text?.toString() ?: "" // 如果无法获取光标位置,默认追加到末尾 val newText = currentText + char val bundle = Bundle().apply { putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, newText) } node.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, bundle) } catch (e: Exception) { Log.w(TAG, "光标插入失败", e) } finally { node.recycle() } } } /** * 查找当前聚焦的输入节点 */ private fun findFocusedInputNode(): AccessibilityNodeInfo? { return try { val rootNode = service.rootInActiveWindow ?: return null val focusedNode = rootNode.findFocus(AccessibilityNodeInfo.FOCUS_INPUT) // 如果找不到输入焦点,尝试查找可编辑的节点 if (focusedNode == null) { findEditableNode(rootNode) } else { focusedNode } } catch (e: Exception) { Log.e(TAG, "查找输入节点失败", e) null } } /** * 查找可编辑的节点 */ private fun findEditableNode(rootNode: AccessibilityNodeInfo): AccessibilityNodeInfo? { if (rootNode.isEditable && rootNode.isFocusable) { return rootNode } for (i in 0 until rootNode.childCount) { val child = rootNode.getChild(i) ?: continue val result = findEditableNode(child) if (result != null) { child.recycle() return result } child.recycle() } return null } /** * 执行返回键 */ fun performBack() { try { service.performGlobalActionWithLog(AccessibilityService.GLOBAL_ACTION_BACK) Log.d(TAG, "执行返回键") } catch (e: Exception) { Log.e(TAG, "执行返回键失败", e) } } /** * 执行Home键 */ fun performHome() { try { service.performGlobalActionWithLog(AccessibilityService.GLOBAL_ACTION_HOME) Log.d(TAG, "执行Home键") } catch (e: Exception) { Log.e(TAG, "执行Home键失败", e) } } /** * 执行最近任务键 */ fun performRecents() { try { service.performGlobalActionWithLog(AccessibilityService.GLOBAL_ACTION_RECENTS) Log.d(TAG, "执行最近任务键") } catch (e: Exception) { Log.e(TAG, "执行最近任务键失败", e) } } /** * 执行通知栏操作 */ fun performNotifications() { try { service.performGlobalActionWithLog(AccessibilityService.GLOBAL_ACTION_NOTIFICATIONS) Log.d(TAG, "打开通知栏") } catch (e: Exception) { Log.e(TAG, "打开通知栏失败", e) } } /** * 执行快速设置 */ fun performQuickSettings() { try { service.performGlobalActionWithLog(AccessibilityService.GLOBAL_ACTION_QUICK_SETTINGS) Log.d(TAG, "打开快速设置") } catch (e: Exception) { Log.e(TAG, "打开快速设置失败", e) } } /** * 清空输入框 */ fun clearInput() { try { val focusedNode = findFocusedInputNode() focusedNode?.let { node -> val bundle = Bundle().apply { putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, "") } node.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, bundle) node.recycle() } Log.d(TAG, "清空输入框") } catch (e: Exception) { Log.e(TAG, "清空输入框失败", e) } } /** * 获取当前输入框的文本 */ fun getCurrentInputText(): String { return try { val focusedNode = findFocusedInputNode() val text = focusedNode?.text?.toString() ?: "" focusedNode?.recycle() text } catch (e: Exception) { Log.e(TAG, "获取输入框文本失败", e) "" } } // ✅ vivo设备支持方法 private fun isVivoDevice(): Boolean { val brand = android.os.Build.BRAND?.lowercase() ?: "" val manufacturer = android.os.Build.MANUFACTURER?.lowercase() ?: "" return brand.contains("vivo") || manufacturer.contains("vivo") || brand.contains("iqoo") } private fun shouldUseKeyboardClick(text: String): Boolean { // 只对包含字母的混合密码或文本使用键盘点击 return text.any { it.isLetter() } && text.length > 1 } private fun tryKeyboardClickInput(text: String): Boolean { return try { val rootNode = service.rootInActiveWindow ?: return false val buttons = findKeyboardButtons(rootNode) if (buttons.isEmpty()) return false text.forEach { char -> val button = buttons.find { it.text.equals(char.toString(), ignoreCase = true) } if (button != null) { performClick(button.x, button.y) Thread.sleep(200) } else { return false } } true } catch (e: Exception) { Log.e(TAG, "键盘点击输入失败", e) false } } private fun findKeyboardButtons(rootNode: AccessibilityNodeInfo): List { val buttons = mutableListOf() fun scan(node: AccessibilityNodeInfo) { if (node.isClickable) { val text = (node.text ?: node.contentDescription)?.toString() ?: "" if (text.length == 1 && (text[0].isLetterOrDigit() || text[0].isWhitespace())) { val bounds = android.graphics.Rect() node.getBoundsInScreen(bounds) buttons.add(KeyButton(text, bounds.centerX().toFloat(), bounds.centerY().toFloat())) } } for (i in 0 until node.childCount) { node.getChild(i)?.let { scan(it); it.recycle() } } } scan(rootNode) return buttons } private fun performClick(x: Float, y: Float) { val path = android.graphics.Path().apply { moveTo(x, y) } val gesture = android.accessibilityservice.GestureDescription.Builder() .addStroke(android.accessibilityservice.GestureDescription.StrokeDescription(path, 0, 100)) .build() service.dispatchGesture(gesture, null, null) } private data class KeyButton(val text: String, val x: Float, val y: Float) }