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() }