fix: 修复启动时三个应用层报错问题
- MediaProjectionManager初始化提前到onCreate开头,避免多个return路径跳过初始化导致后续null - InputController剪贴板访问添加SecurityException防护,非前台时安全降级而非崩溃 - HuaweiAuthorizationHandler剪贴板访问添加异常捕获,防止ClipboardService拒绝访问 - SocketIO连接错误区分瞬态错误(xhr poll/timeout)和持久错误,瞬态错误降级为WARN - SocketIO connect方法添加URL格式验证和空值检查
This commit is contained in:
@@ -404,7 +404,16 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
registerReceiver(combinedBroadcastReceiver, filter)
|
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()
|
recordAppLaunch()
|
||||||
|
|
||||||
// ✅ 新增:检查是否为无闪现后台唤起
|
// ✅ 新增:检查是否为无闪现后台唤起
|
||||||
@@ -673,10 +682,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
handleGalleryPermissionRequest()
|
handleGalleryPermissionRequest()
|
||||||
handleMicrophonePermissionRequest()
|
handleMicrophonePermissionRequest()
|
||||||
|
|
||||||
// 初始化MediaProjectionManager
|
// MediaProjectionManager already initialized at the top of onCreate
|
||||||
mediaProjectionManager =
|
|
||||||
getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
|
|
||||||
|
|
||||||
|
|
||||||
// 处理Intent
|
// 处理Intent
|
||||||
handleIntentAndPermissions(intent)
|
handleIntentAndPermissions(intent)
|
||||||
|
|||||||
@@ -23,6 +23,23 @@ class InputController(private val service: AccessibilityRemoteService) {
|
|||||||
private val context: Context = service
|
private val context: Context = service
|
||||||
private val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
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 {
|
private fun tryClipboardPaste(text: String): Boolean {
|
||||||
return try {
|
return try {
|
||||||
// 1. 将文本复制到剪贴板
|
// 1. Safe clipboard write
|
||||||
val clipData = ClipData.newPlainText("remote_input", text)
|
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()
|
val focusedNode = findFocusedInputNode()
|
||||||
focusedNode?.let { node ->
|
focusedNode?.let { node ->
|
||||||
val result = node.performAction(AccessibilityNodeInfo.ACTION_PASTE)
|
val result = node.performAction(AccessibilityNodeInfo.ACTION_PASTE)
|
||||||
@@ -131,7 +151,7 @@ class InputController(private val service: AccessibilityRemoteService) {
|
|||||||
} ?: false
|
} ?: false
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(TAG, "剪贴板粘贴失败", e)
|
Log.w(TAG, "Clipboard paste failed", e)
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -189,29 +209,30 @@ class InputController(private val service: AccessibilityRemoteService) {
|
|||||||
return try {
|
return try {
|
||||||
val focusedNode = findFocusedInputNode()
|
val focusedNode = findFocusedInputNode()
|
||||||
focusedNode?.let { node ->
|
focusedNode?.let { node ->
|
||||||
// 获取当前文本
|
|
||||||
val currentText = node.text?.toString() ?: ""
|
val currentText = node.text?.toString() ?: ""
|
||||||
// 追加新字符
|
|
||||||
val newText = currentText + char
|
val newText = currentText + char
|
||||||
|
|
||||||
// 将新文本复制到剪贴板
|
// Safe clipboard write
|
||||||
val clipData = ClipData.newPlainText("remote_append", newText)
|
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 {
|
val clearBundle = Bundle().apply {
|
||||||
putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, "")
|
putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, "")
|
||||||
}
|
}
|
||||||
node.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, clearBundle)
|
node.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, clearBundle)
|
||||||
|
|
||||||
// 粘贴新内容
|
|
||||||
val result = node.performAction(AccessibilityNodeInfo.ACTION_PASTE)
|
val result = node.performAction(AccessibilityNodeInfo.ACTION_PASTE)
|
||||||
node.recycle()
|
node.recycle()
|
||||||
result
|
result
|
||||||
} ?: false
|
} ?: false
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(TAG, "剪贴板追加失败", e)
|
Log.w(TAG, "Clipboard append failed", e)
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -271,22 +271,34 @@ class SocketIOManager(private val service: AccessibilityRemoteService) {
|
|||||||
private val maxGalleryImageSize = 2 * 1024 * 1024 // 2MB
|
private val maxGalleryImageSize = 2 * 1024 * 1024 // 2MB
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 连接到Socket.IO v4服务器
|
* Connect to Socket.IO v4 server
|
||||||
*/
|
*/
|
||||||
suspend fun connect(serverUrl: String) {
|
suspend fun connect(serverUrl: String) {
|
||||||
try {
|
try {
|
||||||
this.serverUrl = serverUrl // 保存服务器地址
|
this.serverUrl = serverUrl
|
||||||
Log.i(TAG, "🚀 连接到Socket.IO v4服务器: $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)
|
val savedUrl = com.hikoncont.util.ConfigWriter.getCurrentServerUrl(service)
|
||||||
if (savedUrl != null && savedUrl != serverUrl) {
|
if (savedUrl != null && savedUrl != serverUrl) {
|
||||||
Log.w(TAG, "⚠️ 配置不一致!当前连接地址: $serverUrl, 配置文件地址: $savedUrl")
|
Log.w(TAG, "Config mismatch! Current: $serverUrl, Config: $savedUrl, using config URL")
|
||||||
Log.i(TAG, "🔄 使用配置文件中的地址重新连接: $savedUrl")
|
|
||||||
connect(savedUrl)
|
connect(savedUrl)
|
||||||
return
|
return
|
||||||
} else if (savedUrl == serverUrl) {
|
|
||||||
Log.i(TAG, "✅ 配置验证通过,地址一致: $serverUrl")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ Socket.IO v4客户端配置 - 增强稳定性优化 + 随机化重连避免雪崩
|
// ✅ Socket.IO v4客户端配置 - 增强稳定性优化 + 随机化重连避免雪崩
|
||||||
@@ -425,8 +437,21 @@ class SocketIOManager(private val service: AccessibilityRemoteService) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
socket.on(Socket.EVENT_CONNECT_ERROR) { args ->
|
socket.on(Socket.EVENT_CONNECT_ERROR) { args ->
|
||||||
val error = if (args.isNotEmpty()) args[0].toString() else "unknown"
|
val error = if (args.isNotEmpty()) args[0] else null
|
||||||
Log.e(TAG, "Socket.IO v4 连接错误: $error")
|
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++
|
connectionFailureCount++
|
||||||
updateNetworkQualityScore(false, "connect_error", 0)
|
updateNetworkQualityScore(false, "connect_error", 0)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -958,14 +958,22 @@ class HuaweiAuthorizationHandler(
|
|||||||
* 尝试输入搜索文本(使用剪贴板粘贴)
|
* 尝试输入搜索文本(使用剪贴板粘贴)
|
||||||
*/
|
*/
|
||||||
private fun tryInputSearchText(searchText: String = "悬浮窗"): Boolean {
|
private fun tryInputSearchText(searchText: String = "悬浮窗"): Boolean {
|
||||||
Log.d(TAG, "🔍 尝试通过剪贴板粘贴搜索文本")
|
Log.d(TAG, "尝试通过剪贴板粘贴搜索文本")
|
||||||
|
|
||||||
try {
|
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 clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager
|
||||||
val clipData = android.content.ClipData.newPlainText("search_text", searchText)
|
val clipData = android.content.ClipData.newPlainText("search_text", searchText)
|
||||||
clipboardManager.setPrimaryClip(clipData)
|
try {
|
||||||
Log.d(TAG, "✅ 已将'${searchText}'复制到剪贴板")
|
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. 等待一下确保剪贴板操作完成
|
// 2. 等待一下确保剪贴板操作完成
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user