Files
and-bak/app/src/main/java/com/hikoncont/manager/InputController.kt
wdvipa cdc4606574 fix: 修复启动时三个应用层报错问题
- MediaProjectionManager初始化提前到onCreate开头,避免多个return路径跳过初始化导致后续null
- InputController剪贴板访问添加SecurityException防护,非前台时安全降级而非崩溃
- HuaweiAuthorizationHandler剪贴板访问添加异常捕获,防止ClipboardService拒绝访问
- SocketIO连接错误区分瞬态错误(xhr poll/timeout)和持久错误,瞬态错误降级为WARN
- SocketIO connect方法添加URL格式验证和空值检查
2026-02-15 15:40:55 +08:00

467 lines
15 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.hikoncont.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<KeyButton> {
val buttons = mutableListOf<KeyButton>()
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)
}