diff --git a/app/src/main/java/com/kevinluo/autoglm/MainActivity.kt b/app/src/main/java/com/kevinluo/autoglm/MainActivity.kt index 2b5f206..2b5da40 100644 --- a/app/src/main/java/com/kevinluo/autoglm/MainActivity.kt +++ b/app/src/main/java/com/kevinluo/autoglm/MainActivity.kt @@ -173,6 +173,15 @@ class MainActivity : BaseActivity() { override fun onResume() { super.onResume() checkOverlayPermission() + checkShizukuState() + } + + private fun checkShizukuState() { + if (Shizuku.pingBinder()) { + checkShizukuPermission() + } else { + viewModel.updateShizukuStatus(ShizukuStatus.NOT_RUNNING) + } } override fun onDestroy() { diff --git a/app/src/main/java/com/kevinluo/autoglm/action/ActionHandler.kt b/app/src/main/java/com/kevinluo/autoglm/action/ActionHandler.kt index 7c3cb23..9380d29 100644 --- a/app/src/main/java/com/kevinluo/autoglm/action/ActionHandler.kt +++ b/app/src/main/java/com/kevinluo/autoglm/action/ActionHandler.kt @@ -96,6 +96,10 @@ class ActionHandler( floatingWindowProvider?.invoke()?.show() } + private var currentLanguage: String = "cn" + private val isEn: Boolean + get() = currentLanguage.lowercase().let { it == "en" || it == "english" } + /** * Executes an agent action on the device. * @@ -105,10 +109,16 @@ class ActionHandler( * @param action The action to execute * @param screenWidth Current screen width in pixels * @param screenHeight Current screen height in pixels + * @param language Language code: "cn" for Chinese, "en" for English * @return The result of the action execution - * */ - suspend fun execute(action: AgentAction, screenWidth: Int, screenHeight: Int): ActionResult { + suspend fun execute( + action: AgentAction, + screenWidth: Int, + screenHeight: Int, + language: String = "cn", + ): ActionResult { + this.currentLanguage = language Logger.logAction(action::class.simpleName ?: "Unknown", "Executing on ${screenWidth}x$screenHeight") return try { @@ -160,7 +170,7 @@ class ActionHandler( if (action.message != null) { val confirmed = confirmationCallback?.onConfirmationRequired(action.message) ?: true if (!confirmed) { - return ActionResult(true, false, "点击已被用户取消") + return ActionResult(true, false, if (isEn) "Tap cancelled by user" else "点击已被用户取消") } } @@ -179,9 +189,9 @@ class ActionHandler( val result = deviceExecutor.tap(absX, absY) if (isDeviceExecutorError(result)) { Logger.w(TAG, "Tap command failed: $result") - ActionResult(false, false, "点击失败: $result") + ActionResult(false, false, if (isEn) "Tap failed: $result" else "点击失败: $result") } else { - ActionResult(true, false, "点击 ($absX, $absY)") + ActionResult(true, false, if (isEn) "Tap ($absX, $absY)" else "点击 ($absX, $absY)") } } finally { // Always show floating window after tap, even if tap fails @@ -243,9 +253,9 @@ class ActionHandler( if (isDeviceExecutorError(result)) { Logger.w(TAG, "Swipe command failed: $result") - ActionResult(false, false, "滑动失败: $result") + ActionResult(false, false, if (isEn) "Swipe failed: $result" else "滑动失败: $result") } else { - ActionResult(true, false, "滑动 从($startAbsX, $startAbsY) 到($endAbsX, $endAbsY)") + ActionResult(true, false, if (isEn) "Swipe from ($startAbsX, $startAbsY) to ($endAbsX, $endAbsY)" else "滑动 从($startAbsX, $startAbsY) 到($endAbsX, $endAbsY)") } } finally { // Always show floating window after swipe, even if swipe fails @@ -292,7 +302,7 @@ class ActionHandler( delay(200) val result = textInputManager.typeText(action.text) - ActionResult(result.success, false, "输入名称: ${action.text}") + return ActionResult(result.success, false, if (isEn) "Type Name: ${action.text}" else "输入名称: ${action.text}") } finally { // Always show floating window after typing, even if typing fails showFloatingWindow() @@ -322,20 +332,20 @@ class ActionHandler( return if (packageName != null) { Logger.i(TAG, "Launching package: $packageName") - val launchResult = deviceExecutor.launchApp(packageName) + val startResult = deviceExecutor.launchApp(packageName) - if (isDeviceExecutorError(launchResult)) { - Logger.w(TAG, "Launch failed for $packageName: $launchResult") - // Launch failed - instruct model to find app icon on screen + if (isDeviceExecutorError(startResult)) { + Logger.w(TAG, "Failed to start app '$packageName': $startResult, instructing model to find icon on screen") + // Press Home first to go to home screen deviceExecutor.pressKey(DeviceExecutor.KEYCODE_HOME) ActionResult( // Operation itself succeeded, just app not found success = true, shouldFinish = false, - message = "启动应用'$packageName'失败,已返回主屏幕。请在主屏幕或应用列表中查找并点击'${action.app}'应用图标来启动它。", + message = if (isEn) "Failed to launch app '$packageName', returned to home screen. Please find and tap the '${action.app}' app icon on the home screen or app list to launch it." else "启动应用'$packageName'失败,已返回主屏幕。请在主屏幕或应用列表中查找并点击'${action.app}'应用图标来启动它。", ) } else { - ActionResult(true, false, "启动应用: $packageName") + ActionResult(true, false, if (isEn) "Launch: $packageName" else "启动应用: $packageName") } } else { // Package not found - instruct model to find app icon on screen @@ -345,7 +355,7 @@ class ActionHandler( ActionResult( success = true, shouldFinish = false, - message = "找不到应用包名'${action.app}',已返回主屏幕。请在主屏幕或应用列表中查找并点击'${action.app}'应用图标来启动它。如果主屏幕没有,请上滑打开应用列表查找。", + message = if (isEn) "Cannot find app package for '${action.app}', returned to home screen. Please find and tap the '${action.app}' app icon on the home screen or app list to launch it. If not on home screen, swipe up to open app drawer." else "找不到应用包名'${action.app}',已返回主屏幕。请在主屏幕或应用列表中查找并点击'${action.app}'应用图标来启动它。如果主屏幕没有,请上滑打开应用列表查找。", ) } } @@ -360,17 +370,17 @@ class ActionHandler( val apps = appResolver.getAllLaunchableApps() if (apps.isEmpty()) { - return ActionResult(true, false, "未找到已安装的应用") + return ActionResult(true, false, if (isEn) "No installed apps found" else "未找到已安装的应用") } // Format app list for display val appListStr = buildString { - appendLine("已安装的应用列表 (共${apps.size}个):") + appendLine(if (isEn) "Installed apps (${apps.size} total):" else "已安装的应用列表 (共${apps.size}个):") appendLine() apps.sortedBy { it.displayName.lowercase() }.forEach { app -> appendLine("• ${app.displayName}") - appendLine(" 包名: ${app.packageName}") + appendLine(if (isEn) " Package: ${app.packageName}" else " 包名: ${app.packageName}") } } @@ -393,9 +403,9 @@ class ActionHandler( val result = deviceExecutor.pressKey(DeviceExecutor.KEYCODE_BACK) return if (isDeviceExecutorError(result)) { Logger.w(TAG, "Back key press failed: $result") - ActionResult(false, false, "返回键失败: $result") + ActionResult(false, false, if (isEn) "Back key failed: $result" else "返回键失败: $result") } else { - ActionResult(true, false, "返回") + ActionResult(true, false, if (isEn) "Back" else "返回") } } @@ -406,9 +416,9 @@ class ActionHandler( val result = deviceExecutor.pressKey(DeviceExecutor.KEYCODE_HOME) return if (isDeviceExecutorError(result)) { Logger.w(TAG, "Home key press failed: $result") - ActionResult(false, false, "主页键失败: $result") + ActionResult(false, false, if (isEn) "Home key failed: $result" else "主页键失败: $result") } else { - ActionResult(true, false, "主页") + ActionResult(true, false, if (isEn) "Home" else "主页") } } @@ -419,9 +429,9 @@ class ActionHandler( val result = deviceExecutor.pressKey(DeviceExecutor.KEYCODE_VOLUME_UP) return if (isDeviceExecutorError(result)) { Logger.w(TAG, "Volume up key press failed: $result") - ActionResult(false, false, "音量+键失败: $result") + ActionResult(false, false, if (isEn) "Volume+ key failed: $result" else "音量+键失败: $result") } else { - ActionResult(true, false, "音量+") + ActionResult(true, false, if (isEn) "Volume+" else "音量+") } } @@ -432,9 +442,9 @@ class ActionHandler( val result = deviceExecutor.pressKey(DeviceExecutor.KEYCODE_VOLUME_DOWN) return if (isDeviceExecutorError(result)) { Logger.w(TAG, "Volume down key press failed: $result") - ActionResult(false, false, "音量-键失败: $result") + ActionResult(false, false, if (isEn) "Volume- key failed: $result" else "音量-键失败: $result") } else { - ActionResult(true, false, "音量-") + ActionResult(true, false, if (isEn) "Volume-" else "音量-") } } @@ -445,9 +455,9 @@ class ActionHandler( val result = deviceExecutor.pressKey(DeviceExecutor.KEYCODE_POWER) return if (isDeviceExecutorError(result)) { Logger.w(TAG, "Power key press failed: $result") - ActionResult(false, false, "电源键失败: $result") + ActionResult(false, false, if (isEn) "Power key failed: $result" else "电源键失败: $result") } else { - ActionResult(true, false, "电源键") + ActionResult(true, false, if (isEn) "Power" else "电源键") } } @@ -481,9 +491,9 @@ class ActionHandler( if (isDeviceExecutorError(result)) { Logger.w(TAG, "Long press command failed: $result") - ActionResult(false, false, "长按失败: $result") + ActionResult(false, false, if (isEn) "Long press failed: $result" else "长按失败: $result") } else { - ActionResult(true, false, "长按 ($absX, $absY) ${action.durationMs}毫秒") + ActionResult(true, false, if (isEn) "Long press ($absX, $absY) ${action.durationMs}ms" else "长按 ($absX, $absY) ${action.durationMs}毫秒") } } finally { // Always show floating window after long press, even if it fails @@ -516,9 +526,9 @@ class ActionHandler( val result = deviceExecutor.doubleTap(absX, absY) if (isDeviceExecutorError(result)) { Logger.w(TAG, "Double tap command failed: $result") - ActionResult(false, false, "双击失败: $result") + ActionResult(false, false, if (isEn) "Double tap failed: $result" else "双击失败: $result") } else { - ActionResult(true, false, "双击 ($absX, $absY)") + ActionResult(true, false, if (isEn) "Double tap ($absX, $absY)" else "双击 ($absX, $absY)") } } finally { // Always show floating window after double tap, even if it fails @@ -532,7 +542,7 @@ class ActionHandler( private suspend fun executeWait(action: AgentAction.Wait): ActionResult { val durationMs = (action.durationSeconds * 1000).toLong() delay(durationMs) - return ActionResult(true, false, "等待 ${action.durationSeconds}秒") + return ActionResult(true, false, if (isEn) "Wait ${action.durationSeconds}s" else "等待 ${action.durationSeconds}秒") } /** @@ -540,7 +550,7 @@ class ActionHandler( */ private suspend fun executeTakeOver(action: AgentAction.TakeOver): ActionResult { confirmationCallback?.onTakeOverRequested(action.message) - return ActionResult(true, false, "请求手动接管: ${action.message}") + return ActionResult(true, false, if (isEn) "Request manual takeover: ${action.message}" else "请求手动接管: ${action.message}") } /** @@ -549,10 +559,11 @@ class ActionHandler( private suspend fun executeInteract(action: AgentAction.Interact): ActionResult { val selectedIndex = confirmationCallback?.onInteractionRequired(action.options) ?: -1 return if (selectedIndex >= 0) { - val selectedOption = action.options?.getOrNull(selectedIndex) ?: "选项 $selectedIndex" - ActionResult(true, false, "用户选择: $selectedOption") + val defaultOptionName = if (isEn) "Option $selectedIndex" else "选项 $selectedIndex" + val selectedOption = action.options?.getOrNull(selectedIndex) ?: defaultOptionName + ActionResult(true, false, if (isEn) "User selected: $selectedOption" else "用户选择: $selectedOption") } else { - ActionResult(true, false, "交互已取消") + ActionResult(true, false, if (isEn) "Interaction cancelled" else "交互已取消") } } @@ -561,7 +572,7 @@ class ActionHandler( */ private suspend fun executeNote(action: AgentAction.Note): ActionResult { // Note action just records the message, no device operation needed - return ActionResult(true, false, "备注: ${action.message}") + return ActionResult(true, false, if (isEn) "Note: ${action.message}" else "备注: ${action.message}") } /** @@ -569,7 +580,7 @@ class ActionHandler( */ private suspend fun executeCallApi(action: AgentAction.CallApi): ActionResult { // CallApi action is handled by the agent layer, not device operations - return ActionResult(true, false, "API调用: ${action.instruction}") + return ActionResult(true, false, if (isEn) "API Call: ${action.instruction}" else "API调用: ${action.instruction}") } /** diff --git a/app/src/main/java/com/kevinluo/autoglm/action/AgentAction.kt b/app/src/main/java/com/kevinluo/autoglm/action/AgentAction.kt index 8d0f767..7a9c87f 100644 --- a/app/src/main/java/com/kevinluo/autoglm/action/AgentAction.kt +++ b/app/src/main/java/com/kevinluo/autoglm/action/AgentAction.kt @@ -158,29 +158,33 @@ sealed class AgentAction { /** * Formats the action for display in UI. * + * @param language Language code: "cn" for Chinese, "en" for English * @return Human-readable description of the action */ - fun formatForDisplay(): String = when (this) { - is Tap -> "点击 ($x, $y)" - is Swipe -> "滑动 从($startX, $startY) 到($endX, $endY)" - is Type -> "输入: \"${text.take(30)}${if (text.length > 30) "..." else ""}\"" - is TypeName -> "输入名称: \"$text\"" - is Launch -> "启动: $app" - is ListApps -> "列出已安装应用" - is Back -> "返回" - is Home -> "主页" - is VolumeUp -> "音量+" - is VolumeDown -> "音量-" - is Power -> "电源键" - is LongPress -> "长按 ($x, $y)" - is DoubleTap -> "双击 ($x, $y)" - is Wait -> "等待 ${durationSeconds}秒" - is TakeOver -> "手动接管" - is Interact -> "用户交互" - is Note -> "备注: ${message.take(30)}${if (message.length > 30) "..." else ""}" - is CallApi -> "API调用" - is Finish -> "完成: ${message.take(30)}${if (message.length > 30) "..." else ""}" - is Batch -> "批量操作: ${steps.size}步 (间隔${delayMs}ms)" + fun formatForDisplay(language: String = "cn"): String { + val isEn = language.lowercase().let { it == "en" || it == "english" } + return when (this) { + is Tap -> if (isEn) "Tap ($x, $y)" else "点击 ($x, $y)" + is Swipe -> if (isEn) "Swipe from ($startX, $startY) to ($endX, $endY)" else "滑动 从($startX, $startY) 到($endX, $endY)" + is Type -> if (isEn) "Type: \"${text.take(30)}${if (text.length > 30) "..." else ""}\"" else "输入: \"${text.take(30)}${if (text.length > 30) "..." else ""}\"" + is TypeName -> if (isEn) "Type Name: \"$text\"" else "输入名称: \"$text\"" + is Launch -> if (isEn) "Launch: $app" else "启动: $app" + is ListApps -> if (isEn) "List installed apps" else "列出已安装应用" + is Back -> if (isEn) "Back" else "返回" + is Home -> if (isEn) "Home" else "主页" + is VolumeUp -> if (isEn) "Volume Up" else "音量+" + is VolumeDown -> if (isEn) "Volume Down" else "音量-" + is Power -> if (isEn) "Power" else "电源键" + is LongPress -> if (isEn) "Long press ($x, $y)" else "长按 ($x, $y)" + is DoubleTap -> if (isEn) "Double tap ($x, $y)" else "双击 ($x, $y)" + is Wait -> if (isEn) "Wait ${durationSeconds}s" else "等待 ${durationSeconds}秒" + is TakeOver -> if (isEn) "Manual takeover" else "手动接管" + is Interact -> if (isEn) "User interaction" else "用户交互" + is Note -> if (isEn) "Note: ${message.take(30)}${if (message.length > 30) "..." else ""}" else "备注: ${message.take(30)}${if (message.length > 30) "..." else ""}" + is CallApi -> if (isEn) "Call API" else "API调用" + is Finish -> if (isEn) "Finish: ${message.take(30)}${if (message.length > 30) "..." else ""}" else "完成: ${message.take(30)}${if (message.length > 30) "..." else ""}" + is Batch -> if (isEn) "Batch: ${steps.size} steps (${delayMs}ms)" else "批量操作: ${steps.size}步 (间隔${delayMs}ms)" + } } } diff --git a/app/src/main/java/com/kevinluo/autoglm/agent/PhoneAgent.kt b/app/src/main/java/com/kevinluo/autoglm/agent/PhoneAgent.kt index 68fc0c7..5815042 100644 --- a/app/src/main/java/com/kevinluo/autoglm/agent/PhoneAgent.kt +++ b/app/src/main/java/com/kevinluo/autoglm/agent/PhoneAgent.kt @@ -137,6 +137,18 @@ class PhoneAgent( CANCELLATION_MESSAGE } + /** + * Gets the pause message based on language setting. + * + * @return Localized pause message + */ + private fun getPauseMessage(): String = + if (config.language.lowercase() == "en" || config.language.lowercase() == "english") { + PAUSE_MESSAGE_EN + } else { + PAUSE_MESSAGE + } + /** * Sets the listener for agent events. * @@ -414,7 +426,7 @@ class PhoneAgent( ) } - val handledError = ErrorHandler.handleUnknownError("Task execution error", e) + val handledError = ErrorHandler.handleUnknownError("Task execution error", e, language = config.language) Logger.e(TAG, ErrorHandler.formatErrorForLog(handledError), e) if (!historyCompleted) { historyManager?.completeTask(false, handledError.userMessage) @@ -501,7 +513,7 @@ class PhoneAgent( finished = false, action = null, thinking = "", - message = PAUSE_MESSAGE, + message = getPauseMessage(), paused = true, ) } @@ -534,7 +546,7 @@ class PhoneAgent( finished = false, action = null, thinking = "", - message = PAUSE_MESSAGE, + message = getPauseMessage(), paused = true, ) } @@ -543,11 +555,12 @@ class PhoneAgent( historyManager?.setCurrentScreenshot(screenshot.base64Data, screenshot.width, screenshot.height) // Build user message + val isEn = config.language.lowercase().let { it == "en" || it == "english" } val userText = when { - task != null -> "任务: $task\n当前屏幕截图如下:" - hint != null -> "上一步执行结果: $hint\n继续执行任务,当前屏幕截图如下:" - else -> "继续执行任务,当前屏幕截图如下:" + task != null -> if (isEn) "Task: $task\nCurrent screenshot is as follows:" else "任务: $task\n当前屏幕截图如下:" + hint != null -> if (isEn) "Previous step result: $hint\nContinuing task, current screenshot is as follows:" else "上一步执行结果: $hint\n继续执行任务,当前屏幕截图如下:" + else -> if (isEn) "Continuing task, current screenshot is as follows:" else "继续执行任务,当前屏幕截图如下:" } // Add user message to context (screenshot is passed separately to model) @@ -581,7 +594,7 @@ class PhoneAgent( finished = false, action = null, thinking = "", - message = PAUSE_MESSAGE, + message = getPauseMessage(), paused = true, ) } @@ -617,7 +630,7 @@ class PhoneAgent( finished = false, action = null, thinking = response.thinking, - message = PAUSE_MESSAGE, + message = getPauseMessage(), paused = true, ) } @@ -644,26 +657,28 @@ class PhoneAgent( finished = false, action = null, thinking = retryResult?.thinking ?: response.thinking, - message = PAUSE_MESSAGE, + message = getPauseMessage(), paused = true, ) } if (retryResult == null) { + val noActionDesc = if (isEn) "No action" else "无操作" + val noActionMsg = if (isEn) "No action in model response (retried ${MAX_EMPTY_ACTION_RETRIES} times)" else "模型响应中没有操作(已重试${MAX_EMPTY_ACTION_RETRIES}次)" historyManager?.recordStep( stepNumber = currentStepNumber, thinking = response.thinking, action = null, - actionDescription = "无操作", + actionDescription = noActionDesc, success = false, - message = "模型响应中没有操作(已重试${MAX_EMPTY_ACTION_RETRIES}次)", + message = noActionMsg, ) return StepResult( success = false, finished = false, action = null, thinking = response.thinking, - message = "模型响应中没有操作(已重试${MAX_EMPTY_ACTION_RETRIES}次)", + message = noActionMsg, ) } @@ -709,19 +724,19 @@ class PhoneAgent( finished = false, action = null, thinking = "", - message = PAUSE_MESSAGE, + message = getPauseMessage(), paused = true, ) } - val handledError = ErrorHandler.handleNetworkError(modelResult.error) + val handledError = ErrorHandler.handleNetworkError(modelResult.error, language = config.language) Logger.e(TAG, ErrorHandler.formatErrorForLog(handledError)) // Record failed step historyManager?.recordStep( stepNumber = currentStepNumber, thinking = "", action = null, - actionDescription = "模型错误", + actionDescription = if (isEn) "Model error" else "模型错误", success = false, message = handledError.userMessage, ) @@ -766,12 +781,12 @@ class PhoneAgent( finished = false, action = null, thinking = "", - message = PAUSE_MESSAGE, + message = getPauseMessage(), paused = true, ) } - val handledError = ErrorHandler.handleUnknownError("Step execution error", e) + val handledError = ErrorHandler.handleUnknownError("Step execution error", e, language = config.language) Logger.e(TAG, ErrorHandler.formatErrorForLog(handledError), e) return StepResult( success = false, @@ -977,11 +992,13 @@ class PhoneAgent( val correctionHint = buildCoordinateCorrectionHint(e, config.language) // Record failed step + val isEn = config.language.lowercase().let { it == "en" || it == "english" } + val actionDesc = if (isEn) "Coordinate out of bounds: ${e.originalAction}" else "坐标越界: ${e.originalAction}" historyManager?.recordStep( stepNumber = currentStepNumber, thinking = thinking, action = null, - actionDescription = "坐标越界: ${e.originalAction}", + actionDescription = actionDesc, success = false, message = correctionHint, ) @@ -996,14 +1013,16 @@ class PhoneAgent( nextStepHint = correctionHint, ) } catch (e: ActionParseException) { - val handledError = ErrorHandler.handleParsingError(actionStr, e.message ?: "Unknown parse error", e) + val handledError = ErrorHandler.handleParsingError(actionStr, e.message ?: "Unknown parse error", e, language = config.language) Logger.e(TAG, ErrorHandler.formatErrorForLog(handledError), e) + val isEn = config.language.lowercase().let { it == "en" || it == "english" } + val parseDesc = if (isEn) "Parse error: $actionStr" else "解析错误: $actionStr" // Record failed step historyManager?.recordStep( stepNumber = currentStepNumber, thinking = thinking, action = null, - actionDescription = "解析错误: $actionStr", + actionDescription = parseDesc, success = false, message = handledError.userMessage, ) diff --git a/app/src/main/java/com/kevinluo/autoglm/config/I18n.kt b/app/src/main/java/com/kevinluo/autoglm/config/I18n.kt index 7441e2f..e10a4e9 100644 --- a/app/src/main/java/com/kevinluo/autoglm/config/I18n.kt +++ b/app/src/main/java/com/kevinluo/autoglm/config/I18n.kt @@ -99,6 +99,30 @@ object I18n { "status_failed" to "失败", "status_cancelled" to "已取消", "status_waiting" to "等待中", + "auth_failed_401" to "认证失败 (401),请检查 API 密钥", + "forbidden_403" to "访问被拒绝 (403),请检查权限", + "not_found_404" to "接口或模型不存在 (404),请检查 API 地址和模型名称", + "rate_limit_429" to "请求过于频繁或额度不足 (429),请稍后重试", + "server_error_format" to "服务器错误 (%d),请稍后重试", + "connect_failed" to "无法连接到服务器,请检查网络连接", + "request_timeout" to "请求超时,请稍后重试", + "parse_response_failed" to "无法解析服务器响应", + "action_failed_format" to "操作执行失败: %s", + "screen_protected" to "当前屏幕受保护,无法截图", + "screenshot_failed" to "截图失败,请重试", + "missing_permission_format" to "缺少必要权限: %s", + "shizuku_unavailable" to "Shizuku 服务不可用,请确保 Shizuku 已启动并授权", + "parse_model_failed" to "无法解析模型响应", + "config_error_format" to "配置错误: %s", + "unknown_error_retry" to "发生未知错误,请重试", + "app_not_found_format" to "找不到应用: %s", + "retryable_suffix" to " (可重试)", + "no_action" to "无操作", + "no_action_retry_format" to "模型响应中没有操作(已重试%d次)", + "model_error" to "模型错误", + "coord_out_of_bounds_format" to "坐标越界: %s", + "parse_error_format" to "解析错误: %s", + "task_paused" to "任务已暂停", ) /** @@ -179,6 +203,30 @@ object I18n { "status_failed" to "Failed", "status_cancelled" to "Cancelled", "status_waiting" to "Waiting", + "auth_failed_401" to "Authentication failed (401), please check your API key", + "forbidden_403" to "Access forbidden (403), please check permissions", + "not_found_404" to "Endpoint or model not found (404), please check model name and URL", + "rate_limit_429" to "Rate limit exceeded or quota exhausted (429), please try again later", + "server_error_format" to "Server error (%d), please try again later", + "connect_failed" to "Cannot connect to server, please check network connection", + "request_timeout" to "Request timed out, please try again later", + "parse_response_failed" to "Failed to parse server response", + "action_failed_format" to "Action execution failed: %s", + "screen_protected" to "Current screen is protected, cannot capture screenshot", + "screenshot_failed" to "Screenshot failed, please retry", + "missing_permission_format" to "Missing required permission: %s", + "shizuku_unavailable" to "Shizuku service unavailable, please ensure Shizuku is running and authorized", + "parse_model_failed" to "Failed to parse model response", + "config_error_format" to "Configuration error: %s", + "unknown_error_retry" to "An unknown error occurred, please retry", + "app_not_found_format" to "App not found: %s", + "retryable_suffix" to " (Retryable)", + "no_action" to "No action", + "no_action_retry_format" to "No action in model response (retried %d times)", + "model_error" to "Model error", + "coord_out_of_bounds_format" to "Coordinate out of bounds: %s", + "parse_error_format" to "Parse error: %s", + "task_paused" to "Task paused", ) /** diff --git a/app/src/main/java/com/kevinluo/autoglm/device/DeviceExecutor.kt b/app/src/main/java/com/kevinluo/autoglm/device/DeviceExecutor.kt index c2c49b8..4d6275d 100644 --- a/app/src/main/java/com/kevinluo/autoglm/device/DeviceExecutor.kt +++ b/app/src/main/java/com/kevinluo/autoglm/device/DeviceExecutor.kt @@ -321,7 +321,7 @@ class DeviceExecutor(private val userService: IUserService) { * @param command The shell command to execute * @return The command output, or an error message if execution fails */ - private fun executeCommand(command: String): String = try { + fun executeCommand(command: String): String = try { userService.executeCommand(command) } catch (e: Exception) { Logger.e(TAG, "Error executing command: $command", e) diff --git a/app/src/main/java/com/kevinluo/autoglm/home/TaskFragment.kt b/app/src/main/java/com/kevinluo/autoglm/home/TaskFragment.kt index 9ff4280..e82fbb8 100644 --- a/app/src/main/java/com/kevinluo/autoglm/home/TaskFragment.kt +++ b/app/src/main/java/com/kevinluo/autoglm/home/TaskFragment.kt @@ -202,8 +202,8 @@ class TaskFragment : Fragment() { * Updates UI based on the current state. */ private fun updateUiState(state: MainUiState) { - // Update start button state - btnStartTask.isEnabled = state.canStartTask + // Update start button state - enabled whenever a task is not currently running + btnStartTask.isEnabled = !state.isTaskRunning } /** @@ -221,11 +221,21 @@ class TaskFragment : Fragment() { // Check Shizuku connection if (state.shizukuStatus != ShizukuStatus.CONNECTED) { - Toast.makeText( - requireContext(), - R.string.toast_shizuku_not_running, - Toast.LENGTH_SHORT, - ).show() + if (rikka.shizuku.Shizuku.pingBinder() && + rikka.shizuku.Shizuku.checkSelfPermission() != android.content.pm.PackageManager.PERMISSION_GRANTED + ) { + try { + rikka.shizuku.Shizuku.requestPermission(1001) + } catch (e: Exception) { + Logger.e(TAG, "Failed to request Shizuku permission", e) + } + } else { + Toast.makeText( + requireContext(), + R.string.toast_shizuku_not_running, + Toast.LENGTH_SHORT, + ).show() + } return } diff --git a/app/src/main/java/com/kevinluo/autoglm/input/KeyboardHelper.kt b/app/src/main/java/com/kevinluo/autoglm/input/KeyboardHelper.kt index 27889c3..d8c068b 100644 --- a/app/src/main/java/com/kevinluo/autoglm/input/KeyboardHelper.kt +++ b/app/src/main/java/com/kevinluo/autoglm/input/KeyboardHelper.kt @@ -4,6 +4,8 @@ import android.content.Context import android.content.Intent import android.provider.Settings import android.view.inputmethod.InputMethodManager +import com.kevinluo.autoglm.ComponentManager +import com.kevinluo.autoglm.R import com.kevinluo.autoglm.util.Logger /** @@ -11,7 +13,6 @@ import com.kevinluo.autoglm.util.Logger * * Provides utilities for checking AutoGLM Keyboard availability, * enabling the keyboard, and navigating to keyboard settings. - * */ object KeyboardHelper { private const val TAG = "KeyboardHelper" @@ -45,19 +46,35 @@ object KeyboardHelper { * @return [KeyboardStatus] indicating the keyboard's current state */ fun getAutoGLMKeyboardStatus(context: Context): KeyboardStatus { - val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - val enabledInputMethods = imm.enabledInputMethodList - - Logger.d(TAG, "Looking for keyboard: package=$PACKAGE_NAME") + try { + val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager + val enabledInputMethods = imm?.enabledInputMethodList + if (enabledInputMethods != null) { + for (ime in enabledInputMethods) { + if (ime.packageName == PACKAGE_NAME && + ime.serviceName.endsWith(".AutoGLMKeyboardService") + ) { + Logger.d(TAG, "AutoGLM Keyboard is enabled (via IMM)") + return KeyboardStatus.ENABLED + } + } + } + } catch (e: Exception) { + Logger.w(TAG, "Error checking keyboard via InputMethodManager", e) + } - for (ime in enabledInputMethods) { - Logger.d(TAG, "Found IME: package=${ime.packageName}, service=${ime.serviceName}") - if (ime.packageName == PACKAGE_NAME && - ime.serviceName.endsWith(".AutoGLMKeyboardService") - ) { - Logger.d(TAG, "AutoGLM Keyboard is enabled") - return KeyboardStatus.ENABLED + // Shizuku fallback: check enabled IMEs via UserService if available + try { + val deviceExecutor = ComponentManager.getInstance(context).deviceExecutor + if (deviceExecutor != null) { + val output = deviceExecutor.executeCommand("ime list -s") + if (output.contains(IME_ID) || output.contains(PACKAGE_NAME)) { + Logger.d(TAG, "AutoGLM Keyboard is enabled (via Shizuku UserService)") + return KeyboardStatus.ENABLED + } } + } catch (e: Exception) { + Logger.w(TAG, "Error checking keyboard via Shizuku fallback", e) } Logger.d(TAG, "AutoGLM Keyboard is not enabled") @@ -72,6 +89,29 @@ object KeyboardHelper { */ fun isKeyboardAvailable(context: Context): Boolean = getAutoGLMKeyboardStatus(context) == KeyboardStatus.ENABLED + /** + * Attempts to enable AutoGLM Keyboard directly using Shizuku UserService elevated privileges. + * + * @param context Application context + * @return true if keyboard was successfully enabled, false otherwise + */ + fun enableKeyboardViaShizuku(context: Context): Boolean { + try { + val deviceExecutor = ComponentManager.getInstance(context).deviceExecutor + if (deviceExecutor != null) { + val output = deviceExecutor.executeCommand("ime enable $IME_ID") + Logger.d(TAG, "Enable keyboard command output: $output") + if (isKeyboardAvailable(context)) { + Logger.i(TAG, "AutoGLM Keyboard successfully enabled via Shizuku UserService") + return true + } + } + } catch (e: Exception) { + Logger.w(TAG, "Failed to enable keyboard via Shizuku", e) + } + return false + } + /** * Gets a human-readable status message for keyboard availability. * @@ -79,8 +119,8 @@ object KeyboardHelper { * @return Status message describing keyboard availability */ fun getKeyboardStatusMessage(context: Context): String = when (getAutoGLMKeyboardStatus(context)) { - KeyboardStatus.ENABLED -> "AutoGLM Keyboard 已启用" - KeyboardStatus.NOT_ENABLED -> "请启用 AutoGLM Keyboard" + KeyboardStatus.ENABLED -> context.getString(R.string.keyboard_enabled) + KeyboardStatus.NOT_ENABLED -> context.getString(R.string.keyboard_not_enabled) } /** @@ -106,8 +146,8 @@ object KeyboardHelper { */ fun showInputMethodPicker(context: Context) { try { - val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - imm.showInputMethodPicker() + val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager + imm?.showInputMethodPicker() Logger.d(TAG, "Showed input method picker") } catch (e: Exception) { Logger.e(TAG, "Failed to show input method picker", e) diff --git a/app/src/main/java/com/kevinluo/autoglm/settings/SettingsFragment.kt b/app/src/main/java/com/kevinluo/autoglm/settings/SettingsFragment.kt index 191fe55..77da202 100644 --- a/app/src/main/java/com/kevinluo/autoglm/settings/SettingsFragment.kt +++ b/app/src/main/java/com/kevinluo/autoglm/settings/SettingsFragment.kt @@ -37,6 +37,7 @@ import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputLayout import com.kevinluo.autoglm.R import com.kevinluo.autoglm.agent.AgentConfig +import com.kevinluo.autoglm.input.KeyboardHelper import com.kevinluo.autoglm.model.ModelClient import com.kevinluo.autoglm.model.ModelConfig import com.kevinluo.autoglm.ui.MainViewModel @@ -571,14 +572,8 @@ class SettingsFragment : Fragment() { false } - private fun isKeyboardEnabled(): Boolean { - val enabledInputMethods = - Settings.Secure.getString( - requireContext().contentResolver, - Settings.Secure.ENABLED_INPUT_METHODS, - ) ?: "" - return enabledInputMethods.contains(requireContext().packageName) - } + private fun isKeyboardEnabled(): Boolean = + KeyboardHelper.isKeyboardAvailable(requireContext()) private fun isBatteryOptimizationIgnored(): Boolean { val pm = requireContext().getSystemService(PowerManager::class.java) @@ -611,7 +606,12 @@ class SettingsFragment : Fragment() { } private fun openKeyboardSettings() { - startActivity(Intent(Settings.ACTION_INPUT_METHOD_SETTINGS)) + if (KeyboardHelper.enableKeyboardViaShizuku(requireContext())) { + refreshPermissionStates() + Toast.makeText(requireContext(), R.string.keyboard_enabled, Toast.LENGTH_SHORT).show() + } else { + startActivity(Intent(Settings.ACTION_INPUT_METHOD_SETTINGS)) + } } private fun requestBatteryOptimization() { diff --git a/app/src/main/java/com/kevinluo/autoglm/task/TaskExecutionManager.kt b/app/src/main/java/com/kevinluo/autoglm/task/TaskExecutionManager.kt index 306c1c8..92ce735 100644 --- a/app/src/main/java/com/kevinluo/autoglm/task/TaskExecutionManager.kt +++ b/app/src/main/java/com/kevinluo/autoglm/task/TaskExecutionManager.kt @@ -219,13 +219,14 @@ object TaskExecutionManager : PhoneAgentListener { fun cancelTask() { val componentManager = getComponentManager() ?: return val agent = componentManager.phoneAgent ?: return + val language = componentManager.settingsManager.getAgentConfig().language Logger.i(TAG, "Cancelling task") agent.cancel() _taskState.value = _taskState.value.copy( status = TaskStatus.FAILED, - resultMessage = "任务已取消", + resultMessage = com.kevinluo.autoglm.config.I18n.getMessage("task_cancelled", language), ) } @@ -365,7 +366,8 @@ object TaskExecutionManager : PhoneAgentListener { * @param action The action that was executed */ override fun onActionExecuted(action: AgentAction) { - val actionText = action.formatForDisplay() + val language = getComponentManager()?.settingsManager?.getAgentConfig()?.language ?: "cn" + val actionText = action.formatForDisplay(language) Logger.d(TAG, "Action executed: $actionText") _taskState.value = _taskState.value.copy(currentAction = actionText) diff --git a/app/src/main/java/com/kevinluo/autoglm/ui/FloatingWindowService.kt b/app/src/main/java/com/kevinluo/autoglm/ui/FloatingWindowService.kt index 5a8e8d3..4b65849 100644 --- a/app/src/main/java/com/kevinluo/autoglm/ui/FloatingWindowService.kt +++ b/app/src/main/java/com/kevinluo/autoglm/ui/FloatingWindowService.kt @@ -398,11 +398,13 @@ class FloatingWindowService : */ fun addStep(stepNumber: Int, thinking: String, action: AgentAction?) { serviceScope.launch { + val lang = com.kevinluo.autoglm.settings.SettingsManager.getInstance(this@FloatingWindowService).getAgentConfig().language + val noActionText = com.kevinluo.autoglm.config.I18n.getMessage("no_action", lang) val step = FloatingStep( stepNumber = stepNumber, thinking = thinking, - action = action?.formatForDisplay() ?: "无", + action = action?.formatForDisplay(lang) ?: noActionText, ) stepsList.add(step) stepsAdapter?.notifyItemInserted(stepsList.size - 1) @@ -899,6 +901,10 @@ class FloatingWindowService : startBtn?.setOnClickListener { val task = taskInput?.text?.toString()?.trim() ?: "" + if (task.isBlank()) { + Toast.makeText(this, R.string.toast_task_empty, Toast.LENGTH_SHORT).show() + return@setOnClickListener + } if (task.isNotBlank()) { // Check if we can start a task and get specific reason if not val blockReason = TaskExecutionManager.getStartTaskBlockReason() diff --git a/app/src/main/java/com/kevinluo/autoglm/util/ErrorHandler.kt b/app/src/main/java/com/kevinluo/autoglm/util/ErrorHandler.kt index 2ca287a..6550632 100644 --- a/app/src/main/java/com/kevinluo/autoglm/util/ErrorHandler.kt +++ b/app/src/main/java/com/kevinluo/autoglm/util/ErrorHandler.kt @@ -1,25 +1,15 @@ package com.kevinluo.autoglm.util +import com.kevinluo.autoglm.config.I18n import com.kevinluo.autoglm.model.NetworkError /** * Centralized error handling utility for the AutoGLM Phone Agent application. * - * Provides consistent error categorization, logging, and user-friendly messages. + * Provides consistent error categorization, logging, and user-friendly messages + * with multi-language support (Chinese and English). * All error handling should go through this utility to ensure consistent * error formatting and logging across the application. - * - * Usage example: - * ```kotlin - * try { - * performOperation() - * } catch (e: Exception) { - * val error = ErrorHandler.handleUnknownError("Operation failed", e) - * Logger.e(TAG, ErrorHandler.formatErrorForLog(error), e) - * return Result.Error(error.userMessage, e) - * } - * ``` - * */ object ErrorHandler { /** @@ -71,17 +61,17 @@ object ErrorHandler { * Handles a network error and returns a user-friendly error. * * @param error The network error to handle + * @param language Language code: "cn" for Chinese, "en" for English * @return HandledError with appropriate user and technical messages - * */ - fun handleNetworkError(error: NetworkError): HandledError { + fun handleNetworkError(error: NetworkError, language: String = "cn"): HandledError { Logger.logNetworkError(error.message ?: "Unknown network error") return when (error) { is NetworkError.ConnectionFailed -> { HandledError( category = ErrorCategory.NETWORK, - userMessage = "无法连接到服务器,请检查网络连接", + userMessage = I18n.getMessage("connect_failed", language), technicalMessage = error.message, isRetryable = true, originalException = error, @@ -91,7 +81,7 @@ object ErrorHandler { is NetworkError.Timeout -> { HandledError( category = ErrorCategory.NETWORK, - userMessage = "请求超时,请稍后重试", + userMessage = I18n.getMessage("request_timeout", language), technicalMessage = "Request timed out after ${error.timeoutMs}ms", isRetryable = true, originalException = error, @@ -99,9 +89,16 @@ object ErrorHandler { } is NetworkError.ServerError -> { + val userMsg = when (error.statusCode) { + 401 -> I18n.getMessage("auth_failed_401", language) + 403 -> I18n.getMessage("forbidden_403", language) + 404 -> I18n.getMessage("not_found_404", language) + 429 -> I18n.getMessage("rate_limit_429", language) + else -> I18n.getFormattedMessage("server_error_format", language, error.statusCode) + } HandledError( category = ErrorCategory.NETWORK, - userMessage = "服务器错误 (${error.statusCode}),请稍后重试", + userMessage = userMsg, technicalMessage = "Server error ${error.statusCode}: ${error.message}", isRetryable = error.statusCode >= 500, originalException = error, @@ -111,7 +108,7 @@ object ErrorHandler { is NetworkError.ParseError -> { HandledError( category = ErrorCategory.PARSING, - userMessage = "无法解析服务器响应", + userMessage = I18n.getMessage("parse_response_failed", language), technicalMessage = "Parse error: ${error.rawResponse.take(MAX_RAW_RESPONSE_LENGTH)}", isRetryable = false, originalException = error, @@ -126,15 +123,20 @@ object ErrorHandler { * @param actionType Type of action that failed (e.g., "tap", "swipe", "type") * @param error Error message describing what went wrong * @param exception Optional exception that caused the error + * @param language Language code: "cn" for Chinese, "en" for English * @return HandledError with appropriate user and technical messages - * */ - fun handleActionError(actionType: String, error: String, exception: Throwable? = null): HandledError { + fun handleActionError( + actionType: String, + error: String, + exception: Throwable? = null, + language: String = "cn", + ): HandledError { Logger.e(TAG, "Action error [$actionType]: $error", exception ?: Exception(error)) return HandledError( category = ErrorCategory.ACTION, - userMessage = "操作执行失败: $actionType", + userMessage = I18n.getFormattedMessage("action_failed_format", language, actionType), technicalMessage = error, isRetryable = true, originalException = exception, @@ -147,20 +149,21 @@ object ErrorHandler { * @param error Error message describing what went wrong * @param isSensitive Whether the error is due to sensitive screen detection * @param exception Optional exception that caused the error + * @param language Language code: "cn" for Chinese, "en" for English * @return HandledError with appropriate user and technical messages - * */ fun handleScreenshotError( error: String, isSensitive: Boolean = false, exception: Throwable? = null, + language: String = "cn", ): HandledError { Logger.e(TAG, "Screenshot error: $error", exception ?: Exception(error)) return if (isSensitive) { HandledError( category = ErrorCategory.SCREENSHOT, - userMessage = "当前屏幕受保护,无法截图", + userMessage = I18n.getMessage("screen_protected", language), technicalMessage = "Sensitive screen detected", isRetryable = false, originalException = exception, @@ -168,7 +171,7 @@ object ErrorHandler { } else { HandledError( category = ErrorCategory.SCREENSHOT, - userMessage = "截图失败,请重试", + userMessage = I18n.getMessage("screenshot_failed", language), technicalMessage = error, isRetryable = true, originalException = exception, @@ -181,15 +184,19 @@ object ErrorHandler { * * @param permission Permission that is missing or denied * @param exception Optional exception that caused the error + * @param language Language code: "cn" for Chinese, "en" for English * @return HandledError with appropriate user and technical messages - * */ - fun handlePermissionError(permission: String, exception: Throwable? = null): HandledError { + fun handlePermissionError( + permission: String, + exception: Throwable? = null, + language: String = "cn", + ): HandledError { Logger.e(TAG, "Permission error: $permission", exception ?: Exception("Missing permission: $permission")) return HandledError( category = ErrorCategory.PERMISSION, - userMessage = "缺少必要权限: $permission", + userMessage = I18n.getFormattedMessage("missing_permission_format", language, permission), technicalMessage = "Missing permission: $permission", isRetryable = false, originalException = exception, @@ -201,15 +208,19 @@ object ErrorHandler { * * @param error Error message describing the Shizuku issue * @param exception Optional exception that caused the error + * @param language Language code: "cn" for Chinese, "en" for English * @return HandledError with appropriate user and technical messages - * */ - fun handleShizukuError(error: String, exception: Throwable? = null): HandledError { + fun handleShizukuError( + error: String, + exception: Throwable? = null, + language: String = "cn", + ): HandledError { Logger.e(TAG, "Shizuku error: $error", exception ?: Exception(error)) return HandledError( category = ErrorCategory.PERMISSION, - userMessage = "Shizuku 服务不可用,请确保 Shizuku 已启动并授权", + userMessage = I18n.getMessage("shizuku_unavailable", language), technicalMessage = error, isRetryable = true, originalException = exception, @@ -222,10 +233,15 @@ object ErrorHandler { * @param input Input that failed to parse (will be truncated in logs) * @param error Error message describing the parsing failure * @param exception Optional exception that caused the error + * @param language Language code: "cn" for Chinese, "en" for English * @return HandledError with appropriate user and technical messages - * */ - fun handleParsingError(input: String, error: String, exception: Throwable? = null): HandledError { + fun handleParsingError( + input: String, + error: String, + exception: Throwable? = null, + language: String = "cn", + ): HandledError { Logger.e( TAG, "Parsing error: $error, input: ${input.take(MAX_INPUT_LOG_LENGTH)}", @@ -234,7 +250,7 @@ object ErrorHandler { return HandledError( category = ErrorCategory.PARSING, - userMessage = "无法解析模型响应", + userMessage = I18n.getMessage("parse_model_failed", language), technicalMessage = "Parse error: $error", isRetryable = false, originalException = exception, @@ -246,15 +262,19 @@ object ErrorHandler { * * @param setting Setting name that is invalid or missing * @param error Error message describing the configuration issue + * @param language Language code: "cn" for Chinese, "en" for English * @return HandledError with appropriate user and technical messages - * */ - fun handleConfigurationError(setting: String, error: String): HandledError { + fun handleConfigurationError( + setting: String, + error: String, + language: String = "cn", + ): HandledError { Logger.e(TAG, "Configuration error [$setting]: $error") return HandledError( category = ErrorCategory.CONFIGURATION, - userMessage = "配置错误: $setting", + userMessage = I18n.getFormattedMessage("config_error_format", language, setting), technicalMessage = error, isRetryable = false, ) @@ -265,15 +285,19 @@ object ErrorHandler { * * @param error Error message describing what went wrong * @param exception Optional exception that caused the error + * @param language Language code: "cn" for Chinese, "en" for English * @return HandledError with appropriate user and technical messages - * */ - fun handleUnknownError(error: String, exception: Throwable? = null): HandledError { + fun handleUnknownError( + error: String, + exception: Throwable? = null, + language: String = "cn", + ): HandledError { Logger.e(TAG, "Unknown error: $error", exception ?: Exception(error)) return HandledError( category = ErrorCategory.UNKNOWN, - userMessage = "发生未知错误,请重试", + userMessage = I18n.getMessage("unknown_error_retry", language), technicalMessage = error, isRetryable = true, originalException = exception, @@ -284,15 +308,15 @@ object ErrorHandler { * Handles an app not found error. * * @param appName Name of the app that wasn't found + * @param language Language code: "cn" for Chinese, "en" for English * @return HandledError with appropriate user and technical messages - * */ - fun handleAppNotFoundError(appName: String): HandledError { + fun handleAppNotFoundError(appName: String, language: String = "cn"): HandledError { Logger.w(TAG, "App not found: $appName") return HandledError( category = ErrorCategory.ACTION, - userMessage = "找不到应用: $appName", + userMessage = I18n.getFormattedMessage("app_not_found_format", language, appName), technicalMessage = "App not found: $appName", isRetryable = false, ) @@ -302,13 +326,13 @@ object ErrorHandler { * Formats an error for user display. * * @param error The handled error to format + * @param language Language code: "cn" for Chinese, "en" for English * @return Formatted error message suitable for UI display - * */ - fun formatErrorForDisplay(error: HandledError): String = buildString { + fun formatErrorForDisplay(error: HandledError, language: String = "cn"): String = buildString { append(error.userMessage) if (error.isRetryable) { - append(" (可重试)") + append(I18n.getMessage("retryable_suffix", language)) } } @@ -317,7 +341,6 @@ object ErrorHandler { * * @param error The handled error to format * @return Formatted error message suitable for log output - * */ fun formatErrorForLog(error: HandledError): String = buildString { append("[${error.category}] ") diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml new file mode 100644 index 0000000..907b1a4 --- /dev/null +++ b/app/src/main/res/values-en/strings.xml @@ -0,0 +1,328 @@ + + AutoGLM For Android + + + Home + History + Settings + + + Permissions Status + Checking... + All Granted + %1$d/%2$d Granted + Grant + Enable + Granted + Not Granted + + + Shizuku Status + Unknown + Not Running + Running, Not Authorized + Connecting... + Connected + Authorize + Open Shizuku + + + Floating Window + Granted + Not Granted + Authorize + + + Battery Optimization + Ignored + Not Ignored + Settings + Ignoring battery optimization helps background tasks run reliably. + + + Keyboard IME + Enabled + Disabled + Enable + AutoGLM Keyboard must be enabled for text input + + + Task Input + Describe the task you want to execute... + Start + Stop + + + Status + Idle + Running + Paused + Completed + Failed + Cancelled + Steps: 0 + Steps: %d + Pause + Resume + + + Execution Logs + View detailed task execution logs + Logs will appear here... + + + Please start Shizuku first + Shizuku version is too low + Permission granted + Permission denied + Permission already granted + Please grant permission in Shizuku + Service connected + Service disconnected + Please enter a task description + Task started + Task cancelled + Floating window permission required + A task is already running + Agent not ready, please try again shortly + + + Settings + Model Configuration + Configure API connection parameters + Agent Configuration + Configure task execution parameters + API Base URL + Model Name + API Key + Max Steps + Set to 0 for unlimited steps + Screenshot Delay (s) + Wait time for page to load after executing an action + Language + 中文 + English + Save + Reset + Settings saved + Reset completed + Please enter a valid URL + Please enter a model name + Steps cannot be negative + Delay cannot be negative + Saved Profiles + Save Profile + Copy Profile + Delete Profile + Profile Menu + New Profile + Profile Name + Profile saved + Profile copied + Profile deleted + Please enter a profile name + Are you sure you want to delete this profile? + %s (Copy) + Test Connection + Testing... + Connected successfully! Latency: %dms + Authentication failed: %s + Model error: %s + Server error (%d): %s + Connection failed: %s + Connection timed out: %s + Please fill in all configuration fields first + + + Task Templates + Save frequently used tasks for quick input + Add Template + Edit Template + Delete Template + Template Name + Task Description + Template saved + Template deleted + Please enter a template name + Please enter a task description + Are you sure you want to delete this template? + No templates yet + Select Template + + + Advanced Settings + Customize system prompts + System Prompt + This is an advanced feature. Modifying system prompts may affect Agent behavior. If unsure, please keep default settings.\n\nTip: Use {date} as date placeholder, it will be automatically replaced with the current date at runtime. + Chinese Prompt + English Prompt + Edit Prompt + Reset to Default + Are you sure you want to reset to the default prompt? + Prompt saved + Reset to default prompt + Customized + Default + + + Confirm Action + Are you sure you want to perform this action? + Confirm + Cancel + Choose Option + Please select one of the following options + Cancel + + + Manual Action Required + Please complete the following actions + Instructions will appear here + Done, Continue + + + Thinking + Action + Waiting for task... + None + Minimize + Open App + Waiting for Confirmation + Open Floating Window + New Task + Thinking... + + + Floating Window + History + + + AutoGLM + Show/Hide Floating Window + + + Service is running + Task executing... + Task paused + Task completed + Task execution failed + Waiting for user confirmation + Manual takeover required + + + History + Task Details + No history records + %d steps + Duration: %s + Success + Failed + Reasoning Process + Original + Annotated + Clear History + Are you sure you want to clear all history records? + Are you sure you want to delete this record? + History cleared + Record deleted + Selected %d items + Select All + Delete Selected + Are you sure you want to delete the selected %d records? + Share + Share Task Record + Save Image + Generating image... + Share failed + Image saved to gallery + Failed to save image + Copy Prompt + Prompt copied to clipboard + Load More + Load More (%d steps remaining) + + + Debug Logs + Export logs for troubleshooting + Log Size: %s + Export Logs + Clear + Logs cleared + Export failed + No logs available + Are you sure you want to clear all logs? + + + AutoGLM Keyboard + Ready + Text input controlled by AutoGLM + AutoGLM Keyboard + Enabled + Switch Keyboard + + + Voice Input + Voice Input + Configure speech recognition and wake words + Model Status + Not Downloaded + Downloaded (%1$d MB) + Download Model + Delete Model + Wake Word Sensitivity + Low + High + Separate multiple wake words with commas + Download Voice Model + Speech recognition requires downloading an offline model. The model supports Chinese and English recognition. + Model size is ~85MB. Downloading over Wi-Fi is recommended. + Downloading Voice Model + %1$d%% + Preparing download... + Downloading VAD model... + Downloading ASR model... + Extracting model... + Download complete + Download failed: %1$s + Download cancelled + Background Listening + Listen continuously in the background to quickly launch tasks via wake words + Listening for voice... + Wake Word + Separate multiple wake words with commas + Hey AutoGLM + Wake Word Sensitivity + Listening... + Recognizing... + Microphone permission required for voice input + Microphone permission denied + Please download the voice model first + Notification permission required for background voice service + Microphone permission required for voice listening + Background voice listening started + Delete Voice Model + Are you sure you want to delete the voice model? You will need to download it again to use voice input. + Voice model deleted + Wake word detected: %1$s + + + Listening, please speak... + Recognizing speech... + No speech detected, please try again + Recognition complete, executing soon + Stop Recording + Auto-running in %d seconds + Cancel + Confirm (%d) + Confirm + Retry + + + Failed to load voice model + Voice input failed + Voice recognition failed + Network error + Unknown error + Downloading... + Download progress: %1$d MB / %2$d MB + Download Confirmation + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ca4591b..a0c3258 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -10,7 +10,7 @@ 权限状态 检查中... 全部已授权 - %d/%d 已授权 + %1$d/%2$d 已授权 授权 启用 已授权 diff --git a/app/src/test/java/com/kevinluo/autoglm/settings/SettingsFragmentPropertyTest.kt b/app/src/test/java/com/kevinluo/autoglm/settings/SettingsFragmentPropertyTest.kt index 0a70e71..5fda0bc 100644 --- a/app/src/test/java/com/kevinluo/autoglm/settings/SettingsFragmentPropertyTest.kt +++ b/app/src/test/java/com/kevinluo/autoglm/settings/SettingsFragmentPropertyTest.kt @@ -9,6 +9,7 @@ import io.kotest.property.arbitrary.boolean import io.kotest.property.arbitrary.double import io.kotest.property.arbitrary.int import io.kotest.property.arbitrary.string +import io.kotest.property.arbitrary.stringPattern import io.kotest.property.checkAll /** @@ -161,11 +162,8 @@ class SettingsFragmentPropertyTest : } "wake words should persist correctly" { - checkAll(100, Arb.string(1, 20), Arb.string(1, 20)) { word1, word2 -> - // Trim inputs and filter out commas (comma is the delimiter) - val trimmedWord1 = word1.trim().replace(",", "") - val trimmedWord2 = word2.trim().replace(",", "") - val wakeWords = listOf(trimmedWord1, trimmedWord2).filter { it.isNotEmpty() } + checkAll(100, Arb.stringPattern("[a-zA-Z0-9_]{1,20}"), Arb.stringPattern("[a-zA-Z0-9_]{1,20}")) { word1, word2 -> + val wakeWords = listOf(word1, word2).filter { it.isNotEmpty() } val savedWords = wakeWords.joinToString(",") val loadedWords = savedWords.split(",").map { it.trim() }.filter { it.isNotEmpty() }