feat: stream scene execution progress

This commit is contained in:
2026-08-26 20:15:54 +08:00
parent ca4c34f21c
commit 713106df14
2 changed files with 63 additions and 5 deletions
@@ -8,6 +8,9 @@ import com.flagship.abox.manager.domain.Scene
import com.flagship.abox.manager.domain.SceneExecutionResult
import com.flagship.abox.manager.domain.SceneExecutor
import com.flagship.abox.manager.domain.SceneId
import com.flagship.abox.manager.domain.SceneExecutionId
import com.flagship.abox.manager.domain.SceneExecutionSource
import com.flagship.abox.manager.domain.SceneExecutionUpdate
import com.flagship.abox.manager.domain.SceneRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@@ -15,6 +18,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.collect
sealed interface SceneExecutionUiState {
data object Loading : SceneExecutionUiState
@@ -71,11 +75,19 @@ class SceneExecutionViewModel @Inject constructor(
_uiState.value = SceneExecutionUiState.Failed("Active profile mismatch")
return@launch
}
when (val result = sceneExecutor.execute(scene)) {
is DomainResult.Success -> _uiState.value = SceneExecutionUiState.Completed(result.value)
is DomainResult.Failure -> _uiState.value = SceneExecutionUiState.Failed(
result.error.detail ?: "Execution failed",
sceneExecutor.execute(
scene = scene,
executionId = SceneExecutionId("manual-${scene.id.value}"),
source = SceneExecutionSource.USER,
).collect { update ->
when (update) {
is SceneExecutionUpdate.Completed -> _uiState.value = SceneExecutionUiState.Completed(update.result)
is SceneExecutionUpdate.Rejected -> _uiState.value = SceneExecutionUiState.Failed(
update.error.detail ?: "Execution failed",
)
is SceneExecutionUpdate.Started -> _uiState.value = SceneExecutionUiState.Running(update.scene)
else -> Unit
}
}
}
}
@@ -9,6 +9,8 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
/**
* Production implementation of [SceneExecutor].
@@ -24,6 +26,50 @@ class DefaultSceneExecutor(
) : SceneExecutor {
private val runningScenes = ConcurrentHashMap<SceneId, Boolean>()
override fun execute(
scene: Scene,
executionId: SceneExecutionId,
source: SceneExecutionSource,
): Flow<SceneExecutionUpdate> = flow {
val alreadyRunning = runningScenes.putIfAbsent(scene.id, true) != null
if (alreadyRunning) {
emit(SceneExecutionUpdate.Rejected(DomainError.OperationFailed(detail = "Scene is already running")))
return@flow
}
val startTime = System.currentTimeMillis()
try {
emit(SceneExecutionUpdate.Started(executionId, scene))
val results = mutableListOf<SceneActionExecutionResult>()
var aborted = false
for (action in scene.actions) {
if (aborted) {
val skipped = SceneActionExecutionResult(action, SceneActionOutcome.SKIPPED, null)
results += skipped
emit(SceneExecutionUpdate.StepFinished(skipped))
continue
}
emit(SceneExecutionUpdate.StepPending(action))
emit(SceneExecutionUpdate.StepRunning(action))
val started = System.currentTimeMillis()
val result = executeAction(scene.profileId, action).copy(
executionDurationMillis = System.currentTimeMillis() - started,
)
results += result
emit(SceneExecutionUpdate.StepFinished(result))
if (result.outcome == SceneActionOutcome.FAILURE && scene.strategy == ExecutionStrategy.ABORT_ON_ERROR) {
aborted = true
}
}
emit(
SceneExecutionUpdate.Completed(
SceneExecutionResult(scene, results, System.currentTimeMillis() - startTime),
),
)
} finally {
runningScenes.remove(scene.id)
}
}
override suspend fun execute(scene: Scene): DomainResult<SceneExecutionResult> {
val alreadyRunning = runningScenes.putIfAbsent(scene.id, true) != null
if (alreadyRunning) {