feat(设置): 新增当前位置选择器并优化UI与交互

- 在设置页面添加基于机器人位置列表的下拉选择器,支持手动设置当前位置
- 改进特殊任务模式的开关逻辑,避免与当前任务状态冲突
- 优化MQTT指令处理,新增terminate、continue命令,完善TTS暂停/恢复机制
- 添加表情动画的眨眼效果,改进导航到达和巡逻模式的状态管理
- 重构颜色主题为浅色风格,并添加横屏布局支持
- 更新README文档,补充MQTT指令说明和本地验证步骤
This commit is contained in:
2026-03-13 16:02:33 +08:00
parent b15c5c9021
commit 71e5edc57a
12 changed files with 806 additions and 93 deletions

View File

@@ -54,10 +54,27 @@ Temi SDK 的很多功能(如语音、导航、跟随)依赖于机器人的
- `app/src/main/res/layout/activity_main.xml` (主页布局) - `app/src/main/res/layout/activity_main.xml` (主页布局)
- `app/src/main/res/layout/activity_settings.xml` (设置页布局) - `app/src/main/res/layout/activity_settings.xml` (设置页布局)
## 4. 常见问题 ## 4. MQTT 指令与行为
**Q: 为什么在模拟器上闪退?** 应用订阅 `robot/cmd`,接收 JSON 指令。
A: 因为应用启动时会调用 `Robot.getInstance()`,而普通模拟器没有 Temi 的底层服务。
### 动作列表
- `recharge` 前往充电桩
- `goto` 前往指定地点(字段 `location``target`
- `speak` 立即播报(字段 `text``speech`
- `stream` 流式播报(字段 `text``content`),按句号/感叹号/问号/换行分句
- `stop` 暂停 TTS 与播报队列,不清空 stream buffer
- `continue` 继续播报,优先重播被中断的那句话
- `terminate` 终止导航与 TTS清空队列和 buffer
### special 模式说明
- special 是否启用只看设置页开关 `special_task_mode`
- setCurrentTask 不会开启或关闭 special
- special 开启时,会跳过门控与问候等场景逻辑
## 5. 本地验证
```bash
.\gradlew.bat :app:installDebug
```
**Q: Trae 可以装插件预览吗?**
A: 目前没有插件能直接在 Trae 内部完美模拟 Android 环境。建议使用 Trae 写代码,配合 Android Studio 或真机进行调试预览。

View File

@@ -17,7 +17,7 @@ class AnimatedEmojiView @JvmOverloads constructor(
) : View(context, attrs) { ) : View(context, attrs) {
private val facePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { private val facePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.YELLOW color = Color.WHITE
style = Paint.Style.FILL style = Paint.Style.FILL
} }
@@ -39,7 +39,7 @@ class AnimatedEmojiView @JvmOverloads constructor(
private var mouthOpenRatio = 0.1f private var mouthOpenRatio = 0.1f
private var noddingOffset = 0f private var noddingOffset = 0f
enum class Expression { SMILE, NEUTRAL, TALKING, HAPPY, SAD, WINK, ANGRY } enum class Expression { SMILE, NEUTRAL, TALKING, HAPPY, SAD, WINK, ANGRY, BLINK }
var currentExpression = Expression.SMILE var currentExpression = Expression.SMILE
set(value) { set(value) {
field = value field = value
@@ -70,12 +70,19 @@ class AnimatedEmojiView @JvmOverloads constructor(
val eyeRadius = radius * 0.1f val eyeRadius = radius * 0.1f
val eyeOffsetX = radius * 0.4f val eyeOffsetX = radius * 0.4f
val eyeOffsetY = radius * 0.3f val eyeOffsetY = radius * 0.3f
val closedEyeWidth = eyeRadius * 2.5f
if (currentExpression == Expression.WINK) { if (currentExpression == Expression.WINK) {
val closedEyeWidth = eyeRadius * 2.5f
mouthPaint.style = Paint.Style.STROKE mouthPaint.style = Paint.Style.STROKE
// Left eye closed (wink)
canvas.drawLine(centerX - eyeOffsetX - closedEyeWidth / 2, centerY - eyeOffsetY, centerX - eyeOffsetX + closedEyeWidth / 2, centerY - eyeOffsetY, mouthPaint)
// Right eye open
canvas.drawCircle(centerX + eyeOffsetX, centerY - eyeOffsetY, eyeRadius, eyePaint)
} else if (currentExpression == Expression.BLINK) {
mouthPaint.style = Paint.Style.STROKE
// Both eyes closed
canvas.drawLine(centerX - eyeOffsetX - closedEyeWidth / 2, centerY - eyeOffsetY, centerX - eyeOffsetX + closedEyeWidth / 2, centerY - eyeOffsetY, mouthPaint)
canvas.drawLine(centerX + eyeOffsetX - closedEyeWidth / 2, centerY - eyeOffsetY, centerX + eyeOffsetX + closedEyeWidth / 2, centerY - eyeOffsetY, mouthPaint) canvas.drawLine(centerX + eyeOffsetX - closedEyeWidth / 2, centerY - eyeOffsetY, centerX + eyeOffsetX + closedEyeWidth / 2, centerY - eyeOffsetY, mouthPaint)
canvas.drawCircle(centerX - eyeOffsetX, centerY - eyeOffsetY, eyeRadius, eyePaint)
} else { } else {
canvas.drawCircle(centerX - eyeOffsetX, centerY - eyeOffsetY, eyeRadius, eyePaint) canvas.drawCircle(centerX - eyeOffsetX, centerY - eyeOffsetY, eyeRadius, eyePaint)
canvas.drawCircle(centerX + eyeOffsetX, centerY - eyeOffsetY, eyeRadius, eyePaint) canvas.drawCircle(centerX + eyeOffsetX, centerY - eyeOffsetY, eyeRadius, eyePaint)
@@ -121,6 +128,7 @@ class AnimatedEmojiView @JvmOverloads constructor(
Expression.ANGRY -> canvas.drawArc(mouthPath, 200f, -140f, false, mouthPaint) Expression.ANGRY -> canvas.drawArc(mouthPath, 200f, -140f, false, mouthPaint)
Expression.NEUTRAL -> canvas.drawLine(mouthLeft, mouthTop + mouthHeight / 2, mouthLeft + mouthWidth, mouthTop + mouthHeight / 2, mouthPaint) Expression.NEUTRAL -> canvas.drawLine(mouthLeft, mouthTop + mouthHeight / 2, mouthLeft + mouthWidth, mouthTop + mouthHeight / 2, mouthPaint)
Expression.WINK -> canvas.drawArc(mouthPath, 20f, 140f, false, mouthPaint) Expression.WINK -> canvas.drawArc(mouthPath, 20f, 140f, false, mouthPaint)
Expression.BLINK -> canvas.drawArc(mouthPath, 20f, 140f, false, mouthPaint)
Expression.TALKING -> { Expression.TALKING -> {
mouthPaint.style = Paint.Style.FILL mouthPaint.style = Paint.Style.FILL
val dynamicMouthHeight = mouthHeight * mouthOpenRatio val dynamicMouthHeight = mouthHeight * mouthOpenRatio

View File

@@ -27,6 +27,8 @@ import kotlinx.coroutines.cancel
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlin.random.Random
class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnGoToLocationStatusChangedListener, class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnGoToLocationStatusChangedListener,
OnDetectionStateChangedListener, OnReposeStatusChangedListener, SharedPreferences.OnSharedPreferenceChangeListener, OnDetectionStateChangedListener, OnReposeStatusChangedListener, SharedPreferences.OnSharedPreferenceChangeListener,
@@ -42,16 +44,18 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
private var lastArrivalLocation: String? = null private var lastArrivalLocation: String? = null
private var lastArrivalAt: Long = 0L private var lastArrivalAt: Long = 0L
private val fixedFaceScale = 1.0f
private val baseFaceSizeDp = 1000f
private var currentTask: String = "" private var currentTask: String = ""
private var closeDoorJob: Job? = null private var closeDoorJob: Job? = null
private var blinkJob: Job? = null
private var receptionLocation: String = "" private var receptionLocation: String = ""
private var receptionText: String = "" private var receptionText: String = ""
private var receptionDestination: String = "" private var receptionDestination: String = ""
private var patrolRoute: List<String> = emptyList()
private var patrolIndex: Int = 0
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -65,17 +69,31 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
navCon = NavController(robot) navCon = NavController(robot)
permissionManager = PermissionManager(robot) permissionManager = PermissionManager(robot)
prefs = getSharedPreferences("app_prefs", Context.MODE_PRIVATE) if (savedInstanceState != null) {
if (prefs.getBoolean("special_task_mode", false)) { currentTask = savedInstanceState.getString("currentTask", "") ?: ""
currentTask = "special" receptionLocation = savedInstanceState.getString("receptionLocation", "") ?: ""
receptionText = savedInstanceState.getString("receptionText", "") ?: ""
receptionDestination = savedInstanceState.getString("receptionDestination", "") ?: ""
lastArrivalLocation = savedInstanceState.getString("lastArrivalLocation")
}
if (currentTask == "special") {
currentTask = ""
} }
prefs = getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
prefs.registerOnSharedPreferenceChangeListener(this)
if (lastArrivalLocation == null) {
lastArrivalLocation = prefs.getString("current_location", null)
}
binding.btnSettings.setOnClickListener { binding.btnSettings.setOnClickListener {
startActivity(Intent(this, SettingsActivity::class.java)) startActivity(Intent(this, SettingsActivity::class.java))
} }
binding.animatedEmojiView.currentExpression = AnimatedEmojiView.Expression.SMILE if (currentTask == "patrol") {
// applyFaceScale(fixedFaceScale) // Use XML constraints for layout binding.animatedEmojiView.currentExpression = AnimatedEmojiView.Expression.ANGRY
} else {
binding.animatedEmojiView.currentExpression = AnimatedEmojiView.Expression.SMILE
}
binding.btnReception.setOnClickListener { binding.btnReception.setOnClickListener {
val destination = receptionDestination val destination = receptionDestination
@@ -96,9 +114,13 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
robot.addOnDetectionStateChangedListener(this) robot.addOnDetectionStateChangedListener(this)
robot.addOnReposeStatusChangedListener(this) robot.addOnReposeStatusChangedListener(this)
robot.addOnRequestPermissionResultListener(this) robot.addOnRequestPermissionResultListener(this)
prefs.registerOnSharedPreferenceChangeListener(this)
robot.constraintBeWith() robot.constraintBeWith()
mqttManager?.connect() if (mqttManager == null) {
updateMqttConnection()
} else {
mqttManager?.connect()
}
startBlinking()
} }
override fun onStop() { override fun onStop() {
@@ -109,12 +131,13 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
robot.removeOnDetectionStateChangedListener(this) robot.removeOnDetectionStateChangedListener(this)
robot.removeOnReposeStatusChangedListener(this) robot.removeOnReposeStatusChangedListener(this)
robot.removeOnRequestPermissionResultListener(this) robot.removeOnRequestPermissionResultListener(this)
prefs.unregisterOnSharedPreferenceChangeListener(this) // mqttManager?.disconnect() // Keep MQTT alive in background/settings
mqttManager?.disconnect() stopBlinking()
} }
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
prefs.unregisterOnSharedPreferenceChangeListener(this)
mqttManager?.disconnect() mqttManager?.disconnect()
LogManager.stopLogcatListener() LogManager.stopLogcatListener()
mainScope.cancel() mainScope.cancel()
@@ -168,7 +191,12 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
override fun onGoToLocationStatusChanged(location: String, status: String, descriptionId: Int, description: String) { override fun onGoToLocationStatusChanged(location: String, status: String, descriptionId: Int, description: String) {
val normalized = status.lowercase() val normalized = status.lowercase()
if (normalized != "complete") { val isAbort = normalized in setOf("abort", "aborted", "canceled", "cancelled", "stopped", "stop", "failed", "error")
if (normalized != "complete" && !isAbort) {
return
}
if (isAbort) {
endNonSpecialTask("goTo aborted: $location, status=$status")
return return
} }
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
@@ -177,6 +205,14 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
} }
lastArrivalLocation = location lastArrivalLocation = location
lastArrivalAt = now lastArrivalAt = now
prefs.edit().putString("current_location", location).apply()
if (currentTask == "patrol") {
handlePatrolArrival(location)
}
if (isSpecialModeEnabled() && currentTask.isEmpty()) {
Log.i("MainActivity", "Special task mode: arrival announcement skipped at $location.")
return
}
val text = "已到达$location" val text = "已到达$location"
val ttsRequest = TtsRequest.create(text, false, language = TtsRequest.Language.ZH_CN) val ttsRequest = TtsRequest.create(text, false, language = TtsRequest.Language.ZH_CN)
robot.speak(ttsRequest) robot.speak(ttsRequest)
@@ -209,6 +245,12 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
// Home Base logic // Home Base logic
if (lastArrivalLocation?.lowercase() == "home base") { if (lastArrivalLocation?.lowercase() == "home base") {
// Check if special task mode is enabled, if so, skip door logic
if (isSpecialModeEnabled() && currentTask.isEmpty()) {
Log.i("MainActivity", "Special task mode: Door logic skipped at Home Base.")
return
}
when (state) { when (state) {
DETECTED -> { DETECTED -> {
closeDoorJob?.cancel() closeDoorJob?.cancel()
@@ -241,8 +283,11 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
} }
} }
} }
if (lastArrivalLocation?.lowercase() != "home base" && currentTask.isEmpty() && state == DETECTED) {
if (state == DETECTED && currentTask.isEmpty() && lastArrivalLocation?.lowercase() != "home base") { if (isSpecialModeEnabled()) {
Log.i("MainActivity", "Special task mode enabled (pref check), skipping greeting.")
return
}
val hour = java.util.Calendar.getInstance().get(java.util.Calendar.HOUR_OF_DAY) val hour = java.util.Calendar.getInstance().get(java.util.Calendar.HOUR_OF_DAY)
val greeting = when (hour) { val greeting = when (hour) {
in 6..11 -> "早上好" in 6..11 -> "早上好"
@@ -267,7 +312,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
} }
private fun stopReceptionMode() { private fun stopReceptionMode() {
currentTask = "" setCurrentTask("")
receptionLocation = "" receptionLocation = ""
receptionText = "" receptionText = ""
receptionDestination = "" receptionDestination = ""
@@ -275,15 +320,64 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
Log.i("MainActivity", "Reception mode stopped") Log.i("MainActivity", "Reception mode stopped")
} }
private fun endNonSpecialTask(reason: String) {
if (currentTask.isEmpty()) {
return
}
if (currentTask != "patrol" && currentTask != "reception") {
return
}
Log.i("MainActivity", "Ending task '$currentTask': $reason")
setCurrentTask("")
}
fun startPatrolMode(route: List<String>) {
if (route.isEmpty()) {
setCurrentTask("")
return
}
patrolRoute = route
patrolIndex = 0
setCurrentTask("patrol")
Log.i("MainActivity", "Patrol mode started: route=${route.joinToString()}")
}
private fun handlePatrolArrival(location: String) {
if (patrolRoute.isEmpty()) {
return
}
val matchIndex = patrolRoute.indexOfFirst { it.equals(location, ignoreCase = true) }
if (matchIndex == -1) {
return
}
if (matchIndex >= patrolIndex) {
patrolIndex = matchIndex + 1
}
if (patrolIndex >= patrolRoute.size) {
Log.i("MainActivity", "Patrol route completed: ${patrolRoute.joinToString()}")
setCurrentTask("")
}
}
fun setCurrentTask(task: String) { fun setCurrentTask(task: String) {
currentTask = task val finalTask = task
Log.i("MainActivity", "Current task set to: $task")
// Update expression based on task // Avoid re-setting the same task
if (task == "patrol") { if (currentTask == finalTask) {
return
}
currentTask = finalTask
Log.i("MainActivity", "Current task set to: '$finalTask'")
if (finalTask == "patrol") {
binding.animatedEmojiView.currentExpression = AnimatedEmojiView.Expression.ANGRY binding.animatedEmojiView.currentExpression = AnimatedEmojiView.Expression.ANGRY
} else { } else {
binding.animatedEmojiView.currentExpression = AnimatedEmojiView.Expression.SMILE binding.animatedEmojiView.currentExpression = AnimatedEmojiView.Expression.SMILE
} }
if (finalTask != "patrol") {
patrolRoute = emptyList()
patrolIndex = 0
}
} }
override fun onReposeStatusChanged(status: Int, description: String) { override fun onReposeStatusChanged(status: Int, description: String) {
@@ -311,17 +405,22 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
Log.i("MainActivity", "IP address changed, re-initializing MQTT connection.") Log.i("MainActivity", "IP address changed, re-initializing MQTT connection.")
updateMqttConnection() updateMqttConnection()
} }
if (key == "current_location") {
lastArrivalLocation = sharedPreferences?.getString("current_location", null)
Log.i("MainActivity", "Current location updated manually: $lastArrivalLocation")
}
if (key == "special_task_mode") { if (key == "special_task_mode") {
if (sharedPreferences?.getBoolean("special_task_mode", false) == true) { val isSpecial = sharedPreferences?.getBoolean("special_task_mode", false) == true
setCurrentTask("special") Log.i("MainActivity", "Special mode pref changed: $isSpecial, currentTask: $currentTask")
} else if (currentTask == "special") {
setCurrentTask("")
}
} }
} }
private fun isSpecialModeEnabled(): Boolean {
return ::prefs.isInitialized && prefs.getBoolean("special_task_mode", false)
}
private fun updateMqttConnection() { private fun updateMqttConnection() {
mqttManager?.disconnect() mqttManager?.shutdown()
val ip = prefs.getString("network_ip", null) val ip = prefs.getString("network_ip", null)
if (!ip.isNullOrEmpty()) { if (!ip.isNullOrEmpty()) {
mqttManager = MqttManager(this, ip, robot, navCon) mqttManager = MqttManager(this, ip, robot, navCon)
@@ -333,14 +432,35 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
} }
} }
private fun applyFaceScale(scale: Float) { private fun startBlinking() {
val density = resources.displayMetrics.density stopBlinking()
val sizePx = (baseFaceSizeDp * density * scale).toInt() blinkJob = mainScope.launch {
val params = binding.animatedEmojiView.layoutParams while (isActive) {
params.width = sizePx val delayTime = Random.nextLong(2000, 6000)
params.height = sizePx delay(delayTime)
binding.animatedEmojiView.layoutParams = params
binding.animatedEmojiView.requestLayout() if (binding.animatedEmojiView.currentExpression == AnimatedEmojiView.Expression.SMILE) {
binding.animatedEmojiView.currentExpression = AnimatedEmojiView.Expression.BLINK
delay(200)
if (binding.animatedEmojiView.currentExpression == AnimatedEmojiView.Expression.BLINK) {
binding.animatedEmojiView.currentExpression = AnimatedEmojiView.Expression.SMILE
}
}
}
}
}
private fun stopBlinking() {
blinkJob?.cancel()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString("currentTask", currentTask)
outState.putString("receptionLocation", receptionLocation)
outState.putString("receptionText", receptionText)
outState.putString("receptionDestination", receptionDestination)
outState.putString("lastArrivalLocation", lastArrivalLocation)
} }
} }

View File

@@ -10,7 +10,7 @@ import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence
import org.json.JSONObject import org.json.JSONObject
import java.util.LinkedList import java.util.LinkedList
import java.util.Queue import java.util.Queue
import java.nio.charset.StandardCharsets
class MqttManager( class MqttManager(
private val context: Context, private val context: Context,
private val serverIp: String, private val serverIp: String,
@@ -32,6 +32,13 @@ class MqttManager(
// TTS Queue // TTS Queue
private val ttsQueue: Queue<TtsRequest> = LinkedList() private val ttsQueue: Queue<TtsRequest> = LinkedList()
private var isTtsBusy = false private var isTtsBusy = false
private var isTtsPaused = false
private var currentTtsSpeech: String? = null
private var currentTtsLanguage: TtsRequest.Language? = null
private var interruptedSpeech: String? = null
private var interruptedLanguage: TtsRequest.Language? = null
private var lastStreamLangCode: String? = null
private val ttsLanguageMap = mutableMapOf<TtsRequest, TtsRequest.Language>()
init { init {
try { try {
@@ -48,9 +55,11 @@ class MqttManager(
} }
override fun messageArrived(topic: String?, message: MqttMessage?) { override fun messageArrived(topic: String?, message: MqttMessage?) {
val payload = String(message?.payload ?: ByteArray(0)) val payload = message?.let { String(it.payload, StandardCharsets.UTF_8) }
Log.i(TAG, "Message arrived: Topic=$topic, Payload=$payload") Log.i(TAG, "Message arrived: Topic=$topic, Payload=$payload")
handleIncomingMessage(topic, payload) if (payload != null) {
handleIncomingMessage(topic, payload)
}
} }
override fun deliveryComplete(token: IMqttDeliveryToken?) { override fun deliveryComplete(token: IMqttDeliveryToken?) {
@@ -110,6 +119,21 @@ class MqttManager(
} }
} }
fun shutdown() {
reconnectJob?.cancel()
job.cancel()
try {
if (mqttClient?.isConnected == true) {
mqttClient?.disconnect()
}
mqttClient?.close()
} catch (e: MqttException) {
Log.e(TAG, "Error shutting down MQTT client: ${e.message}")
} finally {
mqttClient = null
}
}
private fun subscribeTopic(topic: String) { private fun subscribeTopic(topic: String) {
try { try {
if (mqttClient?.isConnected == true) { if (mqttClient?.isConnected == true) {
@@ -154,7 +178,9 @@ class MqttManager(
val obj = JSONObject(trimmed) val obj = JSONObject(trimmed)
handleJsonCommand(obj) handleJsonCommand(obj)
} catch (e: Exception) { } catch (e: Exception) {
Log.w(TAG, "Invalid JSON payload: $payload") Log.e(TAG, "Invalid JSON payload: $payload", e)
val ttsRequest = TtsRequest.create("指令格式错误,请检查指令格式", false, language = TtsRequest.Language.ZH_CN)
robot.speak(ttsRequest)
} }
} }
@@ -184,17 +210,29 @@ class MqttManager(
Log.i(TAG, "Repose command sent: $ok") Log.i(TAG, "Repose command sent: $ok")
} }
"stop" -> { "stop" -> {
(context as? MainActivity)?.setCurrentTask("") scope.launch(Dispatchers.Main) {
(context as? MainActivity)?.setCurrentTask("")
}
navController.stop()
pauseTts()
}
"terminate" -> {
scope.launch(Dispatchers.Main) {
(context as? MainActivity)?.setCurrentTask("")
}
navController.stop() navController.stop()
stopTts() stopTts()
} }
"continue" -> {
resumeTts()
}
"patrol" -> { "patrol" -> {
(context as? MainActivity)?.setCurrentTask("patrol")
speak("接到巡逻任务", "zh") speak("接到巡逻任务", "zh")
val flag = obj.optBoolean("flag", true) val flag = obj.optBoolean("flag", true)
var patrolLocations: List<String> = emptyList()
if (flag) { if (flag) {
Log.d(TAG, "navController.randomPatrol() called.") Log.d(TAG, "navController.randomPatrol() called.")
navController.randomPatrol() patrolLocations = navController.randomPatrol()
} else { } else {
val locationsArray = obj.optJSONArray("locations") val locationsArray = obj.optJSONArray("locations")
if (locationsArray != null && locationsArray.length() > 0) { if (locationsArray != null && locationsArray.length() > 0) {
@@ -203,24 +241,40 @@ class MqttManager(
} }
Log.d(TAG, "navController.NavPatrol() called with locations: $locations") Log.d(TAG, "navController.NavPatrol() called with locations: $locations")
navController.NavPatrol(locations) navController.NavPatrol(locations)
patrolLocations = locations
} else { } else {
Log.w(TAG, "Patrol command received without locations, falling back to random patrol.") Log.w(TAG, "Patrol command received without locations, falling back to random patrol.")
navController.randomPatrol() patrolLocations = navController.randomPatrol()
}
}
scope.launch(Dispatchers.Main) {
val activity = context as? MainActivity
if (patrolLocations.isNotEmpty()) {
activity?.startPatrolMode(patrolLocations)
} else {
activity?.setCurrentTask("")
} }
} }
} }
"reception" -> { "reception" -> {
speak("接到接待任务", "zh")
val location = obj.optString("location", "前台") val location = obj.optString("location", "前台")
val text = obj.optString("text", "你是我要接待的贵宾吗?") val text = obj.optString("text", "你是我要接待的贵宾吗?")
val destination = obj.optString("destination", "会议室") val destination = obj.optString("destination", "会议室")
(context as? MainActivity)?.startReceptionMode(location, text, destination) scope.launch(Dispatchers.Main) {
(context as? MainActivity)?.startReceptionMode(location, text, destination)
}
} }
else -> Log.w(TAG, "Unknown command action: $action") else -> Log.w(TAG, "Unknown command action: $action")
} }
} }
private fun processStreamText(text: String, langCode: String?) { private fun processStreamText(text: String, langCode: String?) {
lastStreamLangCode = langCode
speechBuffer.append(text) speechBuffer.append(text)
if (isTtsPaused) {
return
}
while (true) { while (true) {
val content = speechBuffer.toString() val content = speechBuffer.toString()
@@ -252,8 +306,25 @@ class MqttManager(
} }
} }
private fun pauseTts() {
isTtsPaused = true
interruptedSpeech = currentTtsSpeech
interruptedLanguage = currentTtsLanguage
scope.launch(Dispatchers.Main) {
robot.cancelAllTtsRequests()
Log.i(TAG, "TTS paused.")
}
}
private fun stopTts() { private fun stopTts() {
isTtsPaused = false
interruptedSpeech = null
interruptedLanguage = null
currentTtsSpeech = null
currentTtsLanguage = null
lastStreamLangCode = null
speechBuffer.setLength(0) speechBuffer.setLength(0)
ttsLanguageMap.clear()
scope.launch(Dispatchers.Main) { scope.launch(Dispatchers.Main) {
ttsQueue.clear() ttsQueue.clear()
isTtsBusy = false isTtsBusy = false
@@ -262,6 +333,39 @@ class MqttManager(
} }
} }
private fun resumeTts() {
if (!isTtsPaused) {
return
}
isTtsPaused = false
val replaySpeech = interruptedSpeech?.trim().orEmpty()
val replayLanguage = interruptedLanguage
interruptedSpeech = null
interruptedLanguage = null
scope.launch(Dispatchers.Main) {
if (replaySpeech.isNotEmpty()) {
val language = replayLanguage ?: resolveLanguage(lastStreamLangCode)
val replayRequest = TtsRequest.create(replaySpeech, false, language = language)
ttsLanguageMap[replayRequest] = language
if (!isTtsBusy) {
isTtsBusy = true
robot.speak(replayRequest)
Log.i(TAG, "Resume speak: $replaySpeech")
} else {
val list = ttsQueue as? LinkedList<TtsRequest>
if (list != null) {
list.addFirst(replayRequest)
} else {
ttsQueue.offer(replayRequest)
}
Log.i(TAG, "Resume queued: $replaySpeech")
}
}
processStreamText("", lastStreamLangCode)
processNextTts()
}
}
private fun goTo(location: String, backwards: Boolean = false) { private fun goTo(location: String, backwards: Boolean = false) {
val target = location.trim() val target = location.trim()
if (target.isEmpty()) { if (target.isEmpty()) {
@@ -280,6 +384,13 @@ class MqttManager(
val language = resolveLanguage(langCode) val language = resolveLanguage(langCode)
scope.launch(Dispatchers.Main) { scope.launch(Dispatchers.Main) {
val ttsRequest = TtsRequest.create(content, false, language = language) val ttsRequest = TtsRequest.create(content, false, language = language)
ttsLanguageMap[ttsRequest] = language
if (isTtsPaused) {
ttsQueue.offer(ttsRequest)
Log.i(TAG, "Speak queued (paused): $content. Queue size: ${ttsQueue.size}")
return@launch
}
if (!isTtsBusy) { if (!isTtsBusy) {
isTtsBusy = true isTtsBusy = true
@@ -297,13 +408,19 @@ class MqttManager(
when (ttsRequest.status) { when (ttsRequest.status) {
TtsRequest.Status.STARTED -> { TtsRequest.Status.STARTED -> {
isTtsBusy = true isTtsBusy = true
currentTtsSpeech = ttsRequest.speech
currentTtsLanguage = ttsLanguageMap[ttsRequest]
} }
TtsRequest.Status.COMPLETED, TtsRequest.Status.COMPLETED,
TtsRequest.Status.CANCELED, TtsRequest.Status.CANCELED,
TtsRequest.Status.ERROR, TtsRequest.Status.ERROR,
TtsRequest.Status.NOT_ALLOWED -> { TtsRequest.Status.NOT_ALLOWED -> {
isTtsBusy = false isTtsBusy = false
processNextTts() currentTtsSpeech = null
currentTtsLanguage = null
if (!isTtsPaused) {
processNextTts()
}
} }
else -> {} else -> {}
} }
@@ -311,7 +428,7 @@ class MqttManager(
} }
private fun processNextTts() { private fun processNextTts() {
if (!isTtsBusy && ttsQueue.isNotEmpty()) { if (!isTtsBusy && !isTtsPaused && ttsQueue.isNotEmpty()) {
val next = ttsQueue.poll() val next = ttsQueue.poll()
if (next != null) { if (next != null) {
isTtsBusy = true isTtsBusy = true

View File

@@ -40,16 +40,17 @@ class NavController(private val robot: Robot) {
return true return true
} }
fun randomPatrol() { fun randomPatrol(): List<String> {
val allLocations = getAllLocations() val allLocations = getAllLocations()
val availablePatrolLocations = allLocations.filter { !it.equals("home base", ignoreCase = true) } val availablePatrolLocations = allLocations.filter { !it.equals("home base", ignoreCase = true) }
if (availablePatrolLocations.size < 3) { if (availablePatrolLocations.size < 3) {
Log.w(TAG, "Patrol command ignored: Not enough valid locations (excluding home base). Need at least 3, but found ${availablePatrolLocations.size}.") Log.w(TAG, "Patrol command ignored: Not enough valid locations (excluding home base). Need at least 3, but found ${availablePatrolLocations.size}.")
return return emptyList()
} }
val patrolCount = (3..minOf(6, availablePatrolLocations.size)).random() val patrolCount = (3..minOf(6, availablePatrolLocations.size)).random()
val patrolLocations = availablePatrolLocations.shuffled().take(patrolCount) val patrolLocations = availablePatrolLocations.shuffled().take(patrolCount)
Log.i(TAG, "Starting random patrol with $patrolCount locations: ${patrolLocations.joinToString()}.") Log.i(TAG, "Starting random patrol with $patrolCount locations: ${patrolLocations.joinToString()}.")
NavPatrol(patrolLocations, false, 1, 5) NavPatrol(patrolLocations, false, 1, 5)
return patrolLocations
} }
} }

View File

@@ -12,10 +12,12 @@ import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.WindowManager import android.view.WindowManager
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.ArrayAdapter
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import com.example.lzwcai_terminal_temi.databinding.ActivitySettingsBinding import com.example.lzwcai_terminal_temi.databinding.ActivitySettingsBinding
import kotlin.system.exitProcess import kotlin.system.exitProcess
import com.robotemi.sdk.Robot
import android.graphics.drawable.GradientDrawable import android.graphics.drawable.GradientDrawable
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
@@ -23,6 +25,9 @@ class SettingsActivity : AppCompatActivity() {
private lateinit var binding: ActivitySettingsBinding private lateinit var binding: ActivitySettingsBinding
private var restartAnimator: ValueAnimator? = null private var restartAnimator: ValueAnimator? = null
private lateinit var robot: Robot
private lateinit var locationAdapter: ArrayAdapter<String>
private val currentLocationKey = "current_location"
private lateinit var prefs: SharedPreferences private lateinit var prefs: SharedPreferences
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -31,6 +36,7 @@ class SettingsActivity : AppCompatActivity() {
setContentView(binding.root) setContentView(binding.root)
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN) window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN)
robot = Robot.getInstance()
prefs = getSharedPreferences("app_prefs", Context.MODE_PRIVATE) prefs = getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
val savedIp = prefs.getString("network_ip", "") val savedIp = prefs.getString("network_ip", "")
@@ -48,7 +54,6 @@ class SettingsActivity : AppCompatActivity() {
if (ip.isNotEmpty()) { if (ip.isNotEmpty()) {
prefs.edit().putString("network_ip", ip).apply() prefs.edit().putString("network_ip", ip).apply()
Toast.makeText(this, getString(R.string.msg_ip_saved, ip), Toast.LENGTH_SHORT).show() Toast.makeText(this, getString(R.string.msg_ip_saved, ip), Toast.LENGTH_SHORT).show()
Log.i("SettingsActivity", "IP Saved: $ip")
finish() finish()
} else { } else {
Toast.makeText(this, getString(R.string.msg_invalid_ip), Toast.LENGTH_SHORT).show() Toast.makeText(this, getString(R.string.msg_invalid_ip), Toast.LENGTH_SHORT).show()
@@ -63,16 +68,23 @@ class SettingsActivity : AppCompatActivity() {
setupRestartButton() setupRestartButton()
setupSpecialTaskSwitch() setupSpecialTaskSwitch()
setupLocationSelector()
}
override fun onResume() {
super.onResume()
refreshLocationList()
} }
private fun setupSpecialTaskSwitch() { private fun setupSpecialTaskSwitch() {
val isSpecialTaskMode = prefs.getBoolean("special_task_mode", false) val isSpecialTaskMode = prefs.getBoolean("special_task_mode", false)
binding.switchSpecialTask.setOnCheckedChangeListener(null)
binding.switchSpecialTask.isChecked = isSpecialTaskMode binding.switchSpecialTask.isChecked = isSpecialTaskMode
updateStatusIndicator(isSpecialTaskMode) updateStatusIndicator(isSpecialTaskMode)
binding.switchSpecialTask.setOnCheckedChangeListener { _, isChecked -> binding.switchSpecialTask.setOnCheckedChangeListener { _, isChecked ->
prefs.edit().putBoolean("special_task_mode", isChecked).apply() prefs.edit().putBoolean("special_task_mode", isChecked).apply()
updateStatusIndicator(isChecked) updateStatusIndicator(isChecked)
Log.i("SettingsActivity", "Special Task Mode changed to: $isChecked")
} }
} }
@@ -130,6 +142,45 @@ class SettingsActivity : AppCompatActivity() {
binding.restartProgressBar.visibility = View.INVISIBLE binding.restartProgressBar.visibility = View.INVISIBLE
} }
private fun setupLocationSelector() {
val items = buildLocationList()
locationAdapter = ArrayAdapter(this, R.layout.item_dropdown, items)
binding.etCurrentLocation.setAdapter(locationAdapter)
val selection = resolveCurrentLocation(items)
binding.etCurrentLocation.setText(selection, false)
binding.etCurrentLocation.setOnItemClickListener { _, _, position, _ ->
val selected = locationAdapter.getItem(position) ?: getString(R.string.location_unknown)
prefs.edit().putString(currentLocationKey, selected).apply()
}
}
private fun refreshLocationList() {
if (!::locationAdapter.isInitialized) {
return
}
val items = buildLocationList()
locationAdapter.clear()
locationAdapter.addAll(items)
locationAdapter.notifyDataSetChanged()
val selection = resolveCurrentLocation(items)
binding.etCurrentLocation.setText(selection, false)
}
private fun resolveCurrentLocation(items: List<String>): String {
val unknown = getString(R.string.location_unknown)
val saved = prefs.getString(currentLocationKey, unknown) ?: unknown
return if (items.contains(saved)) saved else unknown
}
private fun buildLocationList(): List<String> {
val unknown = getString(R.string.location_unknown)
val locations = runCatching { robot.locations }.getOrDefault(emptyList())
val unique = LinkedHashSet<String>()
unique.add(unknown)
locations.map { it.trim() }.filter { it.isNotEmpty() }.forEach { unique.add(it) }
return unique.toList()
}
private fun restartApplication() { private fun restartApplication() {
val intent = packageManager.getLaunchIntentForPackage(packageName) val intent = packageManager.getLaunchIntentForPackage(packageName)
val pendingIntent = PendingIntent.getActivity(this, 123456, intent, PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE) val pendingIntent = PendingIntent.getActivity(this, 123456, intent, PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE)

View File

@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background_dark"
tools:context=".MainActivity">
<ImageButton
android:id="@+id/btnSettings"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="24dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/btn_settings"
android:src="@android:drawable/ic_menu_manage"
app:tint="@color/text_secondary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.example.lzwcai_terminal_temi.AnimatedEmojiView
android:id="@+id/animatedEmojiView"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.5"
app:layout_constraintHeight_percent="0.8" />
<Button
android:id="@+id/btnReception"
style="@style/Widget.App.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="40dp"
android:paddingStart="48dp"
android:paddingTop="16dp"
android:paddingEnd="48dp"
android:paddingBottom="16dp"
android:text="是的"
android:textSize="20sp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,278 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background_dark"
tools:context=".SettingsActivity">
<!-- Header -->
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/headerLayout"
android:layout_width="match_parent"
android:layout_height="80dp"
android:background="@color/surface_dark"
app:layout_constraintTop_toTopOf="parent">
<ImageButton
android:id="@+id/btnBack"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_marginStart="16dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/btn_back"
android:src="@android:drawable/ic_menu_revert"
app:tint="@color/text_primary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tvSettingsTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/title_settings"
android:textColor="@color/text_primary"
android:textSize="28sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:fillViewport="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/headerLayout">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="24dp"
android:baselineAligned="false">
<!-- Left Column: Network Config -->
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:layout_marginEnd="16dp">
<androidx.cardview.widget.CardView
style="@style/CardView.App"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
android:text="@string/label_ip_config"
android:textColor="@color/text_primary"
android:textSize="22sp"
android:textStyle="bold" />
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.App.TextInputLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/hint_ip_address">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etIpAddress"
android:layout_width="match_parent"
android:layout_height="80dp"
android:inputType="number|numberDecimal"
android:digits="0123456789."
android:textSize="20sp"
android:textColor="@color/text_primary" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/btnSave"
style="@style/Widget.App.Button"
android:layout_width="match_parent"
android:layout_height="72dp"
android:layout_marginTop="24dp"
android:textSize="20sp"
android:text="@string/btn_save" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
style="@style/CardView.App"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="@string/label_location_config"
android:textColor="@color/text_primary"
android:textSize="22sp"
android:textStyle="bold" />
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.App.TextInputLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:endIconMode="dropdown_menu"
android:hint="@string/hint_current_location">
<com.google.android.material.textfield.MaterialAutoCompleteTextView
android:id="@+id/etCurrentLocation"
android:layout_width="match_parent"
android:layout_height="80dp"
android:inputType="none"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:focusable="false"
android:clickable="true"
android:gravity="center_vertical"
android:textColor="@color/text_primary"
android:textSize="20sp" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
<!-- Right Column: Mode Config & System Actions -->
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:layout_marginStart="16dp">
<androidx.cardview.widget.CardView
style="@style/CardView.App"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp">
<LinearLayout
android:id="@+id/specialTaskLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="24dp">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="特殊任务模式"
android:textColor="@color/text_primary"
android:textSize="22sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="启用特定场景下的任务逻辑"
android:textColor="@color/text_secondary"
android:textSize="16sp" />
</LinearLayout>
<View
android:id="@+id/statusIndicator"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginEnd="24dp"
android:background="@drawable/status_indicator" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/switchSpecialTask"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:scaleX="1.5"
android:scaleY="1.5" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
style="@style/CardView.App"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
android:padding="24dp">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Button
android:id="@+id/btnRestart"
style="@style/Widget.App.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="72dp"
android:textSize="20sp"
android:text="@string/btn_restart_app" />
<ProgressBar
android:id="@+id/restartProgressBar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="6dp"
android:layout_gravity="bottom"
android:indeterminate="false"
android:max="100"
android:progressDrawable="@drawable/custom_progress_bar"
android:visibility="invisible" />
</FrameLayout>
<TextView
android:id="@+id/tvVersion"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="Version: --"
android:textColor="@color/text_secondary"
android:textSize="14sp" />
</LinearLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
</LinearLayout>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -11,15 +11,15 @@
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/headerLayout" android:id="@+id/headerLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" android:layout_height="80dp"
android:background="@color/surface_dark" android:background="@color/surface_dark"
app:layout_constraintTop_toTopOf="parent"> app:layout_constraintTop_toTopOf="parent">
<ImageButton <ImageButton
android:id="@+id/btnBack" android:id="@+id/btnBack"
android:layout_width="48dp" android:layout_width="64dp"
android:layout_height="48dp" android:layout_height="64dp"
android:layout_marginStart="8dp" android:layout_marginStart="16dp"
android:background="?attr/selectableItemBackgroundBorderless" android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/btn_back" android:contentDescription="@string/btn_back"
android:src="@android:drawable/ic_menu_revert" android:src="@android:drawable/ic_menu_revert"
@@ -34,7 +34,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/title_settings" android:text="@string/title_settings"
android:textColor="@color/text_primary" android:textColor="@color/text_primary"
android:textSize="20sp" android:textSize="28sp"
android:textStyle="bold" android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
@@ -53,27 +53,28 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:padding="16dp"> android:padding="24dp">
<!-- Network Config Card --> <!-- Network Config Card -->
<androidx.cardview.widget.CardView <androidx.cardview.widget.CardView
style="@style/CardView.App" style="@style/CardView.App"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="16dp"> android:layout_marginBottom="24dp">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"> android:orientation="vertical"
android:padding="16dp">
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="16dp" android:layout_marginBottom="24dp"
android:text="@string/label_ip_config" android:text="@string/label_ip_config"
android:textColor="@color/text_primary" android:textColor="@color/text_primary"
android:textSize="18sp" android:textSize="22sp"
android:textStyle="bold" /> android:textStyle="bold" />
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
@@ -85,9 +86,10 @@
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:id="@+id/etIpAddress" android:id="@+id/etIpAddress"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="80dp"
android:inputType="number|numberDecimal" android:inputType="number|numberDecimal"
android:digits="0123456789." android:digits="0123456789."
android:textSize="20sp"
android:textColor="@color/text_primary" /> android:textColor="@color/text_primary" />
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
@@ -95,26 +97,73 @@
android:id="@+id/btnSave" android:id="@+id/btnSave"
style="@style/Widget.App.Button" style="@style/Widget.App.Button"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="72dp"
android:layout_marginTop="16dp" android:layout_marginTop="24dp"
android:textSize="20sp"
android:text="@string/btn_save" /> android:text="@string/btn_save" />
</LinearLayout> </LinearLayout>
</androidx.cardview.widget.CardView> </androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
style="@style/CardView.App"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="@string/label_location_config"
android:textColor="@color/text_primary"
android:textSize="22sp"
android:textStyle="bold" />
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.App.TextInputLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:endIconMode="dropdown_menu"
android:hint="@string/hint_current_location">
<com.google.android.material.textfield.MaterialAutoCompleteTextView
android:id="@+id/etCurrentLocation"
android:layout_width="match_parent"
android:layout_height="72dp"
android:inputType="none"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:focusable="false"
android:clickable="true"
android:gravity="center_vertical"
android:textColor="@color/text_primary"
android:textSize="20sp" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
<!-- Mode Config Card --> <!-- Mode Config Card -->
<androidx.cardview.widget.CardView <androidx.cardview.widget.CardView
style="@style/CardView.App" style="@style/CardView.App"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="16dp"> android:layout_marginBottom="24dp">
<LinearLayout <LinearLayout
android:id="@+id/specialTaskLayout" android:id="@+id/specialTaskLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="horizontal"> android:orientation="horizontal"
android:padding="16dp">
<LinearLayout <LinearLayout
android:layout_width="0dp" android:layout_width="0dp"
@@ -127,29 +176,31 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="特殊任务模式" android:text="特殊任务模式"
android:textColor="@color/text_primary" android:textColor="@color/text_primary"
android:textSize="18sp" android:textSize="22sp"
android:textStyle="bold" /> android:textStyle="bold" />
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="4dp" android:layout_marginTop="8dp"
android:text="启用特定场景下的任务逻辑" android:text="启用特定场景下的任务逻辑"
android:textColor="@color/text_secondary" android:textColor="@color/text_secondary"
android:textSize="14sp" /> android:textSize="16sp" />
</LinearLayout> </LinearLayout>
<View <View
android:id="@+id/statusIndicator" android:id="@+id/statusIndicator"
android:layout_width="12dp" android:layout_width="16dp"
android:layout_height="12dp" android:layout_height="16dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="24dp"
android:background="@drawable/status_indicator" /> android:background="@drawable/status_indicator" />
<com.google.android.material.switchmaterial.SwitchMaterial <com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/switchSpecialTask" android:id="@+id/switchSpecialTask"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" /> android:layout_height="wrap_content"
android:scaleX="1.5"
android:scaleY="1.5" />
</LinearLayout> </LinearLayout>
</androidx.cardview.widget.CardView> </androidx.cardview.widget.CardView>
@@ -159,13 +210,14 @@
style="@style/CardView.App" style="@style/CardView.App"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="24dp"> android:layout_marginBottom="32dp">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center" android:gravity="center"
android:orientation="vertical"> android:orientation="vertical"
android:padding="16dp">
<FrameLayout <FrameLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -175,14 +227,15 @@
android:id="@+id/btnRestart" android:id="@+id/btnRestart"
style="@style/Widget.App.OutlinedButton" style="@style/Widget.App.OutlinedButton"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="72dp"
android:textSize="20sp"
android:text="@string/btn_restart_app" /> android:text="@string/btn_restart_app" />
<ProgressBar <ProgressBar
android:id="@+id/restartProgressBar" android:id="@+id/restartProgressBar"
style="?android:attr/progressBarStyleHorizontal" style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="4dp" android:layout_height="6dp"
android:layout_gravity="bottom" android:layout_gravity="bottom"
android:indeterminate="false" android:indeterminate="false"
android:max="100" android:max="100"
@@ -194,10 +247,10 @@
android:id="@+id/tvVersion" android:id="@+id/tvVersion"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp" android:layout_marginTop="24dp"
android:text="Version: --" android:text="Version: --"
android:textColor="@color/text_secondary" android:textColor="@color/text_secondary"
android:textSize="12sp" /> android:textSize="14sp" />
</LinearLayout> </LinearLayout>
</androidx.cardview.widget.CardView> </androidx.cardview.widget.CardView>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@android:id/text1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:gravity="center_vertical"
android:minHeight="?attr/listPreferredItemHeight"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:singleLine="true"
android:textAppearance="?attr/textAppearanceSubtitle1"
android:textColor="@color/text_primary" />

View File

@@ -9,13 +9,13 @@
<color name="white">#FFFFFFFF</color> <color name="white">#FFFFFFFF</color>
<!-- New Theme Colors --> <!-- New Theme Colors -->
<color name="background_dark">#121212</color> <color name="background_dark">#FFFFFFFF</color> <!-- Changed to White -->
<color name="surface_dark">#1E1E1E</color> <color name="surface_dark">#FFF0F0F0</color> <!-- Changed to Light Gray for cards -->
<color name="primary_teal">#03DAC6</color> <color name="primary_teal">#03DAC6</color>
<color name="primary_variant_teal">#018786</color> <color name="primary_variant_teal">#018786</color>
<color name="secondary_purple">#BB86FC</color> <color name="secondary_purple">#BB86FC</color>
<color name="text_primary">#FFFFFF</color> <color name="text_primary">#FF000000</color> <!-- Changed to Black -->
<color name="text_secondary">#B0B0B0</color> <color name="text_secondary">#FF757575</color> <!-- Changed to Dark Gray -->
<color name="divider">#2C2C2C</color> <color name="divider">#E0E0E0</color>
<color name="input_background">#2C2C2C</color> <color name="input_background">#F5F5F5</color>
</resources> </resources>

View File

@@ -14,4 +14,7 @@
<string name="btn_random_expression">随机表情</string> <string name="btn_random_expression">随机表情</string>
<string name="btn_speak">让机器人说话</string> <string name="btn_speak">让机器人说话</string>
<string name="btn_restart_app">长按重启应用</string> <string name="btn_restart_app">长按重启应用</string>
<string name="label_location_config">当前位置</string>
<string name="hint_current_location">请选择当前地点</string>
<string name="location_unknown">未知</string>
</resources> </resources>