From 663768ce17c3aa6e099ae0cd8d0bfd77e1c29366 Mon Sep 17 00:00:00 2001 From: tanjianbin <632190820@qq.com> Date: Fri, 24 Apr 2026 19:41:34 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=8F=90=E5=8F=96=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E7=AD=96=E7=95=A5=E3=80=81=E8=87=AA=E5=8A=A8=E5=85=85?= =?UTF-8?q?=E7=94=B5=E5=92=8C=E8=BF=9E=E6=8E=A5=E6=9C=8D=E5=8A=A1=E5=88=B0?= =?UTF-8?q?=E7=8B=AC=E7=AB=8B=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 MainActivity 中的任务策略逻辑、自动充电调度和连接管理代码提取到独立的类中,以提高代码的可维护性和可测试性。具体包括: - 创建 MainTaskPolicy 对象封装任务类型定义和行为决策逻辑 - 创建 AutoRechargeScheduler 类处理空闲到达后的自动充电调度 - 创建 WorkflowService 类管理门控和工作流执行 - 创建 ConnectionService 类统一管理 MQTT 和 LiveKit 连接 - 重命名 ConnectionCoordinator 为 ConnectionService 以更准确反映其职责 --- .../AutoRechargeScheduler.kt | 54 ++++ ...ionCoordinator.kt => ConnectionService.kt} | 6 +- .../lzwcai_terminal_temi/MainActivity.kt | 265 +++++------------- .../lzwcai_terminal_temi/MainTaskPolicy.kt | 38 +++ .../lzwcai_terminal_temi/WorkflowService.kt | 117 ++++++++ 5 files changed, 283 insertions(+), 197 deletions(-) create mode 100644 app/src/main/java/com/example/lzwcai_terminal_temi/AutoRechargeScheduler.kt rename app/src/main/java/com/example/lzwcai_terminal_temi/{ConnectionCoordinator.kt => ConnectionService.kt} (96%) create mode 100644 app/src/main/java/com/example/lzwcai_terminal_temi/MainTaskPolicy.kt create mode 100644 app/src/main/java/com/example/lzwcai_terminal_temi/WorkflowService.kt diff --git a/app/src/main/java/com/example/lzwcai_terminal_temi/AutoRechargeScheduler.kt b/app/src/main/java/com/example/lzwcai_terminal_temi/AutoRechargeScheduler.kt new file mode 100644 index 0000000..7b3ce19 --- /dev/null +++ b/app/src/main/java/com/example/lzwcai_terminal_temi/AutoRechargeScheduler.kt @@ -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()) + } +} diff --git a/app/src/main/java/com/example/lzwcai_terminal_temi/ConnectionCoordinator.kt b/app/src/main/java/com/example/lzwcai_terminal_temi/ConnectionService.kt similarity index 96% rename from app/src/main/java/com/example/lzwcai_terminal_temi/ConnectionCoordinator.kt rename to app/src/main/java/com/example/lzwcai_terminal_temi/ConnectionService.kt index bc2c719..f906538 100644 --- a/app/src/main/java/com/example/lzwcai_terminal_temi/ConnectionCoordinator.kt +++ b/app/src/main/java/com/example/lzwcai_terminal_temi/ConnectionService.kt @@ -6,7 +6,7 @@ import android.util.Log import com.robotemi.sdk.Robot import java.net.URL -class ConnectionCoordinator( +class ConnectionService( private val context: Context, private val prefs: SharedPreferences, private val robot: Robot, @@ -64,7 +64,7 @@ class ConnectionCoordinator( if (host.isNullOrEmpty()) { mqttManager = null 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 } mqttManager = MqttManager( @@ -82,7 +82,7 @@ class ConnectionCoordinator( onPublishStatusSnapshot = onPublishStatusSnapshot ) mqttManager?.connect() - Log.i("ConnectionCoordinator", "MQTT updated with host=$host") + Log.i("ConnectionService", "MQTT updated with host=$host") } fun updateLiveKitConnection(isActivated: Boolean) { diff --git a/app/src/main/java/com/example/lzwcai_terminal_temi/MainActivity.kt b/app/src/main/java/com/example/lzwcai_terminal_temi/MainActivity.kt index 13ca0ff..faa2ad1 100644 --- a/app/src/main/java/com/example/lzwcai_terminal_temi/MainActivity.kt +++ b/app/src/main/java/com/example/lzwcai_terminal_temi/MainActivity.kt @@ -45,9 +45,6 @@ import kotlinx.coroutines.isActive import kotlin.random.Random import org.json.JSONObject import java.nio.charset.StandardCharsets -import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale import javax.crypto.Mac import javax.crypto.spec.SecretKeySpec @@ -55,17 +52,20 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG OnDetectionStateChangedListener, OnReposeStatusChangedListener, SharedPreferences.OnSharedPreferenceChangeListener, OnRequestPermissionResultListener, OnBatteryStatusChangedListener, OnMovementStatusChangedListener, OnCurrentPositionChangedListener { - private data class BehaviorDecision( - val skipArrivalAnnouncement: Boolean, - val allowAutoRecharge: Boolean, - val allowDoorWorkflow: Boolean, - val allowIdleGreeting: Boolean - ) + companion object { + private const val STATE_KEY_CURRENT_TASK = "currentTask" + private const val STATE_KEY_RECEPTION_LOCATION = "receptionLocation" + private const val STATE_KEY_RECEPTION_TEXT = "receptionText" + 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 binding: ActivityMainBinding private lateinit var uiState: UiState - private lateinit var connectionCoordinator: ConnectionCoordinator + private lateinit var connectionService: ConnectionService private val mainScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) private lateinit var prefs: SharedPreferences private val specialStateKey = "special_state" @@ -97,12 +97,11 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG private val idleConfirmDelayMs = 5000L private var blinkJob: Job? = null private var networkErrorJob: Job? = null - private var autoRechargeJob: Job? = null private var latestYaw: Float? = null private var receptionAnchorYaw: Float? = null private var isTtsSpeaking: Boolean = false - private var pendingReceptionReturnWorkflow: Boolean = false - private var lastWorkflowConfigRefreshAt: Long = 0L + private lateinit var autoRechargeScheduler: AutoRechargeScheduler + private lateinit var workflowService: WorkflowService private lateinit var telemetryManager: TelemetryManager private lateinit var taskController: TaskController private val robotEventHandler = RobotEventHandler() @@ -133,15 +132,15 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG navCon = NavController(robot) permissionManager = PermissionManager(robot) - val restoredTask = (savedInstanceState?.getString("currentTask", "") ?: "") - .let { if (it == "special") "" else it } - val restoredReceptionLocation = savedInstanceState?.getString("receptionLocation", "") ?: "" - val restoredReceptionText = savedInstanceState?.getString("receptionText", "") ?: "" - val restoredReceptionDestination = savedInstanceState?.getString("receptionDestination", "") ?: "" - val restoredNotificationLocation = savedInstanceState?.getString("notificationLocation", "") ?: "" - val restoredNotificationText = savedInstanceState?.getString("notificationText", "") ?: "" + val restoredTask = (savedInstanceState?.getString(STATE_KEY_CURRENT_TASK, "") ?: "") + .let { if (it == MainTaskPolicy.LEGACY_TASK_SPECIAL) "" else it } + val restoredReceptionLocation = savedInstanceState?.getString(STATE_KEY_RECEPTION_LOCATION, "") ?: "" + val restoredReceptionText = savedInstanceState?.getString(STATE_KEY_RECEPTION_TEXT, "") ?: "" + val restoredReceptionDestination = savedInstanceState?.getString(STATE_KEY_RECEPTION_DESTINATION, "") ?: "" + val restoredNotificationLocation = savedInstanceState?.getString(STATE_KEY_NOTIFICATION_LOCATION, "") ?: "" + val restoredNotificationText = savedInstanceState?.getString(STATE_KEY_NOTIFICATION_TEXT, "") ?: "" if (savedInstanceState != null) { - lastArrivalLocation = savedInstanceState.getString("lastArrivalLocation") + lastArrivalLocation = savedInstanceState.getString(STATE_KEY_LAST_ARRIVAL_LOCATION) } prefs = getSharedPreferences("app_prefs", Context.MODE_PRIVATE) @@ -166,7 +165,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG clearTaskLauncher.launch(Intent(this, SettingsActivity::class.java)) } - connectionCoordinator = ConnectionCoordinator( + connectionService = ConnectionService( context = this, prefs = prefs, robot = robot, @@ -227,13 +226,28 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG restoredNotificationLocation, 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 { val destination = taskController.confirmReception() if (destination.isNullOrBlank()) { return@setOnClickListener } - pendingReceptionReturnWorkflow = true + workflowService.markReceptionReturnPending() val ttsRequest = TtsRequest.create("接待任务确认,请跟我来", false, language = TtsRequest.Language.ZH_CN) robot.speak(ttsRequest) navCon.goTo(destination, false) @@ -245,7 +259,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG locationProvider = { lastArrivalLocation }, mqttConnectedProvider = { isMqttConnected }, liveKitConnectedProvider = { isLiveKitConnected }, - publish = { topic, payload -> connectionCoordinator.publish(topic, payload) }, + publish = { topic, payload -> connectionService.publish(topic, payload) }, onLowBattery = { _ -> val ttsRequest = TtsRequest.create("电量低,请及时充电。", false, language = TtsRequest.Language.ZH_CN) robot.speak(ttsRequest) @@ -271,8 +285,8 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG robot.constraintBeWith() updateActivationBanner() if (!isActivated()) { - connectionCoordinator.disconnectMqtt() - connectionCoordinator.disconnectLiveKit() + connectionService.disconnectMqtt() + connectionService.disconnectLiveKit() setMqttConnectionStatus(false) setLiveKitStatus(false) stopBlinking() @@ -298,7 +312,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG robot.removeOnCurrentPositionChangedListener(this) robot.removeOnRequestPermissionResultListener(this) // Keep MQTT alive in background/settings - connectionCoordinator.disconnectLiveKit() + connectionService.disconnectLiveKit() stopBlinking() telemetryManager.stop() } @@ -306,7 +320,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG override fun onDestroy() { super.onDestroy() prefs.unregisterOnSharedPreferenceChangeListener(this) - connectionCoordinator.release() + connectionService.release() LogManager.stopLogcatListener() mainScope.cancel() Log.i("MainActivity", "All resources released on destroy.") @@ -334,7 +348,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG } override fun onTtsStatusChanged(ttsRequest: TtsRequest) { - connectionCoordinator.handleTtsStatusChange(ttsRequest) + connectionService.handleTtsStatusChange(ttsRequest) when (ttsRequest.status) { TtsRequest.Status.STARTED -> { isTtsSpeaking = true @@ -347,7 +361,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG TtsRequest.Status.NOT_ALLOWED -> { isTtsSpeaking = false 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 } else { binding.animatedEmojiView.currentExpression = AnimatedEmojiView.Expression.SMILE @@ -386,14 +400,14 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG val isAbort = robotEventHandler.isAbortStatus(status) val isMoving = robotEventHandler.isMovingStatus(status) if (isMoving) { - cancelAutoRecharge("movement_started:$location/$status") + autoRechargeScheduler.cancel("movement_started:$location/$status") taskController.cancelTaskWaitTimeout() } if (normalized != "complete" && !isAbort) { return } if (isAbort) { - cancelAutoRecharge("movement_aborted:$location/$status") + autoRechargeScheduler.cancel("movement_aborted:$location/$status") taskController.cancelTaskWaitTimeout() taskController.clearLeavingHomeBase() taskController.endNonSpecialTask("goTo aborted: $location, status=$status") @@ -409,25 +423,25 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG prefs.edit().putString("current_location", location).apply() if (robotEventHandler.normalizeLocation(location) == "homebase") { navCon.tiltAngle(20) - triggerReceptionReturnWorkflowIfNeeded(location) + workflowService.triggerReceptionReturnWorkflowIfNeeded(location) } - if (taskController.currentTask == "patrol") { + if (MainTaskPolicy.isTask(taskController.currentTask, MainTaskPolicy.TASK_PATROL)) { taskController.handlePatrolArrival(location) } - if (taskController.currentTask == "reception" && + if (MainTaskPolicy.isTask(taskController.currentTask, MainTaskPolicy.TASK_RECEPTION) && robotEventHandler.normalizeLocation(location) == robotEventHandler.normalizeLocation(taskController.getReceptionLocation()) ) { captureReceptionAnchorYawIfNeeded() taskController.startTaskWaitTimeout() } - if (taskController.currentTask != "reception") { + if (!MainTaskPolicy.isTask(taskController.currentTask, MainTaskPolicy.TASK_RECEPTION)) { receptionAnchorYaw = null } if (taskController.handleNotificationArrival(location)) { return } - val behavior = resolveBehaviorDecision() + val behavior = MainTaskPolicy.resolveBehaviorDecision(taskController.currentTask, isSpecialStateEnabled()) if (behavior.skipArrivalAnnouncement) { Log.i("MainActivity", "Special state: arrival announcement skipped at $location.") return @@ -437,7 +451,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG robot.speak(ttsRequest) Log.i("MainActivity", "Arrived at $location, announcement sent.") if (behavior.allowAutoRecharge) { - scheduleAutoRechargeAfterIdleArrival() + autoRechargeScheduler.scheduleAfterIdleArrival() } } @@ -478,7 +492,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG private fun handleStableDetectionStateChanged(state: Int) { Log.i("MainActivity", "Stable detection state: $state") liveKitManager?.setDetectionActive(state == DETECTED) - if (taskController.currentTask == "reception") { + if (MainTaskPolicy.isTask(taskController.currentTask, MainTaskPolicy.TASK_RECEPTION)) { if (state == DETECTED) { captureReceptionAnchorYawIfNeeded() } else if (state == IDLE) { @@ -489,7 +503,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG Log.i("MainActivity", "Detection event handled by task controller.") return } - val behavior = resolveBehaviorDecision() + val behavior = MainTaskPolicy.resolveBehaviorDecision(taskController.currentTask, isSpecialStateEnabled()) val atHomeBase = robotEventHandler.normalizeLocation(lastArrivalLocation) == "homebase" val canHandleDoor = behavior.allowDoorWorkflow && atHomeBase && !taskController.isLeavingHomeBase if (canHandleDoor) { @@ -500,7 +514,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG val ttsRequest = TtsRequest.create("正在为你开门,请稍等。", false, language = TtsRequest.Language.ZH_CN) robot.speak(ttsRequest) mainScope.launch { - val result = executeDoorWorkflow(openDoor = true) + val result = workflowService.executeDoorWorkflow(openDoor = true) if (result == null) { showNetworkErrorBanner() } @@ -511,7 +525,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG val ttsRequest = TtsRequest.create("正准备关门,请小心被夹。", false, language = TtsRequest.Language.ZH_CN) robot.speak(ttsRequest) mainScope.launch { - val result = executeDoorWorkflow(openDoor = false) + val result = workflowService.executeDoorWorkflow(openDoor = false) if (result == null) { showNetworkErrorBanner() } @@ -547,9 +561,9 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG fun setCurrentTask(task: String) { 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 } taskController.setCurrentTask(task) @@ -585,7 +599,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG val isSpecial = isSpecialStateEnabled() Log.i("MainActivity", "Special state pref changed: $isSpecial, currentTask: ${taskController.currentTask}") if (isSpecial) { - cancelAutoRecharge("special_state_enabled") + autoRechargeScheduler.cancel("special_state_enabled") } } if (key == LiveKitManager.PREF_KEY_URL || @@ -605,11 +619,11 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG } private fun updateMqttConnection() { - connectionCoordinator.updateMqttConnection(isActivated()) + connectionService.updateMqttConnection(isActivated()) } private fun updateLiveKitConnection() { - connectionCoordinator.updateLiveKitConnection(isActivated()) + connectionService.updateLiveKitConnection(isActivated()) } private fun hasAudioPermission(): Boolean { @@ -692,7 +706,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG } private fun updateLiveKitStatusSnapshot() { - connectionCoordinator.updateLiveKitStatusSnapshot(isActivated()) + connectionService.updateLiveKitStatusSnapshot(isActivated()) } private fun setLiveKitStatus(connected: Boolean) { @@ -719,7 +733,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG .put("topic", topicLabel) .put("participant", participantLabel) .put("ts", System.currentTimeMillis()) - connectionCoordinator.publish("robot/asr", data.toString()) + connectionService.publish("robot/asr", data.toString()) } 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() { if (receptionAnchorYaw != null) { return @@ -804,7 +773,7 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG } private fun recoverReceptionFacingDirection() { - if (taskController.currentTask != "reception") { + if (!MainTaskPolicy.isTask(taskController.currentTask, MainTaskPolicy.TASK_RECEPTION)) { return } val atReceptionLocation = @@ -828,98 +797,6 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG 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? { - 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 { if (!::prefs.isInitialized) { return false @@ -960,13 +837,13 @@ class MainActivity : AppCompatActivity(), OnRobotReadyListener, TtsListener, OnG override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) - outState.putString("currentTask", taskController.currentTask) - outState.putString("receptionLocation", taskController.getReceptionLocation()) - outState.putString("receptionText", taskController.getReceptionText()) - outState.putString("receptionDestination", taskController.getReceptionDestination()) - outState.putString("notificationLocation", taskController.getNotificationLocation()) - outState.putString("notificationText", taskController.getNotificationText()) - outState.putString("lastArrivalLocation", lastArrivalLocation) + outState.putString(STATE_KEY_CURRENT_TASK, taskController.currentTask) + outState.putString(STATE_KEY_RECEPTION_LOCATION, taskController.getReceptionLocation()) + outState.putString(STATE_KEY_RECEPTION_TEXT, taskController.getReceptionText()) + outState.putString(STATE_KEY_RECEPTION_DESTINATION, taskController.getReceptionDestination()) + outState.putString(STATE_KEY_NOTIFICATION_LOCATION, taskController.getNotificationLocation()) + outState.putString(STATE_KEY_NOTIFICATION_TEXT, taskController.getNotificationText()) + outState.putString(STATE_KEY_LAST_ARRIVAL_LOCATION, lastArrivalLocation) } override fun onReposeStatusChanged(status: Int, description: String) { diff --git a/app/src/main/java/com/example/lzwcai_terminal_temi/MainTaskPolicy.kt b/app/src/main/java/com/example/lzwcai_terminal_temi/MainTaskPolicy.kt new file mode 100644 index 0000000..7316b0a --- /dev/null +++ b/app/src/main/java/com/example/lzwcai_terminal_temi/MainTaskPolicy.kt @@ -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 + ) + } +} diff --git a/app/src/main/java/com/example/lzwcai_terminal_temi/WorkflowService.kt b/app/src/main/java/com/example/lzwcai_terminal_temi/WorkflowService.kt new file mode 100644 index 0000000..dcb5a82 --- /dev/null +++ b/app/src/main/java/com/example/lzwcai_terminal_temi/WorkflowService.kt @@ -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? { + 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.") + } + } +}