From b6cc083bc7501c93a20a0b46c602ac7ceaebda7d Mon Sep 17 00:00:00 2001 From: KremeCN <3265494849@qq.com> Date: Fri, 28 Aug 2026 03:44:27 +0800 Subject: [PATCH] fix(scene): make automation transitions atomic --- .../automation/data/RoomSceneRepository.kt | 24 +++++++++++-------- .../manager/automation/di/AutomationModule.kt | 2 +- .../runtime/AutomationBootstrapper.kt | 18 +++++++++----- .../automation/runtime/AutomationEngine.kt | 14 +++++++++-- 4 files changed, 39 insertions(+), 19 deletions(-) diff --git a/android/feature/automation/src/main/kotlin/com/flagship/abox/manager/automation/data/RoomSceneRepository.kt b/android/feature/automation/src/main/kotlin/com/flagship/abox/manager/automation/data/RoomSceneRepository.kt index f242468..a925617 100644 --- a/android/feature/automation/src/main/kotlin/com/flagship/abox/manager/automation/data/RoomSceneRepository.kt +++ b/android/feature/automation/src/main/kotlin/com/flagship/abox/manager/automation/data/RoomSceneRepository.kt @@ -1,6 +1,7 @@ package com.flagship.abox.manager.automation.data import android.content.Context +import androidx.room.withTransaction import com.flagship.abox.manager.domain.AutomationType import com.flagship.abox.manager.domain.ComparisonOperator import com.flagship.abox.manager.domain.DeviceName @@ -27,8 +28,9 @@ import kotlinx.coroutines.flow.map class RoomSceneRepository( context: Context, - private val dao: AutomationDao, + private val database: AutomationDatabase, ) : SceneRepository { + private val dao = database.automationDao() private val seedPreferences = context.getSharedPreferences("scene_seed_state", Context.MODE_PRIVATE) override fun observeScenes(profileId: ProviderProfileId): Flow>> = flow { @@ -57,15 +59,17 @@ class RoomSceneRepository( return DomainResult.Failure(DomainError.InvalidRequest(detail = "Scene profile mismatch")) } return runCatching { - val existing = dao.getScene(profileId.value, scene.id.value) - if (requireExisting && existing == null) throw IllegalArgumentException("Scene not found") - if (!requireExisting && existing != null) throw IllegalArgumentException("Scene already exists") - dao.upsertScene(scene.toEntity()) - existing?.let(SceneDocumentCodec::decode)?.triggers.orEmpty() - .filter { previous -> scene.triggers.none { current -> current.hasSameDefinitionAs(previous) } } - .forEach { changed -> - dao.deleteTriggerState(profileId.value, scene.id.value, changed.id.value) - } + database.withTransaction { + val existing = dao.getScene(profileId.value, scene.id.value) + if (requireExisting && existing == null) throw IllegalArgumentException("Scene not found") + if (!requireExisting && existing != null) throw IllegalArgumentException("Scene already exists") + dao.upsertScene(scene.toEntity()) + existing?.let(SceneDocumentCodec::decode)?.triggers.orEmpty() + .filter { previous -> scene.triggers.none { current -> current.hasSameDefinitionAs(previous) } } + .forEach { changed -> + dao.deleteTriggerState(profileId.value, scene.id.value, changed.id.value) + } + } scene }.fold({ DomainResult.Success(it) }, { DomainResult.Failure(DomainError.InvalidRequest(detail = it.message)) }) } diff --git a/android/feature/automation/src/main/kotlin/com/flagship/abox/manager/automation/di/AutomationModule.kt b/android/feature/automation/src/main/kotlin/com/flagship/abox/manager/automation/di/AutomationModule.kt index 6cfa23c..3adeb45 100644 --- a/android/feature/automation/src/main/kotlin/com/flagship/abox/manager/automation/di/AutomationModule.kt +++ b/android/feature/automation/src/main/kotlin/com/flagship/abox/manager/automation/di/AutomationModule.kt @@ -63,7 +63,7 @@ object AutomationModule { fun provideSceneRepository( @ApplicationContext context: Context, database: AutomationDatabase, - ): SceneRepository = RoomSceneRepository(context, database.automationDao()) + ): SceneRepository = RoomSceneRepository(context, database) @Provides @Singleton diff --git a/android/feature/automation/src/main/kotlin/com/flagship/abox/manager/automation/runtime/AutomationBootstrapper.kt b/android/feature/automation/src/main/kotlin/com/flagship/abox/manager/automation/runtime/AutomationBootstrapper.kt index c25d22f..eaacb1a 100644 --- a/android/feature/automation/src/main/kotlin/com/flagship/abox/manager/automation/runtime/AutomationBootstrapper.kt +++ b/android/feature/automation/src/main/kotlin/com/flagship/abox/manager/automation/runtime/AutomationBootstrapper.kt @@ -38,18 +38,24 @@ class AutomationBootstrapper( previousProfileId?.takeIf { it != active?.id }?.let { scheduler.cancel(it) } previousProfileId = active?.id if (active != null && session?.profileId == active.id) { + var scheduleReady = false while (true) { isForeground.first { it } when (val validity = sessionCoordinator.validateSession(active.id)) { is DomainResult.Success -> if (validity.value == SessionValidity.VALID) { - scheduler.reconcile(active.id) - try { - engine.evaluate(active.id) - } catch (cancelled: CancellationException) { - throw cancelled - } catch (_: Exception) {} + if (!scheduleReady) { + scheduleReady = scheduler.reconcile(active.id) is DomainResult.Success + } + if (scheduleReady) { + try { + engine.evaluate(active.id) + } catch (cancelled: CancellationException) { + throw cancelled + } catch (_: Exception) {} + } } else { scheduler.cancel(active.id) + scheduleReady = false } is DomainResult.Failure -> Unit } diff --git a/android/feature/automation/src/main/kotlin/com/flagship/abox/manager/automation/runtime/AutomationEngine.kt b/android/feature/automation/src/main/kotlin/com/flagship/abox/manager/automation/runtime/AutomationEngine.kt index c501e73..64b1875 100644 --- a/android/feature/automation/src/main/kotlin/com/flagship/abox/manager/automation/runtime/AutomationEngine.kt +++ b/android/feature/automation/src/main/kotlin/com/flagship/abox/manager/automation/runtime/AutomationEngine.kt @@ -118,8 +118,18 @@ class DefaultAutomationEngine( } } if (!newlyMatched) continue - if (conditionEvaluator.evaluate(scene.conditions, snapshot, now) !is ConditionsEvaluation.Satisfied) { - continue + when (conditionEvaluator.evaluate(scene.conditions, snapshot, now)) { + is ConditionsEvaluation.Unavailable -> continue + is ConditionsEvaluation.NotSatisfied -> { + for ((trigger, occurrenceKey) in matchedTimeTriggers) { + when (val claim = runtimeRepository.claimOccurrence(profileId, scene.id, trigger.id, occurrenceKey)) { + is DomainResult.Failure -> return claim + is DomainResult.Success -> Unit + } + } + continue + } + ConditionsEvaluation.Satisfied -> Unit } var executionCount = if (newlyMatchedStateTriggers.isEmpty()) 0 else 1 if (matchedTimeTriggers.isNotEmpty()) {