feat(设置): 新增当前位置选择器并优化UI与交互
- 在设置页面添加基于机器人位置列表的下拉选择器,支持手动设置当前位置 - 改进特殊任务模式的开关逻辑,避免与当前任务状态冲突 - 优化MQTT指令处理,新增terminate、continue命令,完善TTS暂停/恢复机制 - 添加表情动画的眨眼效果,改进导航到达和巡逻模式的状态管理 - 重构颜色主题为浅色风格,并添加横屏布局支持 - 更新README文档,补充MQTT指令说明和本地验证步骤
This commit is contained in:
27
README.md
27
README.md
@@ -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 或真机进行调试预览。
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (currentTask == "patrol") {
|
||||||
|
binding.animatedEmojiView.currentExpression = AnimatedEmojiView.Expression.ANGRY
|
||||||
|
} else {
|
||||||
binding.animatedEmojiView.currentExpression = AnimatedEmojiView.Expression.SMILE
|
binding.animatedEmojiView.currentExpression = AnimatedEmojiView.Expression.SMILE
|
||||||
// applyFaceScale(fixedFaceScale) // Use XML constraints for layout
|
}
|
||||||
|
|
||||||
binding.btnReception.setOnClickListener {
|
binding.btnReception.setOnClickListener {
|
||||||
val destination = receptionDestination
|
val destination = receptionDestination
|
||||||
@@ -96,10 +114,14 @@ 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()
|
||||||
|
if (mqttManager == null) {
|
||||||
|
updateMqttConnection()
|
||||||
|
} else {
|
||||||
mqttManager?.connect()
|
mqttManager?.connect()
|
||||||
}
|
}
|
||||||
|
startBlinking()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
super.onStop()
|
super.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 == "special_task_mode") {
|
if (key == "current_location") {
|
||||||
if (sharedPreferences?.getBoolean("special_task_mode", false) == true) {
|
lastArrivalLocation = sharedPreferences?.getString("current_location", null)
|
||||||
setCurrentTask("special")
|
Log.i("MainActivity", "Current location updated manually: $lastArrivalLocation")
|
||||||
} else if (currentTask == "special") {
|
|
||||||
setCurrentTask("")
|
|
||||||
}
|
}
|
||||||
|
if (key == "special_task_mode") {
|
||||||
|
val isSpecial = sharedPreferences?.getBoolean("special_task_mode", false) == true
|
||||||
|
Log.i("MainActivity", "Special mode pref changed: $isSpecial, currentTask: $currentTask")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,10 +55,12 @@ 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")
|
||||||
|
if (payload != null) {
|
||||||
handleIncomingMessage(topic, payload)
|
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" -> {
|
||||||
|
scope.launch(Dispatchers.Main) {
|
||||||
(context as? MainActivity)?.setCurrentTask("")
|
(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", "会议室")
|
||||||
|
scope.launch(Dispatchers.Main) {
|
||||||
(context as? MainActivity)?.startReceptionMode(location, text, destination)
|
(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,21 +408,27 @@ 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
|
||||||
|
currentTtsSpeech = null
|
||||||
|
currentTtsLanguage = null
|
||||||
|
if (!isTtsPaused) {
|
||||||
processNextTts()
|
processNextTts()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
52
app/src/main/res/layout-land/activity_main.xml
Normal file
52
app/src/main/res/layout-land/activity_main.xml
Normal 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>
|
||||||
278
app/src/main/res/layout-land/activity_settings.xml
Normal file
278
app/src/main/res/layout-land/activity_settings.xml
Normal 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>
|
||||||
@@ -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>
|
||||||
|
|||||||
13
app/src/main/res/layout/item_dropdown.xml
Normal file
13
app/src/main/res/layout/item_dropdown.xml
Normal 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" />
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user