- MediaProjectionManager初始化提前到onCreate开头,避免多个return路径跳过初始化导致后续null - InputController剪贴板访问添加SecurityException防护,非前台时安全降级而非崩溃 - HuaweiAuthorizationHandler剪贴板访问添加异常捕获,防止ClipboardService拒绝访问 - SocketIO连接错误区分瞬态错误(xhr poll/timeout)和持久错误,瞬态错误降级为WARN - SocketIO connect方法添加URL格式验证和空值检查
467 lines
15 KiB
Kotlin
467 lines
15 KiB
Kotlin
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)
|
||
} |