feat: 添加设备激活功能并重构网络配置
- 引入设备激活流程,支持通过激活码和服务端配置激活设备 - 重构网络配置,将IP地址配置改为base_url配置 - 新增激活状态UI显示和激活提示横幅 - 添加ConnectionCoordinator统一管理MQTT和LiveKit连接 - 新增RobotEventHandler处理机器人状态和位置标准化 - 新增UiState类集中管理UI状态更新 - 在设置页面添加关于对话框显示设备信息 - 更新网络安全配置,限制明文流量仅允许本地地址
This commit is contained in:
@@ -15,10 +15,11 @@
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Lzwcaiterminaltemi"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:usesCleartextTraffic="false"
|
||||
tools:targetApi="31">
|
||||
<meta-data
|
||||
android:name="com.robotemi.sdk.metadata.SKILL"
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
package com.example.lzwcai_terminal_temi
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Log
|
||||
import com.robotemi.sdk.Robot
|
||||
import java.net.URL
|
||||
|
||||
class ConnectionCoordinator(
|
||||
private val context: Context,
|
||||
private val prefs: SharedPreferences,
|
||||
private val robot: Robot,
|
||||
private val navController: NavController,
|
||||
private val liveKitManagerProvider: () -> LiveKitManager?,
|
||||
private val mqttStatusListener: (Boolean) -> Unit,
|
||||
private val liveKitStatusListener: (Boolean) -> Unit,
|
||||
private val hasAudioPermission: () -> Boolean,
|
||||
private val hasCameraPermission: () -> Boolean,
|
||||
private val requestMediaPermissions: () -> Unit,
|
||||
private val buildLiveKitToken: (room: String, savedToken: String) -> String,
|
||||
private val onSetCurrentTask: (String) -> Unit,
|
||||
private val onMarkSpeechTaskActive: () -> Unit,
|
||||
private val onClearSpeechTaskIfActive: () -> Unit,
|
||||
private val onStartNotificationMode: (location: String, text: String) -> Unit,
|
||||
private val onStartPatrolMode: (route: List<String>, times: Int, waiting: Int, nonStop: Boolean) -> Unit,
|
||||
private val onStartReceptionMode: (location: String, text: String, destination: String) -> Unit,
|
||||
private val onPublishStatusSnapshot: (reason: String, force: Boolean) -> Unit
|
||||
) {
|
||||
private var mqttManager: MqttManager? = null
|
||||
|
||||
fun handleTtsStatusChange(ttsRequest: com.robotemi.sdk.TtsRequest) {
|
||||
mqttManager?.handleTtsStatusChange(ttsRequest)
|
||||
}
|
||||
|
||||
fun publish(topic: String, payload: String) {
|
||||
mqttManager?.publish(topic, payload)
|
||||
}
|
||||
|
||||
fun connectMqttIfNeeded() {
|
||||
mqttManager?.connect()
|
||||
}
|
||||
|
||||
fun disconnectMqtt() {
|
||||
mqttManager?.disconnect()
|
||||
}
|
||||
|
||||
fun disconnectLiveKit() {
|
||||
liveKitManagerProvider()?.disconnect()
|
||||
}
|
||||
|
||||
fun release() {
|
||||
mqttManager?.disconnect()
|
||||
liveKitManagerProvider()?.release()
|
||||
}
|
||||
|
||||
fun updateMqttConnection(isActivated: Boolean) {
|
||||
mqttManager?.shutdown()
|
||||
if (!isActivated) {
|
||||
mqttManager = null
|
||||
mqttStatusListener(false)
|
||||
return
|
||||
}
|
||||
val host = resolveBrokerHostFromBaseUrl()
|
||||
if (host.isNullOrEmpty()) {
|
||||
mqttManager = null
|
||||
mqttStatusListener(false)
|
||||
Log.w("ConnectionCoordinator", "MQTT disabled: base_url is invalid or not set.")
|
||||
return
|
||||
}
|
||||
mqttManager = MqttManager(
|
||||
context = context,
|
||||
serverIp = host,
|
||||
robot = robot,
|
||||
navController = navController,
|
||||
statusListener = mqttStatusListener,
|
||||
onSetCurrentTask = onSetCurrentTask,
|
||||
onMarkSpeechTaskActive = onMarkSpeechTaskActive,
|
||||
onClearSpeechTaskIfActive = onClearSpeechTaskIfActive,
|
||||
onStartNotificationMode = onStartNotificationMode,
|
||||
onStartPatrolMode = onStartPatrolMode,
|
||||
onStartReceptionMode = onStartReceptionMode,
|
||||
onPublishStatusSnapshot = onPublishStatusSnapshot
|
||||
)
|
||||
mqttManager?.connect()
|
||||
Log.i("ConnectionCoordinator", "MQTT updated with host=$host")
|
||||
}
|
||||
|
||||
fun updateLiveKitConnection(isActivated: Boolean) {
|
||||
if (!isActivated) {
|
||||
liveKitManagerProvider()?.disconnect()
|
||||
liveKitStatusListener(false)
|
||||
return
|
||||
}
|
||||
val enabled = prefs.getBoolean(LiveKitManager.PREF_KEY_ENABLED, true)
|
||||
val url = resolveLiveKitUrl()
|
||||
val room = prefs.getString(LiveKitManager.PREF_KEY_ROOM, LiveKitManager.DEFAULT_ROOM).orEmpty()
|
||||
val savedToken = prefs.getString(LiveKitManager.PREF_KEY_TOKEN, "").orEmpty()
|
||||
val token = buildLiveKitToken(room, savedToken)
|
||||
if (!enabled || url.isBlank() || room.isBlank()) {
|
||||
liveKitManagerProvider()?.disconnect()
|
||||
liveKitStatusListener(false)
|
||||
return
|
||||
}
|
||||
if (!hasAudioPermission() || !hasCameraPermission()) {
|
||||
liveKitStatusListener(false)
|
||||
requestMediaPermissions()
|
||||
return
|
||||
}
|
||||
liveKitStatusListener(false)
|
||||
liveKitManagerProvider()?.connect(url, token, true, true)
|
||||
}
|
||||
|
||||
fun updateLiveKitStatusSnapshot(isActivated: Boolean) {
|
||||
if (!isActivated) {
|
||||
liveKitStatusListener(false)
|
||||
return
|
||||
}
|
||||
val enabled = prefs.getBoolean(LiveKitManager.PREF_KEY_ENABLED, true)
|
||||
val url = resolveLiveKitUrl()
|
||||
val room = prefs.getString(LiveKitManager.PREF_KEY_ROOM, LiveKitManager.DEFAULT_ROOM).orEmpty()
|
||||
if (!enabled || url.isBlank() || room.isBlank() || !hasAudioPermission() || !hasCameraPermission()) {
|
||||
liveKitStatusListener(false)
|
||||
return
|
||||
}
|
||||
liveKitStatusListener(false)
|
||||
}
|
||||
|
||||
private fun resolveLiveKitUrl(): String {
|
||||
val savedUrl = prefs.getString(LiveKitManager.PREF_KEY_URL, "").orEmpty().trim()
|
||||
if (savedUrl.isNotEmpty()) {
|
||||
return savedUrl
|
||||
}
|
||||
return LiveKitManager.DEFAULT_URL
|
||||
}
|
||||
|
||||
private fun resolveBrokerHostFromBaseUrl(): String? {
|
||||
val baseUrl = prefs.getString(HttpManager.PREF_KEY_BASE_URL, "").orEmpty().trim()
|
||||
if (baseUrl.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
return runCatching { URL(baseUrl).host }
|
||||
.getOrNull()
|
||||
?.trim()
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
}
|
||||
}
|
||||
@@ -12,23 +12,137 @@ import java.net.URL
|
||||
|
||||
object HttpManager {
|
||||
private const val TAG = "HttpManager"
|
||||
const val PREF_KEY_BASE_URL = "base_url"
|
||||
const val PREF_KEY_LOGIN_USERNAME = "login_username"
|
||||
const val PREF_KEY_LOGIN_PASSWORD = "login_password"
|
||||
const val PREF_KEY_DEVICE_ID = "device_id"
|
||||
const val PREF_KEY_ACTIVATION_CODE = "activation_code"
|
||||
const val PREF_KEY_DEVICE_NAME = "device_name"
|
||||
const val PREF_KEY_ACTIVATED = "is_activated"
|
||||
const val PREF_KEY_MQTT_USERNAME = "mqtt_username"
|
||||
const val PREF_KEY_MQTT_PASSWORD = "mqtt_password"
|
||||
const val PREF_KEY_OD_WFID = "od_wfid"
|
||||
const val PREF_KEY_OD_WF_KEY = "od_wf_key"
|
||||
const val PREF_KEY_CD_WFID = "cd_wfid"
|
||||
const val PREF_KEY_CD_WF_KEY = "cd_wf_key"
|
||||
|
||||
fun getBaseUrl(context: Context): String {
|
||||
val prefs = context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
|
||||
val saved = prefs.getString(PREF_KEY_BASE_URL, "").orEmpty().trim()
|
||||
return saved.trimEnd('/')
|
||||
}
|
||||
|
||||
suspend fun login(context: Context): String? = withContext(Dispatchers.IO) {
|
||||
val prefs = context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
|
||||
val username = prefs.getString(PREF_KEY_LOGIN_USERNAME, "").orEmpty().trim()
|
||||
val password = prefs.getString(PREF_KEY_LOGIN_PASSWORD, "").orEmpty()
|
||||
if (username.isEmpty() || password.isEmpty()) {
|
||||
Log.w(TAG, "Login skipped: username or password is empty.")
|
||||
return@withContext null
|
||||
}
|
||||
val body = JSONObject()
|
||||
.put("username", username)
|
||||
.put("password", password)
|
||||
.put("loginType", "user")
|
||||
val response = postJson(context, "/login", body, token = null)
|
||||
if (response == null) {
|
||||
return@withContext null
|
||||
}
|
||||
val code = response.optInt("code", -1)
|
||||
if (code != 200) {
|
||||
Log.e(TAG, "Login failed: code=$code, msg=${response.optString("msg")}")
|
||||
return@withContext null
|
||||
}
|
||||
return@withContext response.optString("token", "").trim().ifEmpty { null }
|
||||
}
|
||||
|
||||
suspend fun activateDevice(
|
||||
context: Context,
|
||||
activationCode: String,
|
||||
deviceName: String,
|
||||
deviceId: String
|
||||
): Boolean = withContext(Dispatchers.IO) {
|
||||
val token = login(context) ?: return@withContext false
|
||||
val body = JSONObject()
|
||||
.put("activationCode", activationCode)
|
||||
.put("deviceName", deviceName)
|
||||
.put("deviceTypeName", "轮足机器人")
|
||||
.put("deviceId", deviceId)
|
||||
val response = postJson(context, "/system/serverConfig/deviceActivate", body, token)
|
||||
if (response == null) {
|
||||
return@withContext false
|
||||
}
|
||||
val code = response.optInt("code", -1)
|
||||
if (code != 200) {
|
||||
Log.e(TAG, "Activation failed: code=$code, msg=${response.optString("msg")}")
|
||||
return@withContext false
|
||||
}
|
||||
return@withContext true
|
||||
}
|
||||
|
||||
suspend fun fetchRuntimeConfigs(context: Context): Map<String, String>? = withContext(Dispatchers.IO) {
|
||||
val token = login(context) ?: return@withContext null
|
||||
val body = JSONArray()
|
||||
.put(PREF_KEY_MQTT_USERNAME)
|
||||
.put(PREF_KEY_OD_WFID)
|
||||
.put(PREF_KEY_OD_WF_KEY)
|
||||
.put(PREF_KEY_CD_WFID)
|
||||
.put(PREF_KEY_CD_WF_KEY)
|
||||
.put(PREF_KEY_MQTT_PASSWORD)
|
||||
val response = postJsonArray(context, "/system/config/getConfig", body, token)
|
||||
if (response == null) {
|
||||
return@withContext null
|
||||
}
|
||||
val code = response.optInt("code", -1)
|
||||
if (code != 200) {
|
||||
Log.e(TAG, "Get runtime config failed: code=$code, msg=${response.optString("msg")}")
|
||||
return@withContext null
|
||||
}
|
||||
val data = response.optJSONArray("data") ?: return@withContext null
|
||||
val result = mutableMapOf<String, String>()
|
||||
for (i in 0 until data.length()) {
|
||||
val item = data.optJSONObject(i) ?: continue
|
||||
val key = item.optString("configKey", "").trim()
|
||||
if (key.isEmpty()) {
|
||||
continue
|
||||
}
|
||||
result[key] = item.optString("configValue", "")
|
||||
}
|
||||
return@withContext result
|
||||
}
|
||||
|
||||
suspend fun fetchMqttCredentials(context: Context): Pair<String, String>? = withContext(Dispatchers.IO) {
|
||||
val configs = fetchRuntimeConfigs(context) ?: return@withContext null
|
||||
val username = configs[PREF_KEY_MQTT_USERNAME].orEmpty()
|
||||
val password = configs[PREF_KEY_MQTT_PASSWORD].orEmpty()
|
||||
if (username.isBlank() || password.isBlank()) {
|
||||
Log.e(TAG, "Get MQTT config failed: username/password empty.")
|
||||
return@withContext null
|
||||
}
|
||||
return@withContext username to password
|
||||
}
|
||||
|
||||
suspend fun workflow_execute(context: Context, apiKey: String, workflowId: String, inputs: Any): String? = withContext(Dispatchers.IO) {
|
||||
var result: String? = null
|
||||
try {
|
||||
val prefs = context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
|
||||
val ip = prefs.getString("network_ip", "") ?: ""
|
||||
if (ip.isEmpty()) {
|
||||
Log.e(TAG, "No IP address configured")
|
||||
val baseUrl = getBaseUrl(context)
|
||||
if (baseUrl.isEmpty()) {
|
||||
Log.e(TAG, "No base_url configured")
|
||||
return@withContext null
|
||||
}
|
||||
val workflowUrl = "http://$ip:8088/open/workflow/execute"
|
||||
val token = login(context)
|
||||
if (token.isNullOrBlank()) {
|
||||
Log.e(TAG, "Workflow execute skipped: login token missing.")
|
||||
return@withContext null
|
||||
}
|
||||
val workflowUrl = "$baseUrl/open/workflow/execute"
|
||||
|
||||
val url = URL(workflowUrl)
|
||||
val connection = url.openConnection() as HttpURLConnection
|
||||
connection.requestMethod = "POST"
|
||||
connection.setRequestProperty("Content-Type", "application/json")
|
||||
connection.setRequestProperty("X-API-Key", apiKey)
|
||||
connection.setRequestProperty("token", token)
|
||||
connection.doOutput = true
|
||||
connection.doInput = true
|
||||
connection.connectTimeout = 10000
|
||||
@@ -65,4 +179,62 @@ object HttpManager {
|
||||
}
|
||||
return@withContext result
|
||||
}
|
||||
|
||||
private fun postJson(context: Context, path: String, body: JSONObject, token: String?): JSONObject? {
|
||||
val baseUrl = getBaseUrl(context)
|
||||
if (baseUrl.isEmpty()) {
|
||||
Log.e(TAG, "Request skipped: base_url is empty.")
|
||||
return null
|
||||
}
|
||||
val finalUrl = "$baseUrl$path"
|
||||
return request(finalUrl, body.toString(), token)
|
||||
}
|
||||
|
||||
private fun postJsonArray(context: Context, path: String, body: JSONArray, token: String?): JSONObject? {
|
||||
val baseUrl = getBaseUrl(context)
|
||||
if (baseUrl.isEmpty()) {
|
||||
Log.e(TAG, "Request skipped: base_url is empty.")
|
||||
return null
|
||||
}
|
||||
val finalUrl = "$baseUrl$path"
|
||||
return request(finalUrl, body.toString(), token)
|
||||
}
|
||||
|
||||
private fun request(urlText: String, bodyText: String, token: String?): JSONObject? {
|
||||
var connection: HttpURLConnection? = null
|
||||
return try {
|
||||
val connectionUrl = URL(urlText)
|
||||
connection = connectionUrl.openConnection() as HttpURLConnection
|
||||
connection.requestMethod = "POST"
|
||||
connection.setRequestProperty("Content-Type", "application/json")
|
||||
if (!token.isNullOrBlank()) {
|
||||
connection.setRequestProperty("token", token)
|
||||
}
|
||||
connection.doOutput = true
|
||||
connection.doInput = true
|
||||
connection.connectTimeout = 10000
|
||||
connection.readTimeout = 10000
|
||||
|
||||
val writer = OutputStreamWriter(connection.outputStream)
|
||||
writer.write(bodyText)
|
||||
writer.flush()
|
||||
writer.close()
|
||||
|
||||
val responseText = if (connection.responseCode in 200..299) {
|
||||
connection.inputStream.bufferedReader().use { it.readText() }
|
||||
} else {
|
||||
connection.errorStream?.bufferedReader()?.use { it.readText() }.orEmpty()
|
||||
}
|
||||
if (responseText.isBlank()) {
|
||||
null
|
||||
} else {
|
||||
JSONObject(responseText)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Request failed: $urlText", e)
|
||||
null
|
||||
} finally {
|
||||
connection?.disconnect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import android.util.Log
|
||||
import android.view.WindowManager
|
||||
import android.view.View
|
||||
import android.util.Base64
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
@@ -56,7 +55,8 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
||||
|
||||
private lateinit var robot: Robot
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
private var mqttManager: MqttManager? = null
|
||||
private lateinit var uiState: UiState
|
||||
private lateinit var connectionCoordinator: ConnectionCoordinator
|
||||
private val mainScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||
private lateinit var prefs: SharedPreferences
|
||||
private val specialStateKey = "special_state"
|
||||
@@ -101,12 +101,14 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
||||
private var receptionAnchorYaw: Float? = null
|
||||
private lateinit var telemetryManager: TelemetryManager
|
||||
private lateinit var taskController: TaskController
|
||||
private val robotEventHandler = RobotEventHandler()
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
uiState = UiState(this, binding)
|
||||
|
||||
LogManager.configureLogcat(
|
||||
tags = listOf(
|
||||
@@ -162,6 +164,38 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
||||
clearTaskLauncher.launch(Intent(this, SettingsActivity::class.java))
|
||||
}
|
||||
|
||||
connectionCoordinator = ConnectionCoordinator(
|
||||
context = this,
|
||||
prefs = prefs,
|
||||
robot = robot,
|
||||
navController = navCon,
|
||||
liveKitManagerProvider = { liveKitManager },
|
||||
mqttStatusListener = { connected -> setMqttConnectionStatus(connected) },
|
||||
liveKitStatusListener = { connected -> setLiveKitStatus(connected) },
|
||||
hasAudioPermission = { hasAudioPermission() },
|
||||
hasCameraPermission = { hasCameraPermission() },
|
||||
requestMediaPermissions = { requestMediaPermissions() },
|
||||
buildLiveKitToken = { room, savedToken ->
|
||||
if (savedToken.isBlank()) {
|
||||
buildLiveKitToken(
|
||||
apiKey = LiveKitManager.DEFAULT_API_KEY,
|
||||
apiSecret = LiveKitManager.DEFAULT_API_SECRET,
|
||||
room = room,
|
||||
identity = buildLiveKitIdentity()
|
||||
)
|
||||
} else {
|
||||
savedToken
|
||||
}
|
||||
},
|
||||
onSetCurrentTask = { task -> setCurrentTask(task) },
|
||||
onMarkSpeechTaskActive = { markSpeechTaskActive() },
|
||||
onClearSpeechTaskIfActive = { clearSpeechTaskIfActive() },
|
||||
onStartNotificationMode = { location, text -> startNotificationMode(location, text) },
|
||||
onStartPatrolMode = { route, times, waiting, nonStop -> startPatrolMode(route, times, waiting, nonStop) },
|
||||
onStartReceptionMode = { location, text, destination -> startReceptionMode(location, text, destination) },
|
||||
onPublishStatusSnapshot = { reason, force -> if (::telemetryManager.isInitialized) telemetryManager.publishStatusSnapshot(reason, force) }
|
||||
)
|
||||
|
||||
taskController = TaskController(
|
||||
scope = mainScope,
|
||||
navController = navCon,
|
||||
@@ -208,13 +242,14 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
||||
locationProvider = { lastArrivalLocation },
|
||||
mqttConnectedProvider = { isMqttConnected },
|
||||
liveKitConnectedProvider = { isLiveKitConnected },
|
||||
publish = { topic, payload -> mqttManager?.publish(topic, payload) },
|
||||
publish = { topic, payload -> connectionCoordinator.publish(topic, payload) },
|
||||
onLowBattery = { _ ->
|
||||
val ttsRequest = TtsRequest.create("电量低,请及时充电。", false, language = TtsRequest.Language.ZH_CN)
|
||||
robot.speak(ttsRequest)
|
||||
}
|
||||
)
|
||||
|
||||
updateActivationBanner()
|
||||
updateMqttConnection()
|
||||
updateLiveKitStatusSnapshot()
|
||||
}
|
||||
@@ -231,15 +266,22 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
||||
robot.addOnCurrentPositionChangedListener(this)
|
||||
robot.addOnRequestPermissionResultListener(this)
|
||||
robot.constraintBeWith()
|
||||
if (mqttManager == null) {
|
||||
updateMqttConnection()
|
||||
updateActivationBanner()
|
||||
if (!isActivated()) {
|
||||
connectionCoordinator.disconnectMqtt()
|
||||
connectionCoordinator.disconnectLiveKit()
|
||||
setMqttConnectionStatus(false)
|
||||
setLiveKitStatus(false)
|
||||
stopBlinking()
|
||||
telemetryManager.stop()
|
||||
Log.w("MainActivity", "Application is not activated yet.")
|
||||
} else {
|
||||
mqttManager?.connect()
|
||||
}
|
||||
updateMqttConnection()
|
||||
updateLiveKitConnection()
|
||||
startBlinking()
|
||||
telemetryManager.start()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
@@ -252,8 +294,8 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
||||
robot.removeOnMovementStatusChangedListener(this)
|
||||
robot.removeOnCurrentPositionChangedListener(this)
|
||||
robot.removeOnRequestPermissionResultListener(this)
|
||||
// mqttManager?.disconnect() // Keep MQTT alive in background/settings
|
||||
liveKitManager?.disconnect()
|
||||
// Keep MQTT alive in background/settings
|
||||
connectionCoordinator.disconnectLiveKit()
|
||||
stopBlinking()
|
||||
telemetryManager.stop()
|
||||
}
|
||||
@@ -261,8 +303,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
prefs.unregisterOnSharedPreferenceChangeListener(this)
|
||||
mqttManager?.disconnect()
|
||||
liveKitManager?.release()
|
||||
connectionCoordinator.release()
|
||||
LogManager.stopLogcatListener()
|
||||
mainScope.cancel()
|
||||
Log.i("MainActivity", "All resources released on destroy.")
|
||||
@@ -290,7 +331,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
||||
}
|
||||
|
||||
override fun onTtsStatusChanged(ttsRequest: TtsRequest) {
|
||||
mqttManager?.handleTtsStatusChange(ttsRequest)
|
||||
connectionCoordinator.handleTtsStatusChange(ttsRequest)
|
||||
when (ttsRequest.status) {
|
||||
TtsRequest.Status.STARTED -> {
|
||||
Log.i("MainActivity", "TTS started")
|
||||
@@ -336,8 +377,8 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
||||
|
||||
override fun onGoToLocationStatusChanged(location: String, status: String, descriptionId: Int, description: String) {
|
||||
val normalized = status.lowercase()
|
||||
val isAbort = normalized in setOf("abort", "aborted", "canceled", "cancelled", "stopped", "stop", "failed", "error")
|
||||
val isMoving = normalized in setOf("start", "starting", "going", "moving", "navigating", "calculating", "recalculating")
|
||||
val isAbort = robotEventHandler.isAbortStatus(status)
|
||||
val isMoving = robotEventHandler.isMovingStatus(status)
|
||||
if (isMoving) {
|
||||
cancelAutoRecharge("movement_started:$location/$status")
|
||||
taskController.cancelTaskWaitTimeout()
|
||||
@@ -360,7 +401,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
||||
lastArrivalLocation = location
|
||||
lastArrivalAt = now
|
||||
prefs.edit().putString("current_location", location).apply()
|
||||
if (normalizeLocation(location) == "homebase") {
|
||||
if (robotEventHandler.normalizeLocation(location) == "homebase") {
|
||||
navCon.tiltAngle(20)
|
||||
}
|
||||
if (taskController.currentTask == "patrol") {
|
||||
@@ -434,12 +475,12 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
||||
}
|
||||
}
|
||||
if (taskController.handleDetectionStateChanged(state)) {
|
||||
Log.i("MainActivity", "what the f**k")
|
||||
Log.i("MainActivity", "Detection event handled by task controller.")
|
||||
return
|
||||
}
|
||||
val isSpecialState = isSpecialStateEnabled()
|
||||
val isIdleTask = taskController.currentTask.isEmpty() || taskController.currentTask == "speech"
|
||||
val atHomeBase = normalizeLocation(lastArrivalLocation) == "homebase"
|
||||
val atHomeBase = robotEventHandler.normalizeLocation(lastArrivalLocation) == "homebase"
|
||||
val canHandleDoor = isIdleTask && atHomeBase && !taskController.isLeavingHomeBase && !isSpecialState
|
||||
if (canHandleDoor) {
|
||||
when (state) {
|
||||
@@ -449,12 +490,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
||||
val ttsRequest = TtsRequest.create("正在为你开门,请稍等。", false, language = TtsRequest.Language.ZH_CN)
|
||||
robot.speak(ttsRequest)
|
||||
mainScope.launch {
|
||||
val result = HttpManager.workflow_execute(
|
||||
context = this@MainActivity,
|
||||
apiKey = "wf_865e80f5fc1a4a319474a21d47470863",
|
||||
workflowId = "2031297462423851009",
|
||||
inputs = emptyMap<String, Any>()
|
||||
)
|
||||
val result = executeDoorWorkflow(openDoor = true)
|
||||
if (result == null) {
|
||||
showNetworkErrorBanner()
|
||||
}
|
||||
@@ -465,12 +501,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
||||
val ttsRequest = TtsRequest.create("正准备关门,请小心被夹。", false, language = TtsRequest.Language.ZH_CN)
|
||||
robot.speak(ttsRequest)
|
||||
mainScope.launch {
|
||||
val result = HttpManager.workflow_execute(
|
||||
context = this@MainActivity,
|
||||
apiKey = "wf_c02aa853371345dbb29572641d083c24",
|
||||
workflowId = "2031634633458520065",
|
||||
inputs = emptyMap<String, Any>()
|
||||
)
|
||||
val result = executeDoorWorkflow(openDoor = false)
|
||||
if (result == null) {
|
||||
showNetworkErrorBanner()
|
||||
}
|
||||
@@ -492,15 +523,6 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
||||
}
|
||||
}
|
||||
|
||||
private fun normalizeLocation(value: String?): String {
|
||||
return value.orEmpty()
|
||||
.trim()
|
||||
.lowercase()
|
||||
.replace(" ", "")
|
||||
.replace("_", "")
|
||||
.replace("-", "")
|
||||
}
|
||||
|
||||
fun startReceptionMode(location: String, text: String, destination: String) {
|
||||
taskController.startReceptionMode(location, text, destination)
|
||||
}
|
||||
@@ -532,8 +554,16 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
||||
if (key == "network_ip") {
|
||||
Log.i("MainActivity", "IP address changed, re-initializing MQTT connection.")
|
||||
if (key == HttpManager.PREF_KEY_BASE_URL ||
|
||||
key == HttpManager.PREF_KEY_MQTT_USERNAME ||
|
||||
key == HttpManager.PREF_KEY_MQTT_PASSWORD
|
||||
) {
|
||||
Log.i("MainActivity", "Base URL or MQTT config changed, re-initializing MQTT connection.")
|
||||
updateMqttConnection()
|
||||
updateLiveKitConnection()
|
||||
}
|
||||
if (key == HttpManager.PREF_KEY_ACTIVATED) {
|
||||
updateActivationBanner()
|
||||
updateMqttConnection()
|
||||
updateLiveKitConnection()
|
||||
}
|
||||
@@ -562,53 +592,11 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
||||
}
|
||||
|
||||
private fun updateMqttConnection() {
|
||||
mqttManager?.shutdown()
|
||||
val ip = prefs.getString("network_ip", null)
|
||||
if (!ip.isNullOrEmpty()) {
|
||||
mqttManager = MqttManager(this, ip, robot, navCon) { connected ->
|
||||
setMqttConnectionStatus(connected)
|
||||
}
|
||||
mqttManager?.connect()
|
||||
Log.i("MainActivity", "MQTT Manager updated with new IP: $ip")
|
||||
} else {
|
||||
mqttManager = null
|
||||
setMqttConnectionStatus(false)
|
||||
Log.w("MainActivity", "MQTT Manager disabled: IP address is not set.")
|
||||
}
|
||||
connectionCoordinator.updateMqttConnection(isActivated())
|
||||
}
|
||||
|
||||
private fun updateLiveKitConnection() {
|
||||
val enabled = prefs.getBoolean(LiveKitManager.PREF_KEY_ENABLED, true)
|
||||
val url = resolveLiveKitUrl()
|
||||
val room = prefs.getString(LiveKitManager.PREF_KEY_ROOM, LiveKitManager.DEFAULT_ROOM).orEmpty()
|
||||
val savedToken = prefs.getString(LiveKitManager.PREF_KEY_TOKEN, "").orEmpty()
|
||||
val token = if (savedToken.isBlank()) {
|
||||
buildLiveKitToken(
|
||||
apiKey = LiveKitManager.DEFAULT_API_KEY,
|
||||
apiSecret = LiveKitManager.DEFAULT_API_SECRET,
|
||||
room = room,
|
||||
identity = buildLiveKitIdentity()
|
||||
)
|
||||
} else {
|
||||
savedToken
|
||||
}
|
||||
if (!enabled) {
|
||||
liveKitManager?.disconnect()
|
||||
setLiveKitStatus(false)
|
||||
return
|
||||
}
|
||||
if (url.isBlank() || room.isBlank()) {
|
||||
liveKitManager?.disconnect()
|
||||
setLiveKitStatus(false)
|
||||
return
|
||||
}
|
||||
if (!hasAudioPermission() || !hasCameraPermission()) {
|
||||
setLiveKitStatus(false)
|
||||
requestMediaPermissions()
|
||||
return
|
||||
}
|
||||
setLiveKitStatus(false)
|
||||
liveKitManager?.connect(url, token, true, true)
|
||||
connectionCoordinator.updateLiveKitConnection(isActivated())
|
||||
}
|
||||
|
||||
private fun hasAudioPermission(): Boolean {
|
||||
@@ -691,32 +679,17 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
||||
}
|
||||
|
||||
private fun updateLiveKitStatusSnapshot() {
|
||||
val enabled = prefs.getBoolean(LiveKitManager.PREF_KEY_ENABLED, true)
|
||||
val url = resolveLiveKitUrl()
|
||||
val room = prefs.getString(LiveKitManager.PREF_KEY_ROOM, LiveKitManager.DEFAULT_ROOM).orEmpty()
|
||||
if (!enabled) {
|
||||
setLiveKitStatus(false)
|
||||
return
|
||||
}
|
||||
if (url.isBlank() || room.isBlank()) {
|
||||
setLiveKitStatus(false)
|
||||
return
|
||||
}
|
||||
if (!hasAudioPermission() || !hasCameraPermission()) {
|
||||
setLiveKitStatus(false)
|
||||
return
|
||||
}
|
||||
setLiveKitStatus(false)
|
||||
connectionCoordinator.updateLiveKitStatusSnapshot(isActivated())
|
||||
}
|
||||
|
||||
private fun setLiveKitStatus(connected: Boolean) {
|
||||
isLiveKitConnected = connected
|
||||
updateConnectionIndicator()
|
||||
uiState.updateConnectionIndicator(isLiveKitConnected, isMqttConnected)
|
||||
}
|
||||
|
||||
private fun setMqttConnectionStatus(connected: Boolean) {
|
||||
isMqttConnected = connected
|
||||
updateConnectionIndicator()
|
||||
uiState.updateConnectionIndicator(isLiveKitConnected, isMqttConnected)
|
||||
if (connected) {
|
||||
telemetryManager.publishStatusSnapshot("mqtt_connected", true)
|
||||
}
|
||||
@@ -733,7 +706,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
||||
.put("topic", topicLabel)
|
||||
.put("participant", participantLabel)
|
||||
.put("ts", System.currentTimeMillis())
|
||||
mqttManager?.publish("robot/asr", data.toString())
|
||||
connectionCoordinator.publish("robot/asr", data.toString())
|
||||
}
|
||||
|
||||
private fun extractAsrText(payload: String): String? {
|
||||
@@ -756,10 +729,10 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
||||
|
||||
private fun showNetworkErrorBanner() {
|
||||
networkErrorJob?.cancel()
|
||||
binding.tvNetworkError.visibility = View.VISIBLE
|
||||
uiState.setNetworkErrorVisible(true)
|
||||
networkErrorJob = mainScope.launch {
|
||||
delay(5000L)
|
||||
binding.tvNetworkError.visibility = View.GONE
|
||||
uiState.setNetworkErrorVisible(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -767,7 +740,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
||||
if (taskController.currentTask.isNotEmpty()) {
|
||||
return
|
||||
}
|
||||
if (normalizeLocation(lastArrivalLocation) == "homebase") {
|
||||
if (robotEventHandler.normalizeLocation(lastArrivalLocation) == "homebase") {
|
||||
return
|
||||
}
|
||||
autoRechargeJob?.cancel()
|
||||
@@ -776,7 +749,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
||||
if (taskController.currentTask.isNotEmpty()) {
|
||||
return@launch
|
||||
}
|
||||
if (normalizeLocation(lastArrivalLocation) == "homebase") {
|
||||
if (robotEventHandler.normalizeLocation(lastArrivalLocation) == "homebase") {
|
||||
return@launch
|
||||
}
|
||||
Log.i("MainActivity", "Auto recharge triggered after idle arrival timeout.")
|
||||
@@ -811,7 +784,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
||||
}
|
||||
val anchorYaw = receptionAnchorYaw ?: return
|
||||
val currentYaw = latestYaw ?: return
|
||||
val delta = normalizeAngle(anchorYaw - currentYaw)
|
||||
val delta = robotEventHandler.normalizeAngle(anchorYaw - currentYaw)
|
||||
// Ignore tiny drift to avoid jitter.
|
||||
if (kotlin.math.abs(delta) < 8f) {
|
||||
return
|
||||
@@ -824,33 +797,33 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
||||
Log.i("MainActivity", "Reception facing recovered by $turn degrees (delta=$delta).")
|
||||
}
|
||||
|
||||
private fun normalizeAngle(angle: Float): Float {
|
||||
var normalized = angle % 360f
|
||||
if (normalized > 180f) {
|
||||
normalized -= 360f
|
||||
} else if (normalized < -180f) {
|
||||
normalized += 360f
|
||||
private suspend fun executeDoorWorkflow(openDoor: Boolean): String? {
|
||||
val workflowIdKey = if (openDoor) HttpManager.PREF_KEY_OD_WFID else HttpManager.PREF_KEY_CD_WFID
|
||||
val workflowApiKey = if (openDoor) HttpManager.PREF_KEY_OD_WF_KEY else HttpManager.PREF_KEY_CD_WF_KEY
|
||||
val workflowId = prefs.getString(workflowIdKey, "").orEmpty().trim()
|
||||
val apiKey = prefs.getString(workflowApiKey, "").orEmpty().trim()
|
||||
if (workflowId.isEmpty() || apiKey.isEmpty()) {
|
||||
Log.w("MainActivity", "Door workflow config missing: openDoor=$openDoor")
|
||||
return null
|
||||
}
|
||||
return normalized
|
||||
return HttpManager.workflow_execute(
|
||||
context = this@MainActivity,
|
||||
apiKey = apiKey,
|
||||
workflowId = workflowId,
|
||||
inputs = emptyMap<String, Any>()
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateConnectionIndicator() {
|
||||
val colorRes = when {
|
||||
!isLiveKitConnected && !isMqttConnected -> android.R.color.holo_red_dark
|
||||
!isLiveKitConnected && isMqttConnected -> android.R.color.holo_blue_light
|
||||
isLiveKitConnected && !isMqttConnected -> android.R.color.holo_orange_light
|
||||
else -> android.R.color.holo_green_light
|
||||
private fun isActivated(): Boolean {
|
||||
if (!::prefs.isInitialized) {
|
||||
return false
|
||||
}
|
||||
val indicatorDrawable = binding.statusIndicator.background as GradientDrawable
|
||||
indicatorDrawable.setColor(ContextCompat.getColor(this, colorRes))
|
||||
return prefs.getBoolean(HttpManager.PREF_KEY_ACTIVATED, false)
|
||||
}
|
||||
|
||||
private fun resolveLiveKitUrl(): String {
|
||||
val savedUrl = prefs.getString(LiveKitManager.PREF_KEY_URL, "").orEmpty().trim()
|
||||
if (savedUrl.isNotEmpty()) {
|
||||
return savedUrl
|
||||
}
|
||||
return LiveKitManager.DEFAULT_URL
|
||||
private fun updateActivationBanner() {
|
||||
val activated = isActivated()
|
||||
uiState.setActivationRequired(!activated)
|
||||
}
|
||||
|
||||
private fun startBlinking() {
|
||||
|
||||
@@ -14,13 +14,21 @@ import java.util.LinkedList
|
||||
import java.util.Queue
|
||||
import java.nio.charset.StandardCharsets
|
||||
class MqttManager(
|
||||
private val context: Context,
|
||||
context: Context,
|
||||
private val serverIp: String,
|
||||
private val robot: Robot,
|
||||
private val navController: NavController,
|
||||
private val statusListener: (Boolean) -> Unit
|
||||
private val statusListener: (Boolean) -> Unit,
|
||||
private val onSetCurrentTask: (String) -> Unit,
|
||||
private val onMarkSpeechTaskActive: () -> Unit,
|
||||
private val onClearSpeechTaskIfActive: () -> Unit,
|
||||
private val onStartNotificationMode: (location: String, text: String) -> Unit,
|
||||
private val onStartPatrolMode: (route: List<String>, times: Int, waiting: Int, nonStop: Boolean) -> Unit,
|
||||
private val onStartReceptionMode: (location: String, text: String, destination: String) -> Unit,
|
||||
private val onPublishStatusSnapshot: (reason: String, force: Boolean) -> Unit
|
||||
) {
|
||||
|
||||
private val appContext = context.applicationContext
|
||||
private var mqttClient: MqttClient? = null
|
||||
private val TAG = "MqttManager"
|
||||
private val brokerUri = "tcp://$serverIp:1883"
|
||||
@@ -28,7 +36,7 @@ class MqttManager(
|
||||
private val job = SupervisorJob()
|
||||
private val scope = CoroutineScope(Dispatchers.IO + job)
|
||||
private var reconnectJob: Job? = null
|
||||
private val prefs = context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
|
||||
private val prefs = appContext.getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
|
||||
private val agentDempIdKey = "agent_demp_id"
|
||||
|
||||
// Streaming text buffer
|
||||
@@ -88,6 +96,13 @@ class MqttManager(
|
||||
updateConnectionStatus(true)
|
||||
return@launch
|
||||
}
|
||||
val username = prefs.getString(HttpManager.PREF_KEY_MQTT_USERNAME, "").orEmpty().trim()
|
||||
val password = prefs.getString(HttpManager.PREF_KEY_MQTT_PASSWORD, "").orEmpty()
|
||||
if (username.isEmpty() || password.isEmpty()) {
|
||||
Log.w(TAG, "MQTT connect skipped: username/password not configured.")
|
||||
updateConnectionStatus(false)
|
||||
return@launch
|
||||
}
|
||||
Log.i(TAG, "Attempting to connect to MQTT broker at $brokerUri")
|
||||
try {
|
||||
val options = MqttConnectOptions().apply {
|
||||
@@ -95,8 +110,8 @@ class MqttManager(
|
||||
isCleanSession = true
|
||||
connectionTimeout = 10
|
||||
keepAliveInterval = 60
|
||||
userName = "lzwc"
|
||||
password = "Lzwc@4187.".toCharArray()
|
||||
userName = username
|
||||
this.password = password.toCharArray()
|
||||
}
|
||||
mqttClient?.connect(options)
|
||||
} catch (e: MqttException) {
|
||||
@@ -224,7 +239,7 @@ class MqttManager(
|
||||
val isFinal = obj.optBoolean("is_final", false)
|
||||
val lang = obj.optString("lang", "").trim()
|
||||
scope.launch(Dispatchers.Main) {
|
||||
(context as? MainActivity)?.markSpeechTaskActive()
|
||||
onMarkSpeechTaskActive()
|
||||
}
|
||||
processStreamText(text, lang)
|
||||
if (isFinal) {
|
||||
@@ -268,11 +283,13 @@ class MqttManager(
|
||||
}
|
||||
|
||||
private fun handleJsonCommand(obj: JSONObject) {
|
||||
// 收到任何 MQTT 指令时清空当前任务,确保开门等基础行为不受影响
|
||||
scope.launch(Dispatchers.Main) {
|
||||
(context as? MainActivity)?.setCurrentTask("")
|
||||
}
|
||||
val action = obj.optString("action", obj.optString("cmd", obj.optString("type", ""))).lowercase()
|
||||
val actionsResetTask = setOf("recharge", "goto", "notification", "reception", "patrol", "repose", "turn", "tilt", "terminate")
|
||||
if (action in actionsResetTask) {
|
||||
scope.launch(Dispatchers.Main) {
|
||||
onSetCurrentTask("")
|
||||
}
|
||||
}
|
||||
when (action) {
|
||||
"recharge" -> {
|
||||
speak("前往充电桩", "zh")
|
||||
@@ -291,7 +308,7 @@ class MqttManager(
|
||||
val text = obj.optString("text", obj.optString("content", ""))
|
||||
val lang = obj.optString("lang", "")
|
||||
scope.launch(Dispatchers.Main) {
|
||||
(context as? MainActivity)?.markSpeechTaskActive()
|
||||
onMarkSpeechTaskActive()
|
||||
}
|
||||
processStreamText(text, lang)
|
||||
}
|
||||
@@ -299,7 +316,7 @@ class MqttManager(
|
||||
val location = obj.optString("location", obj.optString("target", ""))
|
||||
val text = obj.optString("text", obj.optString("content", ""))
|
||||
scope.launch(Dispatchers.Main) {
|
||||
(context as? MainActivity)?.startNotificationMode(location, text)
|
||||
onStartNotificationMode(location, text)
|
||||
}
|
||||
}
|
||||
"repose" -> {
|
||||
@@ -331,7 +348,7 @@ class MqttManager(
|
||||
}
|
||||
"terminate" -> {
|
||||
scope.launch(Dispatchers.Main) {
|
||||
(context as? MainActivity)?.setCurrentTask("")
|
||||
onSetCurrentTask("")
|
||||
}
|
||||
navController.stop()
|
||||
stopTts()
|
||||
@@ -341,7 +358,7 @@ class MqttManager(
|
||||
}
|
||||
"status" -> {
|
||||
scope.launch(Dispatchers.Main) {
|
||||
(context as? MainActivity)?.publishStatusSnapshot("command", true)
|
||||
onPublishStatusSnapshot("command", true)
|
||||
}
|
||||
}
|
||||
"patrol" -> {
|
||||
@@ -369,11 +386,10 @@ class MqttManager(
|
||||
}
|
||||
}
|
||||
scope.launch(Dispatchers.Main) {
|
||||
val activity = context as? MainActivity
|
||||
if (patrolLocations.isNotEmpty()) {
|
||||
activity?.startPatrolMode(patrolLocations, times, waiting, nonStop)
|
||||
onStartPatrolMode(patrolLocations, times, waiting, nonStop)
|
||||
} else {
|
||||
activity?.setCurrentTask("")
|
||||
onSetCurrentTask("")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -383,7 +399,7 @@ class MqttManager(
|
||||
val text = obj.optString("text", "你是我要接待的贵宾吗?")
|
||||
val destination = obj.optString("destination", "会议室")
|
||||
scope.launch(Dispatchers.Main) {
|
||||
(context as? MainActivity)?.startReceptionMode(location, text, destination)
|
||||
onStartReceptionMode(location, text, destination)
|
||||
}
|
||||
}
|
||||
else -> Log.w(TAG, "Unknown command action: $action")
|
||||
@@ -582,7 +598,7 @@ class MqttManager(
|
||||
processNextTts()
|
||||
}
|
||||
if (!isTtsPaused && !isTtsBusy && ttsQueue.isEmpty() && speechBuffer.isEmpty()) {
|
||||
(context as? MainActivity)?.clearSpeechTaskIfActive()
|
||||
onClearSpeechTaskIfActive()
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
|
||||
@@ -5,11 +5,9 @@ import com.robotemi.sdk.Robot
|
||||
|
||||
class NavController(private val robot: Robot) {
|
||||
private val TAG = "NavController"
|
||||
private var playmode = false
|
||||
|
||||
fun recharge(): Boolean {
|
||||
playmode = !playmode
|
||||
robot.goTo("home base", playmode)
|
||||
fun recharge(backwards: Boolean = True): Boolean {
|
||||
robot.goTo("home base", backwards)
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.example.lzwcai_terminal_temi
|
||||
|
||||
class RobotEventHandler {
|
||||
private val abortStatuses = setOf("abort", "aborted", "canceled", "cancelled", "stopped", "stop", "failed", "error")
|
||||
private val movingStatuses = setOf("start", "starting", "going", "moving", "navigating", "calculating", "recalculating")
|
||||
|
||||
fun isAbortStatus(status: String): Boolean {
|
||||
return status.lowercase() in abortStatuses
|
||||
}
|
||||
|
||||
fun isMovingStatus(status: String): Boolean {
|
||||
return status.lowercase() in movingStatuses
|
||||
}
|
||||
|
||||
fun normalizeLocation(value: String?): String {
|
||||
return value.orEmpty()
|
||||
.trim()
|
||||
.lowercase()
|
||||
.replace(" ", "")
|
||||
.replace("_", "")
|
||||
.replace("-", "")
|
||||
}
|
||||
|
||||
fun normalizeAngle(angle: Float): Float {
|
||||
var normalized = angle % 360f
|
||||
if (normalized > 180f) {
|
||||
normalized -= 360f
|
||||
} else if (normalized < -180f) {
|
||||
normalized += 360f
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
}
|
||||
@@ -14,12 +14,19 @@ import android.view.WindowManager
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.example.lzwcai_terminal_temi.databinding.ActivitySettingsBinding
|
||||
import kotlin.system.exitProcess
|
||||
import com.robotemi.sdk.Robot
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import androidx.core.content.ContextCompat
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import java.net.URL
|
||||
|
||||
class SettingsActivity : AppCompatActivity() {
|
||||
|
||||
@@ -31,6 +38,7 @@ class SettingsActivity : AppCompatActivity() {
|
||||
private val specialStateKey = "special_state"
|
||||
private lateinit var prefs: SharedPreferences
|
||||
private val agentDempIdKey = "agent_demp_id"
|
||||
private val uiScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -41,10 +49,16 @@ class SettingsActivity : AppCompatActivity() {
|
||||
robot = Robot.getInstance()
|
||||
|
||||
prefs = getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
|
||||
val savedIp = prefs.getString("network_ip", "")
|
||||
binding.etIpAddress.setText(savedIp)
|
||||
val savedBaseUrl = HttpManager.getBaseUrl(this)
|
||||
binding.etIpAddress.setText(savedBaseUrl)
|
||||
val savedDempId = prefs.getString(agentDempIdKey, "")
|
||||
binding.etAgentDempId.setText(savedDempId)
|
||||
val savedLoginUsername = prefs.getString(HttpManager.PREF_KEY_LOGIN_USERNAME, "")
|
||||
val savedLoginPassword = prefs.getString(HttpManager.PREF_KEY_LOGIN_PASSWORD, "")
|
||||
binding.etLoginUsername.setText(savedLoginUsername)
|
||||
binding.etLoginPassword.setText(savedLoginPassword)
|
||||
binding.etActivationCode.setText(prefs.getString(HttpManager.PREF_KEY_ACTIVATION_CODE, ""))
|
||||
binding.etDeviceName.setText(prefs.getString(HttpManager.PREF_KEY_DEVICE_NAME, ""))
|
||||
val savedLiveKitUrl = prefs.getString(LiveKitManager.PREF_KEY_URL, resolveLiveKitUrl())
|
||||
val savedLiveKitRoom = prefs.getString(LiveKitManager.PREF_KEY_ROOM, LiveKitManager.DEFAULT_ROOM)
|
||||
val savedLiveKitToken = prefs.getString(LiveKitManager.PREF_KEY_TOKEN, "")
|
||||
@@ -55,26 +69,112 @@ class SettingsActivity : AppCompatActivity() {
|
||||
binding.switchLiveKitAuto.setOnCheckedChangeListener(null)
|
||||
binding.switchLiveKitAuto.isChecked = isLiveKitEnabled
|
||||
|
||||
// Set Version Name
|
||||
val versionName = "2603131822"
|
||||
val versionName = getAppVersionName()
|
||||
binding.tvVersion.text = getString(R.string.version_prefix, versionName)
|
||||
updateActivationUi(isActivated())
|
||||
|
||||
binding.root.setOnClickListener { hideKeyboard() }
|
||||
|
||||
binding.btnSave.setOnClickListener {
|
||||
hideKeyboard()
|
||||
val ip = binding.etIpAddress.text.toString().trim()
|
||||
val baseUrl = normalizeBaseUrl(binding.etIpAddress.text.toString())
|
||||
val dempId = binding.etAgentDempId.text?.toString()?.trim().orEmpty()
|
||||
if (ip.isNotEmpty()) {
|
||||
prefs.edit()
|
||||
.putString("network_ip", ip)
|
||||
val loginUsername = binding.etLoginUsername.text?.toString()?.trim().orEmpty()
|
||||
val loginPassword = binding.etLoginPassword.text?.toString().orEmpty()
|
||||
if (baseUrl.isNotEmpty()) {
|
||||
val oldBaseUrl = HttpManager.getBaseUrl(this)
|
||||
val changed = oldBaseUrl != baseUrl
|
||||
val editor = prefs.edit()
|
||||
.putString(HttpManager.PREF_KEY_BASE_URL, baseUrl)
|
||||
.putString(agentDempIdKey, dempId)
|
||||
.apply()
|
||||
Toast.makeText(this, getString(R.string.msg_ip_saved, ip), Toast.LENGTH_SHORT).show()
|
||||
finish()
|
||||
.putString(HttpManager.PREF_KEY_LOGIN_USERNAME, loginUsername)
|
||||
.putString(HttpManager.PREF_KEY_LOGIN_PASSWORD, loginPassword)
|
||||
val host = parseHostFromBaseUrl(baseUrl)
|
||||
if (!host.isNullOrEmpty()) {
|
||||
editor.putString("network_ip", host)
|
||||
}
|
||||
if (changed) {
|
||||
editor.remove(HttpManager.PREF_KEY_ACTIVATION_CODE)
|
||||
.remove(HttpManager.PREF_KEY_DEVICE_NAME)
|
||||
.putBoolean(HttpManager.PREF_KEY_ACTIVATED, false)
|
||||
.remove(HttpManager.PREF_KEY_MQTT_USERNAME)
|
||||
.remove(HttpManager.PREF_KEY_MQTT_PASSWORD)
|
||||
.remove(HttpManager.PREF_KEY_OD_WFID)
|
||||
.remove(HttpManager.PREF_KEY_OD_WF_KEY)
|
||||
.remove(HttpManager.PREF_KEY_CD_WFID)
|
||||
.remove(HttpManager.PREF_KEY_CD_WF_KEY)
|
||||
binding.etActivationCode.setText("")
|
||||
binding.etDeviceName.setText("")
|
||||
updateActivationUi(false)
|
||||
Toast.makeText(this, getString(R.string.msg_base_url_changed_reset), Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
Toast.makeText(this, getString(R.string.msg_base_url_saved, baseUrl), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
editor.apply()
|
||||
} else {
|
||||
Toast.makeText(this, getString(R.string.msg_invalid_ip), Toast.LENGTH_SHORT).show()
|
||||
Log.w("SettingsActivity", "Invalid IP attempt")
|
||||
Log.w("SettingsActivity", "Invalid base_url attempt")
|
||||
}
|
||||
}
|
||||
|
||||
binding.btnActivate.setOnClickListener {
|
||||
hideKeyboard()
|
||||
val activationCode = binding.etActivationCode.text?.toString()?.trim().orEmpty()
|
||||
val deviceName = binding.etDeviceName.text?.toString()?.trim().orEmpty()
|
||||
val baseUrl = normalizeBaseUrl(binding.etIpAddress.text?.toString().orEmpty())
|
||||
val loginUsername = binding.etLoginUsername.text?.toString()?.trim().orEmpty()
|
||||
val loginPassword = binding.etLoginPassword.text?.toString().orEmpty()
|
||||
if (activationCode.isBlank() || deviceName.isBlank() || baseUrl.isBlank() || loginUsername.isBlank() || loginPassword.isBlank()) {
|
||||
Toast.makeText(this, getString(R.string.msg_input_required), Toast.LENGTH_SHORT).show()
|
||||
return@setOnClickListener
|
||||
}
|
||||
prefs.edit()
|
||||
.putString(HttpManager.PREF_KEY_BASE_URL, baseUrl)
|
||||
.putString(HttpManager.PREF_KEY_LOGIN_USERNAME, loginUsername)
|
||||
.putString(HttpManager.PREF_KEY_LOGIN_PASSWORD, loginPassword)
|
||||
.apply()
|
||||
val deviceId = getOrCreateDeviceId()
|
||||
binding.btnActivate.isEnabled = false
|
||||
uiScope.launch {
|
||||
val activateSuccess = HttpManager.activateDevice(
|
||||
context = this@SettingsActivity,
|
||||
activationCode = activationCode,
|
||||
deviceName = deviceName,
|
||||
deviceId = deviceId
|
||||
)
|
||||
if (!activateSuccess) {
|
||||
binding.btnActivate.isEnabled = true
|
||||
Toast.makeText(this@SettingsActivity, getString(R.string.msg_activate_failed), Toast.LENGTH_SHORT).show()
|
||||
return@launch
|
||||
}
|
||||
val runtimeConfigs = HttpManager.fetchRuntimeConfigs(this@SettingsActivity)
|
||||
val mqttUser = runtimeConfigs?.get(HttpManager.PREF_KEY_MQTT_USERNAME).orEmpty()
|
||||
val mqttPass = runtimeConfigs?.get(HttpManager.PREF_KEY_MQTT_PASSWORD).orEmpty()
|
||||
val odWfid = runtimeConfigs?.get(HttpManager.PREF_KEY_OD_WFID).orEmpty()
|
||||
val odWfKey = runtimeConfigs?.get(HttpManager.PREF_KEY_OD_WF_KEY).orEmpty()
|
||||
val cdWfid = runtimeConfigs?.get(HttpManager.PREF_KEY_CD_WFID).orEmpty()
|
||||
val cdWfKey = runtimeConfigs?.get(HttpManager.PREF_KEY_CD_WF_KEY).orEmpty()
|
||||
val mqttReady = mqttUser.isNotBlank() && mqttPass.isNotBlank()
|
||||
val workflowReady = odWfid.isNotBlank() && odWfKey.isNotBlank() && cdWfid.isNotBlank() && cdWfKey.isNotBlank()
|
||||
val configReady = mqttReady && workflowReady
|
||||
val editor = prefs.edit()
|
||||
.putString(HttpManager.PREF_KEY_ACTIVATION_CODE, activationCode)
|
||||
.putString(HttpManager.PREF_KEY_DEVICE_NAME, deviceName)
|
||||
.putString(HttpManager.PREF_KEY_DEVICE_ID, deviceId)
|
||||
.putBoolean(HttpManager.PREF_KEY_ACTIVATED, true)
|
||||
.putString(HttpManager.PREF_KEY_MQTT_USERNAME, mqttUser)
|
||||
.putString(HttpManager.PREF_KEY_MQTT_PASSWORD, mqttPass)
|
||||
.putString(HttpManager.PREF_KEY_OD_WFID, odWfid)
|
||||
.putString(HttpManager.PREF_KEY_OD_WF_KEY, odWfKey)
|
||||
.putString(HttpManager.PREF_KEY_CD_WFID, cdWfid)
|
||||
.putString(HttpManager.PREF_KEY_CD_WF_KEY, cdWfKey)
|
||||
editor.apply()
|
||||
updateActivationUi(true)
|
||||
if (!configReady) {
|
||||
Toast.makeText(this@SettingsActivity, getString(R.string.msg_fetch_mqtt_failed), Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
Toast.makeText(this@SettingsActivity, getString(R.string.msg_activate_success), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,6 +207,10 @@ class SettingsActivity : AppCompatActivity() {
|
||||
finish()
|
||||
}
|
||||
|
||||
binding.btnAbout.setOnClickListener {
|
||||
showAboutDialog()
|
||||
}
|
||||
|
||||
setupRestartButton()
|
||||
setupSpecialStateSwitch()
|
||||
setupLocationSelector()
|
||||
@@ -116,6 +220,17 @@ class SettingsActivity : AppCompatActivity() {
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
refreshLocationList()
|
||||
updateActivationUi(isActivated())
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
persistDraftInputs()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
uiScope.cancel()
|
||||
}
|
||||
|
||||
private fun setupSpecialStateSwitch() {
|
||||
@@ -259,4 +374,84 @@ class SettingsActivity : AppCompatActivity() {
|
||||
}
|
||||
return LiveKitManager.DEFAULT_URL
|
||||
}
|
||||
|
||||
private fun normalizeBaseUrl(input: String): String {
|
||||
return input.trim().trimEnd('/')
|
||||
}
|
||||
|
||||
private fun parseHostFromBaseUrl(baseUrl: String): String? {
|
||||
return runCatching { URL(baseUrl).host }
|
||||
.getOrNull()
|
||||
?.trim()
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
}
|
||||
|
||||
private fun isActivated(): Boolean {
|
||||
return prefs.getBoolean(HttpManager.PREF_KEY_ACTIVATED, false)
|
||||
}
|
||||
|
||||
private fun updateActivationUi(activated: Boolean) {
|
||||
val status = if (activated) getString(R.string.status_activated) else getString(R.string.status_not_activated)
|
||||
binding.tvActivationStatus.text = getString(R.string.label_activation_status, status)
|
||||
binding.activationContainer.visibility = if (activated) View.GONE else View.VISIBLE
|
||||
binding.btnActivate.isEnabled = !activated
|
||||
}
|
||||
|
||||
private fun getOrCreateDeviceId(): String {
|
||||
val saved = prefs.getString(HttpManager.PREF_KEY_DEVICE_ID, "").orEmpty().trim()
|
||||
if (saved.isNotEmpty()) {
|
||||
return saved
|
||||
}
|
||||
val serial = runCatching { robot.serialNumber }.getOrDefault("").trim()
|
||||
val deviceId = if (serial.isNotEmpty()) serial else "unknown-device"
|
||||
prefs.edit().putString(HttpManager.PREF_KEY_DEVICE_ID, deviceId).apply()
|
||||
return deviceId
|
||||
}
|
||||
|
||||
private fun persistDraftInputs() {
|
||||
prefs.edit()
|
||||
.putString(HttpManager.PREF_KEY_LOGIN_USERNAME, binding.etLoginUsername.text?.toString()?.trim().orEmpty())
|
||||
.putString(HttpManager.PREF_KEY_LOGIN_PASSWORD, binding.etLoginPassword.text?.toString().orEmpty())
|
||||
.putString(agentDempIdKey, binding.etAgentDempId.text?.toString()?.trim().orEmpty())
|
||||
.apply()
|
||||
}
|
||||
|
||||
private fun maskActivationCode(code: String): String {
|
||||
val value = code.trim()
|
||||
if (value.length <= 2) {
|
||||
return value
|
||||
}
|
||||
val stars = "*".repeat((value.length - 2).coerceAtLeast(1))
|
||||
return "${value.first()}$stars${value.last()}"
|
||||
}
|
||||
|
||||
private fun showAboutDialog() {
|
||||
val baseUrl = HttpManager.getBaseUrl(this).ifEmpty { "-" }
|
||||
val deviceId = prefs.getString(HttpManager.PREF_KEY_DEVICE_ID, "").orEmpty().ifEmpty { "-" }
|
||||
val activationCode = prefs.getString(HttpManager.PREF_KEY_ACTIVATION_CODE, "").orEmpty()
|
||||
val maskedCode = if (activationCode.isBlank()) "-" else maskActivationCode(activationCode)
|
||||
val deviceName = prefs.getString(HttpManager.PREF_KEY_DEVICE_NAME, "").orEmpty().ifEmpty { "-" }
|
||||
val activated = if (isActivated()) getString(R.string.status_activated) else getString(R.string.status_not_activated)
|
||||
val version = getAppVersionName()
|
||||
val message = listOf(
|
||||
getString(R.string.about_base_url, baseUrl),
|
||||
getString(R.string.about_device_id, deviceId),
|
||||
getString(R.string.about_activation_code, maskedCode),
|
||||
getString(R.string.about_device_name, deviceName),
|
||||
getString(R.string.about_activated, activated),
|
||||
getString(R.string.about_version, version)
|
||||
).joinToString("\n")
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(getString(R.string.title_about))
|
||||
.setMessage(message)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun getAppVersionName(): String {
|
||||
return runCatching {
|
||||
val packageInfo = packageManager.getPackageInfo(packageName, 0)
|
||||
packageInfo.versionName ?: "--"
|
||||
}.getOrDefault("--")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.example.lzwcai_terminal_temi
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.view.View
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.example.lzwcai_terminal_temi.databinding.ActivityMainBinding
|
||||
|
||||
class UiState(
|
||||
private val context: Context,
|
||||
private val binding: ActivityMainBinding
|
||||
) {
|
||||
fun setActivationRequired(required: Boolean) {
|
||||
binding.tvActivationRequired.visibility = if (required) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
fun setNetworkErrorVisible(visible: Boolean) {
|
||||
binding.tvNetworkError.visibility = if (visible) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
fun updateConnectionIndicator(liveKitConnected: Boolean, mqttConnected: Boolean) {
|
||||
val colorRes = when {
|
||||
!liveKitConnected && !mqttConnected -> android.R.color.holo_red_dark
|
||||
!liveKitConnected && mqttConnected -> android.R.color.holo_blue_light
|
||||
liveKitConnected && !mqttConnected -> android.R.color.holo_orange_light
|
||||
else -> android.R.color.holo_green_light
|
||||
}
|
||||
val indicatorDrawable = binding.statusIndicator.background as GradientDrawable
|
||||
indicatorDrawable.setColor(ContextCompat.getColor(context, colorRes))
|
||||
}
|
||||
}
|
||||
@@ -51,6 +51,27 @@
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/statusIndicator" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvActivationRequired"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:background="@android:color/holo_orange_dark"
|
||||
android:gravity="center"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:text="@string/msg_activation_required"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="18sp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tvNetworkError" />
|
||||
|
||||
<com.example.lzwcai_terminal_temi.AnimatedEmojiView
|
||||
android:id="@+id/animatedEmojiView"
|
||||
android:layout_width="0dp"
|
||||
|
||||
@@ -96,8 +96,7 @@
|
||||
android:id="@+id/etIpAddress"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="80dp"
|
||||
android:inputType="number|numberDecimal"
|
||||
android:digits="0123456789."
|
||||
android:inputType="textUri"
|
||||
android:textSize="20sp"
|
||||
android:textColor="@color/text_primary" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
@@ -118,6 +117,38 @@
|
||||
android:textColor="@color/text_primary" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.App.TextInputLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:hint="@string/hint_login_username">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etLoginUsername"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="72dp"
|
||||
android:inputType="text"
|
||||
android:textSize="18sp"
|
||||
android:textColor="@color/text_primary" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.App.TextInputLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:hint="@string/hint_login_password">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etLoginPassword"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="72dp"
|
||||
android:inputType="textPassword"
|
||||
android:textSize="18sp"
|
||||
android:textColor="@color/text_primary" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnSave"
|
||||
style="@style/Widget.App.Button"
|
||||
@@ -127,6 +158,72 @@
|
||||
android:textSize="20sp"
|
||||
android:text="@string/btn_save" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvActivationStatus"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="16sp"
|
||||
tools:text="激活状态:未激活" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/activationContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:text="@string/label_activation_config"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="18sp"
|
||||
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_activation_code">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etActivationCode"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="72dp"
|
||||
android:inputType="text"
|
||||
android:textSize="18sp"
|
||||
android:textColor="@color/text_primary" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.App.TextInputLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:hint="@string/hint_device_name">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etDeviceName"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="72dp"
|
||||
android:inputType="text"
|
||||
android:textSize="18sp"
|
||||
android:textColor="@color/text_primary" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnActivate"
|
||||
style="@style/Widget.App.Button"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="72dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:text="@string/btn_activate"
|
||||
android:textSize="20sp" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
@@ -310,7 +407,7 @@
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="特殊状态"
|
||||
android:text="@string/label_special_state"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="22sp"
|
||||
android:textStyle="bold" />
|
||||
@@ -319,7 +416,7 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="启用特定场景下的行为逻辑"
|
||||
android:text="@string/desc_special_state"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="16sp" />
|
||||
</LinearLayout>
|
||||
@@ -387,6 +484,15 @@
|
||||
android:textSize="20sp"
|
||||
android:text="@string/btn_clear_task" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnAbout"
|
||||
style="@style/Widget.App.OutlinedButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="72dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/btn_about"
|
||||
android:textSize="20sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvVersion"
|
||||
android:layout_width="wrap_content"
|
||||
|
||||
@@ -51,6 +51,27 @@
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/statusIndicator" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvActivationRequired"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:background="@android:color/holo_orange_dark"
|
||||
android:gravity="center"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:text="@string/msg_activation_required"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="18sp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tvNetworkError" />
|
||||
|
||||
<com.example.lzwcai_terminal_temi.AnimatedEmojiView
|
||||
android:id="@+id/animatedEmojiView"
|
||||
android:layout_width="0dp"
|
||||
|
||||
@@ -87,8 +87,7 @@
|
||||
android:id="@+id/etIpAddress"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="80dp"
|
||||
android:inputType="number|numberDecimal"
|
||||
android:digits="0123456789."
|
||||
android:inputType="textUri"
|
||||
android:textSize="20sp"
|
||||
android:textColor="@color/text_primary" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
@@ -109,6 +108,38 @@
|
||||
android:textColor="@color/text_primary" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.App.TextInputLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:hint="@string/hint_login_username">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etLoginUsername"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="72dp"
|
||||
android:inputType="text"
|
||||
android:textSize="18sp"
|
||||
android:textColor="@color/text_primary" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.App.TextInputLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:hint="@string/hint_login_password">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etLoginPassword"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="72dp"
|
||||
android:inputType="textPassword"
|
||||
android:textSize="18sp"
|
||||
android:textColor="@color/text_primary" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnSave"
|
||||
style="@style/Widget.App.Button"
|
||||
@@ -118,6 +149,72 @@
|
||||
android:textSize="20sp"
|
||||
android:text="@string/btn_save" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvActivationStatus"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="16sp"
|
||||
tools:text="激活状态:未激活" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/activationContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:text="@string/label_activation_config"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="18sp"
|
||||
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_activation_code">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etActivationCode"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="72dp"
|
||||
android:inputType="text"
|
||||
android:textSize="18sp"
|
||||
android:textColor="@color/text_primary" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.App.TextInputLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:hint="@string/hint_device_name">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etDeviceName"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="72dp"
|
||||
android:inputType="text"
|
||||
android:textSize="18sp"
|
||||
android:textColor="@color/text_primary" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnActivate"
|
||||
style="@style/Widget.App.Button"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="72dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:text="@string/btn_activate"
|
||||
android:textSize="20sp" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
@@ -293,7 +390,7 @@
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="特殊状态"
|
||||
android:text="@string/label_special_state"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="22sp"
|
||||
android:textStyle="bold" />
|
||||
@@ -302,7 +399,7 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="启用特定场景下的行为逻辑"
|
||||
android:text="@string/desc_special_state"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="16sp" />
|
||||
</LinearLayout>
|
||||
@@ -371,6 +468,15 @@
|
||||
android:textSize="20sp"
|
||||
android:text="@string/btn_clear_task" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnAbout"
|
||||
style="@style/Widget.App.OutlinedButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="72dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/btn_about"
|
||||
android:textSize="20sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvVersion"
|
||||
android:layout_width="wrap_content"
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
<string name="title_main">主界面</string>
|
||||
<string name="btn_settings">设置</string>
|
||||
<string name="title_settings">设置</string>
|
||||
<string name="label_ip_config">网络 IP 配置</string>
|
||||
<string name="hint_ip_address">请输入 IP 地址 (例如 192.168.1.100)</string>
|
||||
<string name="btn_save">保存</string>
|
||||
<string name="label_ip_config">服务配置</string>
|
||||
<string name="hint_ip_address">请输入 base_url (例如 http://192.168.11.24:8088)</string>
|
||||
<string name="btn_save">确认生效</string>
|
||||
<string name="btn_back">返回主界面</string>
|
||||
<string name="msg_ip_saved">IP 已保存: %1$s</string>
|
||||
<string name="msg_invalid_ip">请输入有效的 IP 地址</string>
|
||||
@@ -37,4 +37,30 @@
|
||||
<string name="livekit_status_permission">需要麦克风/摄像头权限</string>
|
||||
<string name="btn_clear_task">清除当前任务</string>
|
||||
<string name="msg_task_cleared">当前任务已清除</string>
|
||||
<string name="hint_login_username">请输入登录用户名</string>
|
||||
<string name="hint_login_password">请输入登录密码</string>
|
||||
<string name="label_activation_config">设备激活</string>
|
||||
<string name="hint_activation_code">请输入激活码</string>
|
||||
<string name="hint_device_name">请输入机器名称</string>
|
||||
<string name="btn_activate">激活设备</string>
|
||||
<string name="label_activation_status">激活状态:%1$s</string>
|
||||
<string name="status_activated">已激活</string>
|
||||
<string name="status_not_activated">未激活</string>
|
||||
<string name="msg_base_url_saved">base_url 已生效: %1$s</string>
|
||||
<string name="msg_base_url_changed_reset">base_url 已变更,激活信息已重置</string>
|
||||
<string name="msg_activate_success">激活成功,MQTT 配置已更新</string>
|
||||
<string name="msg_activate_failed">激活失败,请检查账号、网络或激活码</string>
|
||||
<string name="msg_fetch_mqtt_failed">激活成功,但拉取运行配置失败</string>
|
||||
<string name="msg_activation_required">请先激活应用</string>
|
||||
<string name="btn_about">关于</string>
|
||||
<string name="title_about">关于</string>
|
||||
<string name="about_base_url">base_url: %1$s</string>
|
||||
<string name="about_device_id">deviceId: %1$s</string>
|
||||
<string name="about_activation_code">activationCode: %1$s</string>
|
||||
<string name="about_device_name">deviceName: %1$s</string>
|
||||
<string name="about_activated">激活状态: %1$s</string>
|
||||
<string name="about_version">版本: %1$s</string>
|
||||
<string name="msg_input_required">请完整填写必填项</string>
|
||||
<string name="label_special_state">特殊状态</string>
|
||||
<string name="desc_special_state">启用特定场景下的行为逻辑</string>
|
||||
</resources>
|
||||
|
||||
10
app/src/main/res/xml/network_security_config.xml
Normal file
10
app/src/main/res/xml/network_security_config.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config>
|
||||
<base-config cleartextTrafficPermitted="false" />
|
||||
<domain-config cleartextTrafficPermitted="true">
|
||||
<domain includeSubdomains="false">localhost</domain>
|
||||
<domain includeSubdomains="false">127.0.0.1</domain>
|
||||
<domain includeSubdomains="false">10.0.2.2</domain>
|
||||
<domain includeSubdomains="false">192.168.11.24</domain>
|
||||
</domain-config>
|
||||
</network-security-config>
|
||||
Reference in New Issue
Block a user