fix(scene): make automation transitions atomic

This commit is contained in:
2026-08-28 03:44:27 +08:00
parent afb65a25c6
commit b6cc083bc7
4 changed files with 39 additions and 19 deletions
@@ -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<DomainResult<List<Scene>>> = flow {
@@ -57,6 +59,7 @@ class RoomSceneRepository(
return DomainResult.Failure(DomainError.InvalidRequest(detail = "Scene profile mismatch"))
}
return runCatching {
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")
@@ -66,6 +69,7 @@ class RoomSceneRepository(
.forEach { changed ->
dao.deleteTriggerState(profileId.value, scene.id.value, changed.id.value)
}
}
scene
}.fold({ DomainResult.Success(it) }, { DomainResult.Failure(DomainError.InvalidRequest(detail = it.message)) })
}
@@ -63,7 +63,7 @@ object AutomationModule {
fun provideSceneRepository(
@ApplicationContext context: Context,
database: AutomationDatabase,
): SceneRepository = RoomSceneRepository(context, database.automationDao())
): SceneRepository = RoomSceneRepository(context, database)
@Provides
@Singleton
@@ -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)
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
}
@@ -118,9 +118,19 @@ class DefaultAutomationEngine(
}
}
if (!newlyMatched) continue
if (conditionEvaluator.evaluate(scene.conditions, snapshot, now) !is ConditionsEvaluation.Satisfied) {
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()) {
for ((trigger, occurrenceKey) in matchedTimeTriggers) {