446 lines
15 KiB
Kotlin
446 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
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 输入文本 - 智能输入策略
|
|||
|
|
*/
|
|||
|
|
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. 将文本复制到剪贴板
|
|||
|
|
val clipData = ClipData.newPlainText("remote_input", text)
|
|||
|
|
clipboardManager.setPrimaryClip(clipData)
|
|||
|
|
|
|||
|
|
// 2. 模拟粘贴操作
|
|||
|
|
val focusedNode = findFocusedInputNode()
|
|||
|
|
focusedNode?.let { node ->
|
|||
|
|
val result = node.performAction(AccessibilityNodeInfo.ACTION_PASTE)
|
|||
|
|
node.recycle()
|
|||
|
|
result
|
|||
|
|
} ?: false
|
|||
|
|
|
|||
|
|
} catch (e: Exception) {
|
|||
|
|
Log.w(TAG, "剪贴板粘贴失败", 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
|
|||
|
|
|
|||
|
|
// 将新文本复制到剪贴板
|
|||
|
|
val clipData = ClipData.newPlainText("remote_append", newText)
|
|||
|
|
clipboardManager.setPrimaryClip(clipData)
|
|||
|
|
|
|||
|
|
// 先清空再粘贴
|
|||
|
|
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, "剪贴板追加失败", 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)
|
|||
|
|
}
|