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:fullBackupContent="@xml/backup_rules"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.Lzwcaiterminaltemi"
|
android:theme="@style/Theme.Lzwcaiterminaltemi"
|
||||||
android:usesCleartextTraffic="true"
|
android:usesCleartextTraffic="false"
|
||||||
tools:targetApi="31">
|
tools:targetApi="31">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="com.robotemi.sdk.metadata.SKILL"
|
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 {
|
object HttpManager {
|
||||||
private const val TAG = "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) {
|
suspend fun workflow_execute(context: Context, apiKey: String, workflowId: String, inputs: Any): String? = withContext(Dispatchers.IO) {
|
||||||
var result: String? = null
|
var result: String? = null
|
||||||
try {
|
try {
|
||||||
val prefs = context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
|
val baseUrl = getBaseUrl(context)
|
||||||
val ip = prefs.getString("network_ip", "") ?: ""
|
if (baseUrl.isEmpty()) {
|
||||||
if (ip.isEmpty()) {
|
Log.e(TAG, "No base_url configured")
|
||||||
Log.e(TAG, "No IP address configured")
|
|
||||||
return@withContext null
|
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 url = URL(workflowUrl)
|
||||||
val connection = url.openConnection() as HttpURLConnection
|
val connection = url.openConnection() as HttpURLConnection
|
||||||
connection.requestMethod = "POST"
|
connection.requestMethod = "POST"
|
||||||
connection.setRequestProperty("Content-Type", "application/json")
|
connection.setRequestProperty("Content-Type", "application/json")
|
||||||
connection.setRequestProperty("X-API-Key", apiKey)
|
connection.setRequestProperty("X-API-Key", apiKey)
|
||||||
|
connection.setRequestProperty("token", token)
|
||||||
connection.doOutput = true
|
connection.doOutput = true
|
||||||
connection.doInput = true
|
connection.doInput = true
|
||||||
connection.connectTimeout = 10000
|
connection.connectTimeout = 10000
|
||||||
@@ -65,4 +179,62 @@ object HttpManager {
|
|||||||
}
|
}
|
||||||
return@withContext result
|
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.WindowManager
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import android.graphics.drawable.GradientDrawable
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
@@ -56,7 +55,8 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
|||||||
|
|
||||||
private lateinit var robot: Robot
|
private lateinit var robot: Robot
|
||||||
private lateinit var binding: ActivityMainBinding
|
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 val mainScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||||
private lateinit var prefs: SharedPreferences
|
private lateinit var prefs: SharedPreferences
|
||||||
private val specialStateKey = "special_state"
|
private val specialStateKey = "special_state"
|
||||||
@@ -101,12 +101,14 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
|||||||
private var receptionAnchorYaw: Float? = null
|
private var receptionAnchorYaw: Float? = null
|
||||||
private lateinit var telemetryManager: TelemetryManager
|
private lateinit var telemetryManager: TelemetryManager
|
||||||
private lateinit var taskController: TaskController
|
private lateinit var taskController: TaskController
|
||||||
|
private val robotEventHandler = RobotEventHandler()
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
@SuppressLint("SetTextI18n")
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
uiState = UiState(this, binding)
|
||||||
|
|
||||||
LogManager.configureLogcat(
|
LogManager.configureLogcat(
|
||||||
tags = listOf(
|
tags = listOf(
|
||||||
@@ -162,6 +164,38 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
|||||||
clearTaskLauncher.launch(Intent(this, SettingsActivity::class.java))
|
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(
|
taskController = TaskController(
|
||||||
scope = mainScope,
|
scope = mainScope,
|
||||||
navController = navCon,
|
navController = navCon,
|
||||||
@@ -208,13 +242,14 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
|||||||
locationProvider = { lastArrivalLocation },
|
locationProvider = { lastArrivalLocation },
|
||||||
mqttConnectedProvider = { isMqttConnected },
|
mqttConnectedProvider = { isMqttConnected },
|
||||||
liveKitConnectedProvider = { isLiveKitConnected },
|
liveKitConnectedProvider = { isLiveKitConnected },
|
||||||
publish = { topic, payload -> mqttManager?.publish(topic, payload) },
|
publish = { topic, payload -> connectionCoordinator.publish(topic, payload) },
|
||||||
onLowBattery = { _ ->
|
onLowBattery = { _ ->
|
||||||
val ttsRequest = TtsRequest.create("电量低,请及时充电。", false, language = TtsRequest.Language.ZH_CN)
|
val ttsRequest = TtsRequest.create("电量低,请及时充电。", false, language = TtsRequest.Language.ZH_CN)
|
||||||
robot.speak(ttsRequest)
|
robot.speak(ttsRequest)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
updateActivationBanner()
|
||||||
updateMqttConnection()
|
updateMqttConnection()
|
||||||
updateLiveKitStatusSnapshot()
|
updateLiveKitStatusSnapshot()
|
||||||
}
|
}
|
||||||
@@ -231,14 +266,21 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
|||||||
robot.addOnCurrentPositionChangedListener(this)
|
robot.addOnCurrentPositionChangedListener(this)
|
||||||
robot.addOnRequestPermissionResultListener(this)
|
robot.addOnRequestPermissionResultListener(this)
|
||||||
robot.constraintBeWith()
|
robot.constraintBeWith()
|
||||||
if (mqttManager == null) {
|
updateActivationBanner()
|
||||||
updateMqttConnection()
|
if (!isActivated()) {
|
||||||
|
connectionCoordinator.disconnectMqtt()
|
||||||
|
connectionCoordinator.disconnectLiveKit()
|
||||||
|
setMqttConnectionStatus(false)
|
||||||
|
setLiveKitStatus(false)
|
||||||
|
stopBlinking()
|
||||||
|
telemetryManager.stop()
|
||||||
|
Log.w("MainActivity", "Application is not activated yet.")
|
||||||
} else {
|
} else {
|
||||||
mqttManager?.connect()
|
updateMqttConnection()
|
||||||
|
updateLiveKitConnection()
|
||||||
|
startBlinking()
|
||||||
|
telemetryManager.start()
|
||||||
}
|
}
|
||||||
updateLiveKitConnection()
|
|
||||||
startBlinking()
|
|
||||||
telemetryManager.start()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
@@ -252,8 +294,8 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
|||||||
robot.removeOnMovementStatusChangedListener(this)
|
robot.removeOnMovementStatusChangedListener(this)
|
||||||
robot.removeOnCurrentPositionChangedListener(this)
|
robot.removeOnCurrentPositionChangedListener(this)
|
||||||
robot.removeOnRequestPermissionResultListener(this)
|
robot.removeOnRequestPermissionResultListener(this)
|
||||||
// mqttManager?.disconnect() // Keep MQTT alive in background/settings
|
// Keep MQTT alive in background/settings
|
||||||
liveKitManager?.disconnect()
|
connectionCoordinator.disconnectLiveKit()
|
||||||
stopBlinking()
|
stopBlinking()
|
||||||
telemetryManager.stop()
|
telemetryManager.stop()
|
||||||
}
|
}
|
||||||
@@ -261,8 +303,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
|||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
prefs.unregisterOnSharedPreferenceChangeListener(this)
|
prefs.unregisterOnSharedPreferenceChangeListener(this)
|
||||||
mqttManager?.disconnect()
|
connectionCoordinator.release()
|
||||||
liveKitManager?.release()
|
|
||||||
LogManager.stopLogcatListener()
|
LogManager.stopLogcatListener()
|
||||||
mainScope.cancel()
|
mainScope.cancel()
|
||||||
Log.i("MainActivity", "All resources released on destroy.")
|
Log.i("MainActivity", "All resources released on destroy.")
|
||||||
@@ -290,7 +331,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onTtsStatusChanged(ttsRequest: TtsRequest) {
|
override fun onTtsStatusChanged(ttsRequest: TtsRequest) {
|
||||||
mqttManager?.handleTtsStatusChange(ttsRequest)
|
connectionCoordinator.handleTtsStatusChange(ttsRequest)
|
||||||
when (ttsRequest.status) {
|
when (ttsRequest.status) {
|
||||||
TtsRequest.Status.STARTED -> {
|
TtsRequest.Status.STARTED -> {
|
||||||
Log.i("MainActivity", "TTS 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) {
|
override fun onGoToLocationStatusChanged(location: String, status: String, descriptionId: Int, description: String) {
|
||||||
val normalized = status.lowercase()
|
val normalized = status.lowercase()
|
||||||
val isAbort = normalized in setOf("abort", "aborted", "canceled", "cancelled", "stopped", "stop", "failed", "error")
|
val isAbort = robotEventHandler.isAbortStatus(status)
|
||||||
val isMoving = normalized in setOf("start", "starting", "going", "moving", "navigating", "calculating", "recalculating")
|
val isMoving = robotEventHandler.isMovingStatus(status)
|
||||||
if (isMoving) {
|
if (isMoving) {
|
||||||
cancelAutoRecharge("movement_started:$location/$status")
|
cancelAutoRecharge("movement_started:$location/$status")
|
||||||
taskController.cancelTaskWaitTimeout()
|
taskController.cancelTaskWaitTimeout()
|
||||||
@@ -360,7 +401,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
|||||||
lastArrivalLocation = location
|
lastArrivalLocation = location
|
||||||
lastArrivalAt = now
|
lastArrivalAt = now
|
||||||
prefs.edit().putString("current_location", location).apply()
|
prefs.edit().putString("current_location", location).apply()
|
||||||
if (normalizeLocation(location) == "homebase") {
|
if (robotEventHandler.normalizeLocation(location) == "homebase") {
|
||||||
navCon.tiltAngle(20)
|
navCon.tiltAngle(20)
|
||||||
}
|
}
|
||||||
if (taskController.currentTask == "patrol") {
|
if (taskController.currentTask == "patrol") {
|
||||||
@@ -434,12 +475,12 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (taskController.handleDetectionStateChanged(state)) {
|
if (taskController.handleDetectionStateChanged(state)) {
|
||||||
Log.i("MainActivity", "what the f**k")
|
Log.i("MainActivity", "Detection event handled by task controller.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val isSpecialState = isSpecialStateEnabled()
|
val isSpecialState = isSpecialStateEnabled()
|
||||||
val isIdleTask = taskController.currentTask.isEmpty() || taskController.currentTask == "speech"
|
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
|
val canHandleDoor = isIdleTask && atHomeBase && !taskController.isLeavingHomeBase && !isSpecialState
|
||||||
if (canHandleDoor) {
|
if (canHandleDoor) {
|
||||||
when (state) {
|
when (state) {
|
||||||
@@ -449,12 +490,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
|||||||
val ttsRequest = TtsRequest.create("正在为你开门,请稍等。", false, language = TtsRequest.Language.ZH_CN)
|
val ttsRequest = TtsRequest.create("正在为你开门,请稍等。", false, language = TtsRequest.Language.ZH_CN)
|
||||||
robot.speak(ttsRequest)
|
robot.speak(ttsRequest)
|
||||||
mainScope.launch {
|
mainScope.launch {
|
||||||
val result = HttpManager.workflow_execute(
|
val result = executeDoorWorkflow(openDoor = true)
|
||||||
context = this@MainActivity,
|
|
||||||
apiKey = "wf_865e80f5fc1a4a319474a21d47470863",
|
|
||||||
workflowId = "2031297462423851009",
|
|
||||||
inputs = emptyMap<String, Any>()
|
|
||||||
)
|
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
showNetworkErrorBanner()
|
showNetworkErrorBanner()
|
||||||
}
|
}
|
||||||
@@ -465,12 +501,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
|||||||
val ttsRequest = TtsRequest.create("正准备关门,请小心被夹。", false, language = TtsRequest.Language.ZH_CN)
|
val ttsRequest = TtsRequest.create("正准备关门,请小心被夹。", false, language = TtsRequest.Language.ZH_CN)
|
||||||
robot.speak(ttsRequest)
|
robot.speak(ttsRequest)
|
||||||
mainScope.launch {
|
mainScope.launch {
|
||||||
val result = HttpManager.workflow_execute(
|
val result = executeDoorWorkflow(openDoor = false)
|
||||||
context = this@MainActivity,
|
|
||||||
apiKey = "wf_c02aa853371345dbb29572641d083c24",
|
|
||||||
workflowId = "2031634633458520065",
|
|
||||||
inputs = emptyMap<String, Any>()
|
|
||||||
)
|
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
showNetworkErrorBanner()
|
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) {
|
fun startReceptionMode(location: String, text: String, destination: String) {
|
||||||
taskController.startReceptionMode(location, text, destination)
|
taskController.startReceptionMode(location, text, destination)
|
||||||
}
|
}
|
||||||
@@ -532,8 +554,16 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
||||||
if (key == "network_ip") {
|
if (key == HttpManager.PREF_KEY_BASE_URL ||
|
||||||
Log.i("MainActivity", "IP address changed, re-initializing MQTT connection.")
|
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()
|
updateMqttConnection()
|
||||||
updateLiveKitConnection()
|
updateLiveKitConnection()
|
||||||
}
|
}
|
||||||
@@ -562,53 +592,11 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun updateMqttConnection() {
|
private fun updateMqttConnection() {
|
||||||
mqttManager?.shutdown()
|
connectionCoordinator.updateMqttConnection(isActivated())
|
||||||
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.")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateLiveKitConnection() {
|
private fun updateLiveKitConnection() {
|
||||||
val enabled = prefs.getBoolean(LiveKitManager.PREF_KEY_ENABLED, true)
|
connectionCoordinator.updateLiveKitConnection(isActivated())
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun hasAudioPermission(): Boolean {
|
private fun hasAudioPermission(): Boolean {
|
||||||
@@ -691,32 +679,17 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun updateLiveKitStatusSnapshot() {
|
private fun updateLiveKitStatusSnapshot() {
|
||||||
val enabled = prefs.getBoolean(LiveKitManager.PREF_KEY_ENABLED, true)
|
connectionCoordinator.updateLiveKitStatusSnapshot(isActivated())
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setLiveKitStatus(connected: Boolean) {
|
private fun setLiveKitStatus(connected: Boolean) {
|
||||||
isLiveKitConnected = connected
|
isLiveKitConnected = connected
|
||||||
updateConnectionIndicator()
|
uiState.updateConnectionIndicator(isLiveKitConnected, isMqttConnected)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setMqttConnectionStatus(connected: Boolean) {
|
private fun setMqttConnectionStatus(connected: Boolean) {
|
||||||
isMqttConnected = connected
|
isMqttConnected = connected
|
||||||
updateConnectionIndicator()
|
uiState.updateConnectionIndicator(isLiveKitConnected, isMqttConnected)
|
||||||
if (connected) {
|
if (connected) {
|
||||||
telemetryManager.publishStatusSnapshot("mqtt_connected", true)
|
telemetryManager.publishStatusSnapshot("mqtt_connected", true)
|
||||||
}
|
}
|
||||||
@@ -733,7 +706,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
|||||||
.put("topic", topicLabel)
|
.put("topic", topicLabel)
|
||||||
.put("participant", participantLabel)
|
.put("participant", participantLabel)
|
||||||
.put("ts", System.currentTimeMillis())
|
.put("ts", System.currentTimeMillis())
|
||||||
mqttManager?.publish("robot/asr", data.toString())
|
connectionCoordinator.publish("robot/asr", data.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun extractAsrText(payload: String): String? {
|
private fun extractAsrText(payload: String): String? {
|
||||||
@@ -756,10 +729,10 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
|||||||
|
|
||||||
private fun showNetworkErrorBanner() {
|
private fun showNetworkErrorBanner() {
|
||||||
networkErrorJob?.cancel()
|
networkErrorJob?.cancel()
|
||||||
binding.tvNetworkError.visibility = View.VISIBLE
|
uiState.setNetworkErrorVisible(true)
|
||||||
networkErrorJob = mainScope.launch {
|
networkErrorJob = mainScope.launch {
|
||||||
delay(5000L)
|
delay(5000L)
|
||||||
binding.tvNetworkError.visibility = View.GONE
|
uiState.setNetworkErrorVisible(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -767,7 +740,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
|||||||
if (taskController.currentTask.isNotEmpty()) {
|
if (taskController.currentTask.isNotEmpty()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (normalizeLocation(lastArrivalLocation) == "homebase") {
|
if (robotEventHandler.normalizeLocation(lastArrivalLocation) == "homebase") {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
autoRechargeJob?.cancel()
|
autoRechargeJob?.cancel()
|
||||||
@@ -776,7 +749,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
|||||||
if (taskController.currentTask.isNotEmpty()) {
|
if (taskController.currentTask.isNotEmpty()) {
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
if (normalizeLocation(lastArrivalLocation) == "homebase") {
|
if (robotEventHandler.normalizeLocation(lastArrivalLocation) == "homebase") {
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
Log.i("MainActivity", "Auto recharge triggered after idle arrival timeout.")
|
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 anchorYaw = receptionAnchorYaw ?: return
|
||||||
val currentYaw = latestYaw ?: return
|
val currentYaw = latestYaw ?: return
|
||||||
val delta = normalizeAngle(anchorYaw - currentYaw)
|
val delta = robotEventHandler.normalizeAngle(anchorYaw - currentYaw)
|
||||||
// Ignore tiny drift to avoid jitter.
|
// Ignore tiny drift to avoid jitter.
|
||||||
if (kotlin.math.abs(delta) < 8f) {
|
if (kotlin.math.abs(delta) < 8f) {
|
||||||
return
|
return
|
||||||
@@ -824,33 +797,33 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
|
|||||||
Log.i("MainActivity", "Reception facing recovered by $turn degrees (delta=$delta).")
|
Log.i("MainActivity", "Reception facing recovered by $turn degrees (delta=$delta).")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun normalizeAngle(angle: Float): Float {
|
private suspend fun executeDoorWorkflow(openDoor: Boolean): String? {
|
||||||
var normalized = angle % 360f
|
val workflowIdKey = if (openDoor) HttpManager.PREF_KEY_OD_WFID else HttpManager.PREF_KEY_CD_WFID
|
||||||
if (normalized > 180f) {
|
val workflowApiKey = if (openDoor) HttpManager.PREF_KEY_OD_WF_KEY else HttpManager.PREF_KEY_CD_WF_KEY
|
||||||
normalized -= 360f
|
val workflowId = prefs.getString(workflowIdKey, "").orEmpty().trim()
|
||||||
} else if (normalized < -180f) {
|
val apiKey = prefs.getString(workflowApiKey, "").orEmpty().trim()
|
||||||
normalized += 360f
|
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() {
|
private fun isActivated(): Boolean {
|
||||||
val colorRes = when {
|
if (!::prefs.isInitialized) {
|
||||||
!isLiveKitConnected && !isMqttConnected -> android.R.color.holo_red_dark
|
return false
|
||||||
!isLiveKitConnected && isMqttConnected -> android.R.color.holo_blue_light
|
|
||||||
isLiveKitConnected && !isMqttConnected -> android.R.color.holo_orange_light
|
|
||||||
else -> android.R.color.holo_green_light
|
|
||||||
}
|
}
|
||||||
val indicatorDrawable = binding.statusIndicator.background as GradientDrawable
|
return prefs.getBoolean(HttpManager.PREF_KEY_ACTIVATED, false)
|
||||||
indicatorDrawable.setColor(ContextCompat.getColor(this, colorRes))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun resolveLiveKitUrl(): String {
|
private fun updateActivationBanner() {
|
||||||
val savedUrl = prefs.getString(LiveKitManager.PREF_KEY_URL, "").orEmpty().trim()
|
val activated = isActivated()
|
||||||
if (savedUrl.isNotEmpty()) {
|
uiState.setActivationRequired(!activated)
|
||||||
return savedUrl
|
|
||||||
}
|
|
||||||
return LiveKitManager.DEFAULT_URL
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startBlinking() {
|
private fun startBlinking() {
|
||||||
|
|||||||
@@ -14,13 +14,21 @@ import java.util.LinkedList
|
|||||||
import java.util.Queue
|
import java.util.Queue
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
class MqttManager(
|
class MqttManager(
|
||||||
private val context: Context,
|
context: Context,
|
||||||
private val serverIp: String,
|
private val serverIp: String,
|
||||||
private val robot: Robot,
|
private val robot: Robot,
|
||||||
private val navController: NavController,
|
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 var mqttClient: MqttClient? = null
|
||||||
private val TAG = "MqttManager"
|
private val TAG = "MqttManager"
|
||||||
private val brokerUri = "tcp://$serverIp:1883"
|
private val brokerUri = "tcp://$serverIp:1883"
|
||||||
@@ -28,7 +36,7 @@ class MqttManager(
|
|||||||
private val job = SupervisorJob()
|
private val job = SupervisorJob()
|
||||||
private val scope = CoroutineScope(Dispatchers.IO + job)
|
private val scope = CoroutineScope(Dispatchers.IO + job)
|
||||||
private var reconnectJob: Job? = null
|
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"
|
private val agentDempIdKey = "agent_demp_id"
|
||||||
|
|
||||||
// Streaming text buffer
|
// Streaming text buffer
|
||||||
@@ -88,6 +96,13 @@ class MqttManager(
|
|||||||
updateConnectionStatus(true)
|
updateConnectionStatus(true)
|
||||||
return@launch
|
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")
|
Log.i(TAG, "Attempting to connect to MQTT broker at $brokerUri")
|
||||||
try {
|
try {
|
||||||
val options = MqttConnectOptions().apply {
|
val options = MqttConnectOptions().apply {
|
||||||
@@ -95,8 +110,8 @@ class MqttManager(
|
|||||||
isCleanSession = true
|
isCleanSession = true
|
||||||
connectionTimeout = 10
|
connectionTimeout = 10
|
||||||
keepAliveInterval = 60
|
keepAliveInterval = 60
|
||||||
userName = "lzwc"
|
userName = username
|
||||||
password = "Lzwc@4187.".toCharArray()
|
this.password = password.toCharArray()
|
||||||
}
|
}
|
||||||
mqttClient?.connect(options)
|
mqttClient?.connect(options)
|
||||||
} catch (e: MqttException) {
|
} catch (e: MqttException) {
|
||||||
@@ -224,7 +239,7 @@ class MqttManager(
|
|||||||
val isFinal = obj.optBoolean("is_final", false)
|
val isFinal = obj.optBoolean("is_final", false)
|
||||||
val lang = obj.optString("lang", "").trim()
|
val lang = obj.optString("lang", "").trim()
|
||||||
scope.launch(Dispatchers.Main) {
|
scope.launch(Dispatchers.Main) {
|
||||||
(context as? MainActivity)?.markSpeechTaskActive()
|
onMarkSpeechTaskActive()
|
||||||
}
|
}
|
||||||
processStreamText(text, lang)
|
processStreamText(text, lang)
|
||||||
if (isFinal) {
|
if (isFinal) {
|
||||||
@@ -268,11 +283,13 @@ class MqttManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun handleJsonCommand(obj: JSONObject) {
|
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 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) {
|
when (action) {
|
||||||
"recharge" -> {
|
"recharge" -> {
|
||||||
speak("前往充电桩", "zh")
|
speak("前往充电桩", "zh")
|
||||||
@@ -291,7 +308,7 @@ class MqttManager(
|
|||||||
val text = obj.optString("text", obj.optString("content", ""))
|
val text = obj.optString("text", obj.optString("content", ""))
|
||||||
val lang = obj.optString("lang", "")
|
val lang = obj.optString("lang", "")
|
||||||
scope.launch(Dispatchers.Main) {
|
scope.launch(Dispatchers.Main) {
|
||||||
(context as? MainActivity)?.markSpeechTaskActive()
|
onMarkSpeechTaskActive()
|
||||||
}
|
}
|
||||||
processStreamText(text, lang)
|
processStreamText(text, lang)
|
||||||
}
|
}
|
||||||
@@ -299,7 +316,7 @@ class MqttManager(
|
|||||||
val location = obj.optString("location", obj.optString("target", ""))
|
val location = obj.optString("location", obj.optString("target", ""))
|
||||||
val text = obj.optString("text", obj.optString("content", ""))
|
val text = obj.optString("text", obj.optString("content", ""))
|
||||||
scope.launch(Dispatchers.Main) {
|
scope.launch(Dispatchers.Main) {
|
||||||
(context as? MainActivity)?.startNotificationMode(location, text)
|
onStartNotificationMode(location, text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"repose" -> {
|
"repose" -> {
|
||||||
@@ -331,7 +348,7 @@ class MqttManager(
|
|||||||
}
|
}
|
||||||
"terminate" -> {
|
"terminate" -> {
|
||||||
scope.launch(Dispatchers.Main) {
|
scope.launch(Dispatchers.Main) {
|
||||||
(context as? MainActivity)?.setCurrentTask("")
|
onSetCurrentTask("")
|
||||||
}
|
}
|
||||||
navController.stop()
|
navController.stop()
|
||||||
stopTts()
|
stopTts()
|
||||||
@@ -341,7 +358,7 @@ class MqttManager(
|
|||||||
}
|
}
|
||||||
"status" -> {
|
"status" -> {
|
||||||
scope.launch(Dispatchers.Main) {
|
scope.launch(Dispatchers.Main) {
|
||||||
(context as? MainActivity)?.publishStatusSnapshot("command", true)
|
onPublishStatusSnapshot("command", true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"patrol" -> {
|
"patrol" -> {
|
||||||
@@ -369,11 +386,10 @@ class MqttManager(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
scope.launch(Dispatchers.Main) {
|
scope.launch(Dispatchers.Main) {
|
||||||
val activity = context as? MainActivity
|
|
||||||
if (patrolLocations.isNotEmpty()) {
|
if (patrolLocations.isNotEmpty()) {
|
||||||
activity?.startPatrolMode(patrolLocations, times, waiting, nonStop)
|
onStartPatrolMode(patrolLocations, times, waiting, nonStop)
|
||||||
} else {
|
} else {
|
||||||
activity?.setCurrentTask("")
|
onSetCurrentTask("")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -383,7 +399,7 @@ class MqttManager(
|
|||||||
val text = obj.optString("text", "你是我要接待的贵宾吗?")
|
val text = obj.optString("text", "你是我要接待的贵宾吗?")
|
||||||
val destination = obj.optString("destination", "会议室")
|
val destination = obj.optString("destination", "会议室")
|
||||||
scope.launch(Dispatchers.Main) {
|
scope.launch(Dispatchers.Main) {
|
||||||
(context as? MainActivity)?.startReceptionMode(location, text, destination)
|
onStartReceptionMode(location, text, destination)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> Log.w(TAG, "Unknown command action: $action")
|
else -> Log.w(TAG, "Unknown command action: $action")
|
||||||
@@ -582,7 +598,7 @@ class MqttManager(
|
|||||||
processNextTts()
|
processNextTts()
|
||||||
}
|
}
|
||||||
if (!isTtsPaused && !isTtsBusy && ttsQueue.isEmpty() && speechBuffer.isEmpty()) {
|
if (!isTtsPaused && !isTtsBusy && ttsQueue.isEmpty() && speechBuffer.isEmpty()) {
|
||||||
(context as? MainActivity)?.clearSpeechTaskIfActive()
|
onClearSpeechTaskIfActive()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {}
|
else -> {}
|
||||||
|
|||||||
@@ -5,11 +5,9 @@ import com.robotemi.sdk.Robot
|
|||||||
|
|
||||||
class NavController(private val robot: Robot) {
|
class NavController(private val robot: Robot) {
|
||||||
private val TAG = "NavController"
|
private val TAG = "NavController"
|
||||||
private var playmode = false
|
|
||||||
|
|
||||||
fun recharge(): Boolean {
|
fun recharge(backwards: Boolean = True): Boolean {
|
||||||
playmode = !playmode
|
robot.goTo("home base", backwards)
|
||||||
robot.goTo("home base", playmode)
|
|
||||||
return true
|
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.view.inputmethod.InputMethodManager
|
||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
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 com.robotemi.sdk.Robot
|
||||||
import android.graphics.drawable.GradientDrawable
|
import android.graphics.drawable.GradientDrawable
|
||||||
import androidx.core.content.ContextCompat
|
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() {
|
class SettingsActivity : AppCompatActivity() {
|
||||||
|
|
||||||
@@ -31,6 +38,7 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
private val specialStateKey = "special_state"
|
private val specialStateKey = "special_state"
|
||||||
private lateinit var prefs: SharedPreferences
|
private lateinit var prefs: SharedPreferences
|
||||||
private val agentDempIdKey = "agent_demp_id"
|
private val agentDempIdKey = "agent_demp_id"
|
||||||
|
private val uiScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -41,10 +49,16 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
robot = Robot.getInstance()
|
robot = Robot.getInstance()
|
||||||
|
|
||||||
prefs = getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
|
prefs = getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
|
||||||
val savedIp = prefs.getString("network_ip", "")
|
val savedBaseUrl = HttpManager.getBaseUrl(this)
|
||||||
binding.etIpAddress.setText(savedIp)
|
binding.etIpAddress.setText(savedBaseUrl)
|
||||||
val savedDempId = prefs.getString(agentDempIdKey, "")
|
val savedDempId = prefs.getString(agentDempIdKey, "")
|
||||||
binding.etAgentDempId.setText(savedDempId)
|
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 savedLiveKitUrl = prefs.getString(LiveKitManager.PREF_KEY_URL, resolveLiveKitUrl())
|
||||||
val savedLiveKitRoom = prefs.getString(LiveKitManager.PREF_KEY_ROOM, LiveKitManager.DEFAULT_ROOM)
|
val savedLiveKitRoom = prefs.getString(LiveKitManager.PREF_KEY_ROOM, LiveKitManager.DEFAULT_ROOM)
|
||||||
val savedLiveKitToken = prefs.getString(LiveKitManager.PREF_KEY_TOKEN, "")
|
val savedLiveKitToken = prefs.getString(LiveKitManager.PREF_KEY_TOKEN, "")
|
||||||
@@ -55,26 +69,112 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
binding.switchLiveKitAuto.setOnCheckedChangeListener(null)
|
binding.switchLiveKitAuto.setOnCheckedChangeListener(null)
|
||||||
binding.switchLiveKitAuto.isChecked = isLiveKitEnabled
|
binding.switchLiveKitAuto.isChecked = isLiveKitEnabled
|
||||||
|
|
||||||
// Set Version Name
|
val versionName = getAppVersionName()
|
||||||
val versionName = "2603131822"
|
|
||||||
binding.tvVersion.text = getString(R.string.version_prefix, versionName)
|
binding.tvVersion.text = getString(R.string.version_prefix, versionName)
|
||||||
|
updateActivationUi(isActivated())
|
||||||
|
|
||||||
binding.root.setOnClickListener { hideKeyboard() }
|
binding.root.setOnClickListener { hideKeyboard() }
|
||||||
|
|
||||||
binding.btnSave.setOnClickListener {
|
binding.btnSave.setOnClickListener {
|
||||||
hideKeyboard()
|
hideKeyboard()
|
||||||
val ip = binding.etIpAddress.text.toString().trim()
|
val baseUrl = normalizeBaseUrl(binding.etIpAddress.text.toString())
|
||||||
val dempId = binding.etAgentDempId.text?.toString()?.trim().orEmpty()
|
val dempId = binding.etAgentDempId.text?.toString()?.trim().orEmpty()
|
||||||
if (ip.isNotEmpty()) {
|
val loginUsername = binding.etLoginUsername.text?.toString()?.trim().orEmpty()
|
||||||
prefs.edit()
|
val loginPassword = binding.etLoginPassword.text?.toString().orEmpty()
|
||||||
.putString("network_ip", ip)
|
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)
|
.putString(agentDempIdKey, dempId)
|
||||||
.apply()
|
.putString(HttpManager.PREF_KEY_LOGIN_USERNAME, loginUsername)
|
||||||
Toast.makeText(this, getString(R.string.msg_ip_saved, ip), Toast.LENGTH_SHORT).show()
|
.putString(HttpManager.PREF_KEY_LOGIN_PASSWORD, loginPassword)
|
||||||
finish()
|
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 {
|
} 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()
|
||||||
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()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
binding.btnAbout.setOnClickListener {
|
||||||
|
showAboutDialog()
|
||||||
|
}
|
||||||
|
|
||||||
setupRestartButton()
|
setupRestartButton()
|
||||||
setupSpecialStateSwitch()
|
setupSpecialStateSwitch()
|
||||||
setupLocationSelector()
|
setupLocationSelector()
|
||||||
@@ -116,6 +220,17 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
refreshLocationList()
|
refreshLocationList()
|
||||||
|
updateActivationUi(isActivated())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
persistDraftInputs()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
uiScope.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupSpecialStateSwitch() {
|
private fun setupSpecialStateSwitch() {
|
||||||
@@ -259,4 +374,84 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
return LiveKitManager.DEFAULT_URL
|
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_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/statusIndicator" />
|
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
|
<com.example.lzwcai_terminal_temi.AnimatedEmojiView
|
||||||
android:id="@+id/animatedEmojiView"
|
android:id="@+id/animatedEmojiView"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
|
|||||||
@@ -96,8 +96,7 @@
|
|||||||
android:id="@+id/etIpAddress"
|
android:id="@+id/etIpAddress"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="80dp"
|
android:layout_height="80dp"
|
||||||
android:inputType="number|numberDecimal"
|
android:inputType="textUri"
|
||||||
android:digits="0123456789."
|
|
||||||
android:textSize="20sp"
|
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>
|
||||||
@@ -118,6 +117,38 @@
|
|||||||
android:textColor="@color/text_primary" />
|
android:textColor="@color/text_primary" />
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</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
|
<Button
|
||||||
android:id="@+id/btnSave"
|
android:id="@+id/btnSave"
|
||||||
style="@style/Widget.App.Button"
|
style="@style/Widget.App.Button"
|
||||||
@@ -127,6 +158,72 @@
|
|||||||
android:textSize="20sp"
|
android:textSize="20sp"
|
||||||
android:text="@string/btn_save" />
|
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>
|
</LinearLayout>
|
||||||
</androidx.cardview.widget.CardView>
|
</androidx.cardview.widget.CardView>
|
||||||
|
|
||||||
@@ -310,7 +407,7 @@
|
|||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="特殊状态"
|
android:text="@string/label_special_state"
|
||||||
android:textColor="@color/text_primary"
|
android:textColor="@color/text_primary"
|
||||||
android:textSize="22sp"
|
android:textSize="22sp"
|
||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
@@ -319,7 +416,7 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="8dp"
|
android:layout_marginTop="8dp"
|
||||||
android:text="启用特定场景下的行为逻辑"
|
android:text="@string/desc_special_state"
|
||||||
android:textColor="@color/text_secondary"
|
android:textColor="@color/text_secondary"
|
||||||
android:textSize="16sp" />
|
android:textSize="16sp" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
@@ -387,6 +484,15 @@
|
|||||||
android:textSize="20sp"
|
android:textSize="20sp"
|
||||||
android:text="@string/btn_clear_task" />
|
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
|
<TextView
|
||||||
android:id="@+id/tvVersion"
|
android:id="@+id/tvVersion"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
|||||||
@@ -51,6 +51,27 @@
|
|||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/statusIndicator" />
|
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
|
<com.example.lzwcai_terminal_temi.AnimatedEmojiView
|
||||||
android:id="@+id/animatedEmojiView"
|
android:id="@+id/animatedEmojiView"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
|
|||||||
@@ -87,8 +87,7 @@
|
|||||||
android:id="@+id/etIpAddress"
|
android:id="@+id/etIpAddress"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="80dp"
|
android:layout_height="80dp"
|
||||||
android:inputType="number|numberDecimal"
|
android:inputType="textUri"
|
||||||
android:digits="0123456789."
|
|
||||||
android:textSize="20sp"
|
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>
|
||||||
@@ -109,6 +108,38 @@
|
|||||||
android:textColor="@color/text_primary" />
|
android:textColor="@color/text_primary" />
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</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
|
<Button
|
||||||
android:id="@+id/btnSave"
|
android:id="@+id/btnSave"
|
||||||
style="@style/Widget.App.Button"
|
style="@style/Widget.App.Button"
|
||||||
@@ -118,6 +149,72 @@
|
|||||||
android:textSize="20sp"
|
android:textSize="20sp"
|
||||||
android:text="@string/btn_save" />
|
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>
|
</LinearLayout>
|
||||||
</androidx.cardview.widget.CardView>
|
</androidx.cardview.widget.CardView>
|
||||||
|
|
||||||
@@ -293,7 +390,7 @@
|
|||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="特殊状态"
|
android:text="@string/label_special_state"
|
||||||
android:textColor="@color/text_primary"
|
android:textColor="@color/text_primary"
|
||||||
android:textSize="22sp"
|
android:textSize="22sp"
|
||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
@@ -302,7 +399,7 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="8dp"
|
android:layout_marginTop="8dp"
|
||||||
android:text="启用特定场景下的行为逻辑"
|
android:text="@string/desc_special_state"
|
||||||
android:textColor="@color/text_secondary"
|
android:textColor="@color/text_secondary"
|
||||||
android:textSize="16sp" />
|
android:textSize="16sp" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
@@ -371,6 +468,15 @@
|
|||||||
android:textSize="20sp"
|
android:textSize="20sp"
|
||||||
android:text="@string/btn_clear_task" />
|
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
|
<TextView
|
||||||
android:id="@+id/tvVersion"
|
android:id="@+id/tvVersion"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
<string name="title_main">主界面</string>
|
<string name="title_main">主界面</string>
|
||||||
<string name="btn_settings">设置</string>
|
<string name="btn_settings">设置</string>
|
||||||
<string name="title_settings">设置</string>
|
<string name="title_settings">设置</string>
|
||||||
<string name="label_ip_config">网络 IP 配置</string>
|
<string name="label_ip_config">服务配置</string>
|
||||||
<string name="hint_ip_address">请输入 IP 地址 (例如 192.168.1.100)</string>
|
<string name="hint_ip_address">请输入 base_url (例如 http://192.168.11.24:8088)</string>
|
||||||
<string name="btn_save">保存</string>
|
<string name="btn_save">确认生效</string>
|
||||||
<string name="btn_back">返回主界面</string>
|
<string name="btn_back">返回主界面</string>
|
||||||
<string name="msg_ip_saved">IP 已保存: %1$s</string>
|
<string name="msg_ip_saved">IP 已保存: %1$s</string>
|
||||||
<string name="msg_invalid_ip">请输入有效的 IP 地址</string>
|
<string name="msg_invalid_ip">请输入有效的 IP 地址</string>
|
||||||
@@ -37,4 +37,30 @@
|
|||||||
<string name="livekit_status_permission">需要麦克风/摄像头权限</string>
|
<string name="livekit_status_permission">需要麦克风/摄像头权限</string>
|
||||||
<string name="btn_clear_task">清除当前任务</string>
|
<string name="btn_clear_task">清除当前任务</string>
|
||||||
<string name="msg_task_cleared">当前任务已清除</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>
|
</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