refactor: 提取任务策略、自动充电和连接服务到独立类
将 MainActivity 中的任务策略逻辑、自动充电调度和连接管理代码提取到独立的类中,以提高代码的可维护性和可测试性。具体包括: - 创建 MainTaskPolicy 对象封装任务类型定义和行为决策逻辑 - 创建 AutoRechargeScheduler 类处理空闲到达后的自动充电调度 - 创建 WorkflowService 类管理门控和工作流执行 - 创建 ConnectionService 类统一管理 MQTT 和 LiveKit 连接 - 重命名 ConnectionCoordinator 为 ConnectionService 以更准确反映其职责
This commit is contained in:
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
@@ -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, 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 {
|
||||
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) {
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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.")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user