refactor: 提取任务策略、自动充电和连接服务到独立类

将 MainActivity 中的任务策略逻辑、自动充电调度和连接管理代码提取到独立的类中,以提高代码的可维护性和可测试性。具体包括:
- 创建 MainTaskPolicy 对象封装任务类型定义和行为决策逻辑
- 创建 AutoRechargeScheduler 类处理空闲到达后的自动充电调度
- 创建 WorkflowService 类管理门控和工作流执行
- 创建 ConnectionService 类统一管理 MQTT 和 LiveKit 连接
- 重命名 ConnectionCoordinator 为 ConnectionService 以更准确反映其职责
This commit is contained in:
2026-04-24 19:41:34 +08:00
parent c898345919
commit 663768ce17
5 changed files with 283 additions and 197 deletions

View File

@@ -0,0 +1,54 @@
package com.example.lzwcai_terminal_temi
import android.util.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class AutoRechargeScheduler(
private val scope: CoroutineScope,
private val navController: NavController,
private val getCurrentTask: () -> String,
private val isSpecialStateEnabled: () -> Boolean,
private val getLastArrivalLocation: () -> String?,
private val normalizeLocation: (String?) -> String
) {
private var autoRechargeJob: Job? = null
fun scheduleAfterIdleArrival() {
if (!isAutoRechargeAllowedTask()) {
return
}
if (normalizeLocation(getLastArrivalLocation()) == "homebase") {
return
}
autoRechargeJob?.cancel()
autoRechargeJob = scope.launch {
delay(10_000L)
if (!isAutoRechargeAllowedTask()) {
return@launch
}
if (normalizeLocation(getLastArrivalLocation()) == "homebase") {
return@launch
}
Log.i("MainActivity", "Auto recharge triggered after idle arrival timeout.")
navController.recharge()
}
}
fun cancel(reason: String) {
if (autoRechargeJob?.isActive == true) {
Log.i("MainActivity", "Auto recharge canceled: $reason")
}
autoRechargeJob?.cancel()
autoRechargeJob = null
}
private fun isAutoRechargeAllowedTask(): Boolean {
if (isSpecialStateEnabled()) {
return false
}
return MainTaskPolicy.isIdleTask(getCurrentTask())
}
}

View File

@@ -6,7 +6,7 @@ import android.util.Log
import com.robotemi.sdk.Robot import com.robotemi.sdk.Robot
import java.net.URL import java.net.URL
class ConnectionCoordinator( class ConnectionService(
private val context: Context, private val context: Context,
private val prefs: SharedPreferences, private val prefs: SharedPreferences,
private val robot: Robot, private val robot: Robot,
@@ -64,7 +64,7 @@ class ConnectionCoordinator(
if (host.isNullOrEmpty()) { if (host.isNullOrEmpty()) {
mqttManager = null mqttManager = null
mqttStatusListener(false) mqttStatusListener(false)
Log.w("ConnectionCoordinator", "MQTT disabled: base_url is invalid or not set.") Log.w("ConnectionService", "MQTT disabled: base_url is invalid or not set.")
return return
} }
mqttManager = MqttManager( mqttManager = MqttManager(
@@ -82,7 +82,7 @@ class ConnectionCoordinator(
onPublishStatusSnapshot = onPublishStatusSnapshot onPublishStatusSnapshot = onPublishStatusSnapshot
) )
mqttManager?.connect() mqttManager?.connect()
Log.i("ConnectionCoordinator", "MQTT updated with host=$host") Log.i("ConnectionService", "MQTT updated with host=$host")
} }
fun updateLiveKitConnection(isActivated: Boolean) { fun updateLiveKitConnection(isActivated: Boolean) {

View File

@@ -45,9 +45,6 @@ import kotlinx.coroutines.isActive
import kotlin.random.Random import kotlin.random.Random
import org.json.JSONObject import org.json.JSONObject
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import javax.crypto.Mac import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec import javax.crypto.spec.SecretKeySpec
@@ -55,17 +52,20 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
OnDetectionStateChangedListener, OnReposeStatusChangedListener, SharedPreferences.OnSharedPreferenceChangeListener, OnDetectionStateChangedListener, OnReposeStatusChangedListener, SharedPreferences.OnSharedPreferenceChangeListener,
OnRequestPermissionResultListener, OnBatteryStatusChangedListener, OnMovementStatusChangedListener, OnRequestPermissionResultListener, OnBatteryStatusChangedListener, OnMovementStatusChangedListener,
OnCurrentPositionChangedListener { OnCurrentPositionChangedListener {
private data class BehaviorDecision( companion object {
val skipArrivalAnnouncement: Boolean, private const val STATE_KEY_CURRENT_TASK = "currentTask"
val allowAutoRecharge: Boolean, private const val STATE_KEY_RECEPTION_LOCATION = "receptionLocation"
val allowDoorWorkflow: Boolean, private const val STATE_KEY_RECEPTION_TEXT = "receptionText"
val allowIdleGreeting: Boolean private const val STATE_KEY_RECEPTION_DESTINATION = "receptionDestination"
) private const val STATE_KEY_NOTIFICATION_LOCATION = "notificationLocation"
private const val STATE_KEY_NOTIFICATION_TEXT = "notificationText"
private const val STATE_KEY_LAST_ARRIVAL_LOCATION = "lastArrivalLocation"
}
private lateinit var robot: Robot private lateinit var robot: Robot
private lateinit var binding: ActivityMainBinding private lateinit var binding: ActivityMainBinding
private lateinit var uiState: UiState private lateinit var uiState: UiState
private lateinit var connectionCoordinator: ConnectionCoordinator private lateinit var connectionService: ConnectionService
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"
@@ -97,12 +97,11 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
private val idleConfirmDelayMs = 5000L private val idleConfirmDelayMs = 5000L
private var blinkJob: Job? = null private var blinkJob: Job? = null
private var networkErrorJob: Job? = null private var networkErrorJob: Job? = null
private var autoRechargeJob: Job? = null
private var latestYaw: Float? = null private var latestYaw: Float? = null
private var receptionAnchorYaw: Float? = null private var receptionAnchorYaw: Float? = null
private var isTtsSpeaking: Boolean = false private var isTtsSpeaking: Boolean = false
private var pendingReceptionReturnWorkflow: Boolean = false private lateinit var autoRechargeScheduler: AutoRechargeScheduler
private var lastWorkflowConfigRefreshAt: Long = 0L private lateinit var workflowService: WorkflowService
private lateinit var telemetryManager: TelemetryManager private lateinit var telemetryManager: TelemetryManager
private lateinit var taskController: TaskController private lateinit var taskController: TaskController
private val robotEventHandler = RobotEventHandler() private val robotEventHandler = RobotEventHandler()
@@ -133,15 +132,15 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
navCon = NavController(robot) navCon = NavController(robot)
permissionManager = PermissionManager(robot) permissionManager = PermissionManager(robot)
val restoredTask = (savedInstanceState?.getString("currentTask", "") ?: "") val restoredTask = (savedInstanceState?.getString(STATE_KEY_CURRENT_TASK, "") ?: "")
.let { if (it == "special") "" else it } .let { if (it == MainTaskPolicy.LEGACY_TASK_SPECIAL) "" else it }
val restoredReceptionLocation = savedInstanceState?.getString("receptionLocation", "") ?: "" val restoredReceptionLocation = savedInstanceState?.getString(STATE_KEY_RECEPTION_LOCATION, "") ?: ""
val restoredReceptionText = savedInstanceState?.getString("receptionText", "") ?: "" val restoredReceptionText = savedInstanceState?.getString(STATE_KEY_RECEPTION_TEXT, "") ?: ""
val restoredReceptionDestination = savedInstanceState?.getString("receptionDestination", "") ?: "" val restoredReceptionDestination = savedInstanceState?.getString(STATE_KEY_RECEPTION_DESTINATION, "") ?: ""
val restoredNotificationLocation = savedInstanceState?.getString("notificationLocation", "") ?: "" val restoredNotificationLocation = savedInstanceState?.getString(STATE_KEY_NOTIFICATION_LOCATION, "") ?: ""
val restoredNotificationText = savedInstanceState?.getString("notificationText", "") ?: "" val restoredNotificationText = savedInstanceState?.getString(STATE_KEY_NOTIFICATION_TEXT, "") ?: ""
if (savedInstanceState != null) { if (savedInstanceState != null) {
lastArrivalLocation = savedInstanceState.getString("lastArrivalLocation") lastArrivalLocation = savedInstanceState.getString(STATE_KEY_LAST_ARRIVAL_LOCATION)
} }
prefs = getSharedPreferences("app_prefs", Context.MODE_PRIVATE) prefs = getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
@@ -166,7 +165,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
clearTaskLauncher.launch(Intent(this, SettingsActivity::class.java)) clearTaskLauncher.launch(Intent(this, SettingsActivity::class.java))
} }
connectionCoordinator = ConnectionCoordinator( connectionService = ConnectionService(
context = this, context = this,
prefs = prefs, prefs = prefs,
robot = robot, robot = robot,
@@ -227,13 +226,28 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
restoredNotificationLocation, restoredNotificationLocation,
restoredNotificationText restoredNotificationText
) )
autoRechargeScheduler = AutoRechargeScheduler(
scope = mainScope,
navController = navCon,
getCurrentTask = { taskController.currentTask },
isSpecialStateEnabled = { isSpecialStateEnabled() },
getLastArrivalLocation = { lastArrivalLocation },
normalizeLocation = { value -> robotEventHandler.normalizeLocation(value) }
)
workflowService = WorkflowService(
context = this@MainActivity,
prefs = prefs,
scope = mainScope,
normalizeLocation = { value -> robotEventHandler.normalizeLocation(value) },
onWorkflowFailed = { showNetworkErrorBanner() }
)
binding.btnReception.setOnClickListener { binding.btnReception.setOnClickListener {
val destination = taskController.confirmReception() val destination = taskController.confirmReception()
if (destination.isNullOrBlank()) { if (destination.isNullOrBlank()) {
return@setOnClickListener return@setOnClickListener
} }
pendingReceptionReturnWorkflow = true workflowService.markReceptionReturnPending()
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)
navCon.goTo(destination, false) navCon.goTo(destination, false)
@@ -245,7 +259,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
locationProvider = { lastArrivalLocation }, locationProvider = { lastArrivalLocation },
mqttConnectedProvider = { isMqttConnected }, mqttConnectedProvider = { isMqttConnected },
liveKitConnectedProvider = { isLiveKitConnected }, liveKitConnectedProvider = { isLiveKitConnected },
publish = { topic, payload -> connectionCoordinator.publish(topic, payload) }, publish = { topic, payload -> connectionService.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)
@@ -271,8 +285,8 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
robot.constraintBeWith() robot.constraintBeWith()
updateActivationBanner() updateActivationBanner()
if (!isActivated()) { if (!isActivated()) {
connectionCoordinator.disconnectMqtt() connectionService.disconnectMqtt()
connectionCoordinator.disconnectLiveKit() connectionService.disconnectLiveKit()
setMqttConnectionStatus(false) setMqttConnectionStatus(false)
setLiveKitStatus(false) setLiveKitStatus(false)
stopBlinking() stopBlinking()
@@ -298,7 +312,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
robot.removeOnCurrentPositionChangedListener(this) robot.removeOnCurrentPositionChangedListener(this)
robot.removeOnRequestPermissionResultListener(this) robot.removeOnRequestPermissionResultListener(this)
// Keep MQTT alive in background/settings // Keep MQTT alive in background/settings
connectionCoordinator.disconnectLiveKit() connectionService.disconnectLiveKit()
stopBlinking() stopBlinking()
telemetryManager.stop() telemetryManager.stop()
} }
@@ -306,7 +320,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
prefs.unregisterOnSharedPreferenceChangeListener(this) prefs.unregisterOnSharedPreferenceChangeListener(this)
connectionCoordinator.release() connectionService.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.")
@@ -334,7 +348,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
} }
override fun onTtsStatusChanged(ttsRequest: TtsRequest) { override fun onTtsStatusChanged(ttsRequest: TtsRequest) {
connectionCoordinator.handleTtsStatusChange(ttsRequest) connectionService.handleTtsStatusChange(ttsRequest)
when (ttsRequest.status) { when (ttsRequest.status) {
TtsRequest.Status.STARTED -> { TtsRequest.Status.STARTED -> {
isTtsSpeaking = true isTtsSpeaking = true
@@ -347,7 +361,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
TtsRequest.Status.NOT_ALLOWED -> { TtsRequest.Status.NOT_ALLOWED -> {
isTtsSpeaking = false isTtsSpeaking = false
Log.i("MainActivity", "TTS finished: ${ttsRequest.speech}") Log.i("MainActivity", "TTS finished: ${ttsRequest.speech}")
if (taskController.currentTask == "patrol") { if (MainTaskPolicy.isTask(taskController.currentTask, MainTaskPolicy.TASK_PATROL)) {
binding.animatedEmojiView.currentExpression = AnimatedEmojiView.Expression.ANGRY binding.animatedEmojiView.currentExpression = AnimatedEmojiView.Expression.ANGRY
} else { } else {
binding.animatedEmojiView.currentExpression = AnimatedEmojiView.Expression.SMILE binding.animatedEmojiView.currentExpression = AnimatedEmojiView.Expression.SMILE
@@ -386,14 +400,14 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
val isAbort = robotEventHandler.isAbortStatus(status) val isAbort = robotEventHandler.isAbortStatus(status)
val isMoving = robotEventHandler.isMovingStatus(status) val isMoving = robotEventHandler.isMovingStatus(status)
if (isMoving) { if (isMoving) {
cancelAutoRecharge("movement_started:$location/$status") autoRechargeScheduler.cancel("movement_started:$location/$status")
taskController.cancelTaskWaitTimeout() taskController.cancelTaskWaitTimeout()
} }
if (normalized != "complete" && !isAbort) { if (normalized != "complete" && !isAbort) {
return return
} }
if (isAbort) { if (isAbort) {
cancelAutoRecharge("movement_aborted:$location/$status") autoRechargeScheduler.cancel("movement_aborted:$location/$status")
taskController.cancelTaskWaitTimeout() taskController.cancelTaskWaitTimeout()
taskController.clearLeavingHomeBase() taskController.clearLeavingHomeBase()
taskController.endNonSpecialTask("goTo aborted: $location, status=$status") taskController.endNonSpecialTask("goTo aborted: $location, status=$status")
@@ -409,25 +423,25 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
prefs.edit().putString("current_location", location).apply() prefs.edit().putString("current_location", location).apply()
if (robotEventHandler.normalizeLocation(location) == "homebase") { if (robotEventHandler.normalizeLocation(location) == "homebase") {
navCon.tiltAngle(20) navCon.tiltAngle(20)
triggerReceptionReturnWorkflowIfNeeded(location) workflowService.triggerReceptionReturnWorkflowIfNeeded(location)
} }
if (taskController.currentTask == "patrol") { if (MainTaskPolicy.isTask(taskController.currentTask, MainTaskPolicy.TASK_PATROL)) {
taskController.handlePatrolArrival(location) taskController.handlePatrolArrival(location)
} }
if (taskController.currentTask == "reception" && if (MainTaskPolicy.isTask(taskController.currentTask, MainTaskPolicy.TASK_RECEPTION) &&
robotEventHandler.normalizeLocation(location) == robotEventHandler.normalizeLocation(location) ==
robotEventHandler.normalizeLocation(taskController.getReceptionLocation()) robotEventHandler.normalizeLocation(taskController.getReceptionLocation())
) { ) {
captureReceptionAnchorYawIfNeeded() captureReceptionAnchorYawIfNeeded()
taskController.startTaskWaitTimeout() taskController.startTaskWaitTimeout()
} }
if (taskController.currentTask != "reception") { if (!MainTaskPolicy.isTask(taskController.currentTask, MainTaskPolicy.TASK_RECEPTION)) {
receptionAnchorYaw = null receptionAnchorYaw = null
} }
if (taskController.handleNotificationArrival(location)) { if (taskController.handleNotificationArrival(location)) {
return return
} }
val behavior = resolveBehaviorDecision() val behavior = MainTaskPolicy.resolveBehaviorDecision(taskController.currentTask, isSpecialStateEnabled())
if (behavior.skipArrivalAnnouncement) { if (behavior.skipArrivalAnnouncement) {
Log.i("MainActivity", "Special state: arrival announcement skipped at $location.") Log.i("MainActivity", "Special state: arrival announcement skipped at $location.")
return return
@@ -437,7 +451,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
robot.speak(ttsRequest) robot.speak(ttsRequest)
Log.i("MainActivity", "Arrived at $location, announcement sent.") Log.i("MainActivity", "Arrived at $location, announcement sent.")
if (behavior.allowAutoRecharge) { if (behavior.allowAutoRecharge) {
scheduleAutoRechargeAfterIdleArrival() autoRechargeScheduler.scheduleAfterIdleArrival()
} }
} }
@@ -478,7 +492,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
private fun handleStableDetectionStateChanged(state: Int) { private fun handleStableDetectionStateChanged(state: Int) {
Log.i("MainActivity", "Stable detection state: $state") Log.i("MainActivity", "Stable detection state: $state")
liveKitManager?.setDetectionActive(state == DETECTED) liveKitManager?.setDetectionActive(state == DETECTED)
if (taskController.currentTask == "reception") { if (MainTaskPolicy.isTask(taskController.currentTask, MainTaskPolicy.TASK_RECEPTION)) {
if (state == DETECTED) { if (state == DETECTED) {
captureReceptionAnchorYawIfNeeded() captureReceptionAnchorYawIfNeeded()
} else if (state == IDLE) { } else if (state == IDLE) {
@@ -489,7 +503,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
Log.i("MainActivity", "Detection event handled by task controller.") Log.i("MainActivity", "Detection event handled by task controller.")
return return
} }
val behavior = resolveBehaviorDecision() val behavior = MainTaskPolicy.resolveBehaviorDecision(taskController.currentTask, isSpecialStateEnabled())
val atHomeBase = robotEventHandler.normalizeLocation(lastArrivalLocation) == "homebase" val atHomeBase = robotEventHandler.normalizeLocation(lastArrivalLocation) == "homebase"
val canHandleDoor = behavior.allowDoorWorkflow && atHomeBase && !taskController.isLeavingHomeBase val canHandleDoor = behavior.allowDoorWorkflow && atHomeBase && !taskController.isLeavingHomeBase
if (canHandleDoor) { if (canHandleDoor) {
@@ -500,7 +514,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 = executeDoorWorkflow(openDoor = true) val result = workflowService.executeDoorWorkflow(openDoor = true)
if (result == null) { if (result == null) {
showNetworkErrorBanner() showNetworkErrorBanner()
} }
@@ -511,7 +525,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 = executeDoorWorkflow(openDoor = false) val result = workflowService.executeDoorWorkflow(openDoor = false)
if (result == null) { if (result == null) {
showNetworkErrorBanner() showNetworkErrorBanner()
} }
@@ -547,9 +561,9 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
fun setCurrentTask(task: String) { fun setCurrentTask(task: String) {
if (task.trim().isNotEmpty()) { if (task.trim().isNotEmpty()) {
cancelAutoRecharge("task_set:$task") autoRechargeScheduler.cancel("task_set:$task")
} }
if (!task.equals("reception", ignoreCase = true)) { if (!MainTaskPolicy.isTask(task, MainTaskPolicy.TASK_RECEPTION)) {
receptionAnchorYaw = null receptionAnchorYaw = null
} }
taskController.setCurrentTask(task) taskController.setCurrentTask(task)
@@ -585,7 +599,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
val isSpecial = isSpecialStateEnabled() val isSpecial = isSpecialStateEnabled()
Log.i("MainActivity", "Special state pref changed: $isSpecial, currentTask: ${taskController.currentTask}") Log.i("MainActivity", "Special state pref changed: $isSpecial, currentTask: ${taskController.currentTask}")
if (isSpecial) { if (isSpecial) {
cancelAutoRecharge("special_state_enabled") autoRechargeScheduler.cancel("special_state_enabled")
} }
} }
if (key == LiveKitManager.PREF_KEY_URL || if (key == LiveKitManager.PREF_KEY_URL ||
@@ -605,11 +619,11 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
} }
private fun updateMqttConnection() { private fun updateMqttConnection() {
connectionCoordinator.updateMqttConnection(isActivated()) connectionService.updateMqttConnection(isActivated())
} }
private fun updateLiveKitConnection() { private fun updateLiveKitConnection() {
connectionCoordinator.updateLiveKitConnection(isActivated()) connectionService.updateLiveKitConnection(isActivated())
} }
private fun hasAudioPermission(): Boolean { private fun hasAudioPermission(): Boolean {
@@ -692,7 +706,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
} }
private fun updateLiveKitStatusSnapshot() { private fun updateLiveKitStatusSnapshot() {
connectionCoordinator.updateLiveKitStatusSnapshot(isActivated()) connectionService.updateLiveKitStatusSnapshot(isActivated())
} }
private fun setLiveKitStatus(connected: Boolean) { private fun setLiveKitStatus(connected: Boolean) {
@@ -719,7 +733,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())
connectionCoordinator.publish("robot/asr", data.toString()) connectionService.publish("robot/asr", data.toString())
} }
private fun extractAsrText(payload: String): String? { private fun extractAsrText(payload: String): String? {
@@ -749,51 +763,6 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
} }
} }
private fun scheduleAutoRechargeAfterIdleArrival() {
if (!shouldAutoRechargeAfterIdleArrival()) {
return
}
if (robotEventHandler.normalizeLocation(lastArrivalLocation) == "homebase") {
return
}
autoRechargeJob?.cancel()
autoRechargeJob = mainScope.launch {
delay(10_000L)
if (!shouldAutoRechargeAfterIdleArrival()) {
return@launch
}
if (robotEventHandler.normalizeLocation(lastArrivalLocation) == "homebase") {
return@launch
}
Log.i("MainActivity", "Auto recharge triggered after idle arrival timeout.")
navCon.recharge()
}
}
private fun shouldAutoRechargeAfterIdleArrival(): Boolean {
return resolveBehaviorDecision().allowAutoRecharge
}
private fun resolveBehaviorDecision(): BehaviorDecision {
val task = taskController.currentTask.trim().lowercase()
val isSpecialState = isSpecialStateEnabled()
val isIdleTask = task.isEmpty() || task == "speech"
return BehaviorDecision(
skipArrivalAnnouncement = isSpecialState && task.isEmpty(),
allowAutoRecharge = !isSpecialState && isIdleTask,
allowDoorWorkflow = !isSpecialState && isIdleTask,
allowIdleGreeting = !isSpecialState && isIdleTask
)
}
private fun cancelAutoRecharge(reason: String) {
if (autoRechargeJob?.isActive == true) {
Log.i("MainActivity", "Auto recharge canceled: $reason")
}
autoRechargeJob?.cancel()
autoRechargeJob = null
}
private fun captureReceptionAnchorYawIfNeeded() { private fun captureReceptionAnchorYawIfNeeded() {
if (receptionAnchorYaw != null) { if (receptionAnchorYaw != null) {
return return
@@ -804,7 +773,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
} }
private fun recoverReceptionFacingDirection() { private fun recoverReceptionFacingDirection() {
if (taskController.currentTask != "reception") { if (!MainTaskPolicy.isTask(taskController.currentTask, MainTaskPolicy.TASK_RECEPTION)) {
return return
} }
val atReceptionLocation = val atReceptionLocation =
@@ -828,98 +797,6 @@ 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 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 workflowName = if (openDoor) "open-door" else "close-door"
return executeConfiguredWorkflow(workflowIdKey, workflowApiKey, workflowName)
}
private suspend fun executeConfiguredWorkflow(
workflowIdKey: String,
workflowApiKey: String,
workflowName: String,
inputs: Any = emptyMap<String, Any>()
): String? {
var workflowId = prefs.getString(workflowIdKey, "").orEmpty().trim()
var apiKey = prefs.getString(workflowApiKey, "").orEmpty().trim()
if (workflowId.isEmpty() || apiKey.isEmpty()) {
refreshWorkflowConfigsIfNeeded()
workflowId = prefs.getString(workflowIdKey, "").orEmpty().trim()
apiKey = prefs.getString(workflowApiKey, "").orEmpty().trim()
}
if (workflowId.isEmpty() || apiKey.isEmpty()) {
Log.w(
"MainActivity",
"Workflow config missing after refresh: workflow=$workflowName, " +
"workflowIdKey=$workflowIdKey, workflowApiKey=$workflowApiKey"
)
return null
}
return HttpManager.workflow_execute(
context = this@MainActivity,
apiKey = apiKey,
workflowId = workflowId,
inputs = inputs
)
}
private fun triggerReceptionReturnWorkflowIfNeeded(location: String) {
if (!pendingReceptionReturnWorkflow) {
return
}
if (robotEventHandler.normalizeLocation(location) != "homebase") {
return
}
pendingReceptionReturnWorkflow = false
mainScope.launch {
val nowText = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(Date())
val inputs = mapOf("flag" to nowText)
val result = executeConfiguredWorkflow(
workflowIdKey = HttpManager.PREF_KEY_VR_WFID,
workflowApiKey = HttpManager.PREF_KEY_VR_WF_KEY,
workflowName = "reception-return-home",
inputs = inputs
)
if (result == null) {
showNetworkErrorBanner()
}
}
}
private suspend fun refreshWorkflowConfigsIfNeeded() {
val now = System.currentTimeMillis()
if (now - lastWorkflowConfigRefreshAt < 5000L) {
return
}
lastWorkflowConfigRefreshAt = now
val runtimeConfigs = HttpManager.fetchRuntimeConfigs(this@MainActivity) ?: return
val workflowKeys = listOf(
HttpManager.PREF_KEY_OD_WFID,
HttpManager.PREF_KEY_OD_WF_KEY,
HttpManager.PREF_KEY_CD_WFID,
HttpManager.PREF_KEY_CD_WF_KEY,
HttpManager.PREF_KEY_VR_WFID,
HttpManager.PREF_KEY_VR_WF_KEY
)
val editor = prefs.edit()
var changed = false
for (key in workflowKeys) {
val value = runtimeConfigs[key]?.trim().orEmpty()
if (value.isEmpty()) {
continue
}
if (prefs.getString(key, "").orEmpty() != value) {
editor.putString(key, value)
changed = true
}
}
if (changed) {
editor.apply()
Log.i("MainActivity", "Workflow configs refreshed from server.")
}
}
private fun isActivated(): Boolean { private fun isActivated(): Boolean {
if (!::prefs.isInitialized) { if (!::prefs.isInitialized) {
return false return false
@@ -960,13 +837,13 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
outState.putString("currentTask", taskController.currentTask) outState.putString(STATE_KEY_CURRENT_TASK, taskController.currentTask)
outState.putString("receptionLocation", taskController.getReceptionLocation()) outState.putString(STATE_KEY_RECEPTION_LOCATION, taskController.getReceptionLocation())
outState.putString("receptionText", taskController.getReceptionText()) outState.putString(STATE_KEY_RECEPTION_TEXT, taskController.getReceptionText())
outState.putString("receptionDestination", taskController.getReceptionDestination()) outState.putString(STATE_KEY_RECEPTION_DESTINATION, taskController.getReceptionDestination())
outState.putString("notificationLocation", taskController.getNotificationLocation()) outState.putString(STATE_KEY_NOTIFICATION_LOCATION, taskController.getNotificationLocation())
outState.putString("notificationText", taskController.getNotificationText()) outState.putString(STATE_KEY_NOTIFICATION_TEXT, taskController.getNotificationText())
outState.putString("lastArrivalLocation", lastArrivalLocation) outState.putString(STATE_KEY_LAST_ARRIVAL_LOCATION, lastArrivalLocation)
} }
override fun onReposeStatusChanged(status: Int, description: String) { override fun onReposeStatusChanged(status: Int, description: String) {

View File

@@ -0,0 +1,38 @@
package com.example.lzwcai_terminal_temi
object MainTaskPolicy {
const val TASK_PATROL = "patrol"
const val TASK_RECEPTION = "reception"
const val TASK_SPEECH = "speech"
const val LEGACY_TASK_SPECIAL = "special"
data class BehaviorDecision(
val skipArrivalAnnouncement: Boolean,
val allowAutoRecharge: Boolean,
val allowDoorWorkflow: Boolean,
val allowIdleGreeting: Boolean
)
fun normalizeTask(task: String?): String {
return task.orEmpty().trim().lowercase()
}
fun isTask(task: String?, expected: String): Boolean {
return normalizeTask(task) == expected
}
fun isIdleTask(task: String?): Boolean {
val normalized = normalizeTask(task)
return normalized.isEmpty() || normalized == TASK_SPEECH
}
fun resolveBehaviorDecision(task: String?, isSpecialState: Boolean): BehaviorDecision {
val isIdleTask = isIdleTask(task)
return BehaviorDecision(
skipArrivalAnnouncement = isSpecialState && normalizeTask(task).isEmpty(),
allowAutoRecharge = !isSpecialState && isIdleTask,
allowDoorWorkflow = !isSpecialState && isIdleTask,
allowIdleGreeting = !isSpecialState && isIdleTask
)
}
}

View File

@@ -0,0 +1,117 @@
package com.example.lzwcai_terminal_temi
import android.content.Context
import android.content.SharedPreferences
import android.util.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
class WorkflowService(
private val context: Context,
private val prefs: SharedPreferences,
private val scope: CoroutineScope,
private val normalizeLocation: (String?) -> String,
private val onWorkflowFailed: () -> Unit
) {
private var pendingReceptionReturnWorkflow: Boolean = false
private var lastWorkflowConfigRefreshAt: Long = 0L
fun markReceptionReturnPending() {
pendingReceptionReturnWorkflow = true
}
fun triggerReceptionReturnWorkflowIfNeeded(location: String) {
if (!pendingReceptionReturnWorkflow) {
return
}
if (normalizeLocation(location) != "homebase") {
return
}
pendingReceptionReturnWorkflow = false
scope.launch {
val nowText = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(Date())
val inputs = mapOf("flag" to nowText)
val result = executeConfiguredWorkflow(
workflowIdKey = HttpManager.PREF_KEY_VR_WFID,
workflowApiKey = HttpManager.PREF_KEY_VR_WF_KEY,
workflowName = "reception-return-home",
inputs = inputs
)
if (result == null) {
onWorkflowFailed()
}
}
}
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 workflowName = if (openDoor) "open-door" else "close-door"
return executeConfiguredWorkflow(workflowIdKey, workflowApiKey, workflowName)
}
private suspend fun executeConfiguredWorkflow(
workflowIdKey: String,
workflowApiKey: String,
workflowName: String,
inputs: Any = emptyMap<String, Any>()
): String? {
var workflowId = prefs.getString(workflowIdKey, "").orEmpty().trim()
var apiKey = prefs.getString(workflowApiKey, "").orEmpty().trim()
if (workflowId.isEmpty() || apiKey.isEmpty()) {
refreshWorkflowConfigsIfNeeded()
workflowId = prefs.getString(workflowIdKey, "").orEmpty().trim()
apiKey = prefs.getString(workflowApiKey, "").orEmpty().trim()
}
if (workflowId.isEmpty() || apiKey.isEmpty()) {
Log.w(
"MainActivity",
"Workflow config missing after refresh: workflow=$workflowName, " +
"workflowIdKey=$workflowIdKey, workflowApiKey=$workflowApiKey"
)
return null
}
return HttpManager.workflow_execute(
context = context,
apiKey = apiKey,
workflowId = workflowId,
inputs = inputs
)
}
private suspend fun refreshWorkflowConfigsIfNeeded() {
val now = System.currentTimeMillis()
if (now - lastWorkflowConfigRefreshAt < 5000L) {
return
}
lastWorkflowConfigRefreshAt = now
val runtimeConfigs = HttpManager.fetchRuntimeConfigs(context) ?: return
val workflowKeys = listOf(
HttpManager.PREF_KEY_OD_WFID,
HttpManager.PREF_KEY_OD_WF_KEY,
HttpManager.PREF_KEY_CD_WFID,
HttpManager.PREF_KEY_CD_WF_KEY,
HttpManager.PREF_KEY_VR_WFID,
HttpManager.PREF_KEY_VR_WF_KEY
)
val editor = prefs.edit()
var changed = false
for (key in workflowKeys) {
val value = runtimeConfigs[key]?.trim().orEmpty()
if (value.isEmpty()) {
continue
}
if (prefs.getString(key, "").orEmpty() != value) {
editor.putString(key, value)
changed = true
}
}
if (changed) {
editor.apply()
Log.i("MainActivity", "Workflow configs refreshed from server.")
}
}
}