fix(scene): make automation transitions atomic
This commit is contained in:
+14
-10
@@ -1,6 +1,7 @@
|
|||||||
package com.flagship.abox.manager.automation.data
|
package com.flagship.abox.manager.automation.data
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import androidx.room.withTransaction
|
||||||
import com.flagship.abox.manager.domain.AutomationType
|
import com.flagship.abox.manager.domain.AutomationType
|
||||||
import com.flagship.abox.manager.domain.ComparisonOperator
|
import com.flagship.abox.manager.domain.ComparisonOperator
|
||||||
import com.flagship.abox.manager.domain.DeviceName
|
import com.flagship.abox.manager.domain.DeviceName
|
||||||
@@ -27,8 +28,9 @@ import kotlinx.coroutines.flow.map
|
|||||||
|
|
||||||
class RoomSceneRepository(
|
class RoomSceneRepository(
|
||||||
context: Context,
|
context: Context,
|
||||||
private val dao: AutomationDao,
|
private val database: AutomationDatabase,
|
||||||
) : SceneRepository {
|
) : SceneRepository {
|
||||||
|
private val dao = database.automationDao()
|
||||||
private val seedPreferences = context.getSharedPreferences("scene_seed_state", Context.MODE_PRIVATE)
|
private val seedPreferences = context.getSharedPreferences("scene_seed_state", Context.MODE_PRIVATE)
|
||||||
|
|
||||||
override fun observeScenes(profileId: ProviderProfileId): Flow<DomainResult<List<Scene>>> = flow {
|
override fun observeScenes(profileId: ProviderProfileId): Flow<DomainResult<List<Scene>>> = flow {
|
||||||
@@ -57,15 +59,17 @@ class RoomSceneRepository(
|
|||||||
return DomainResult.Failure(DomainError.InvalidRequest(detail = "Scene profile mismatch"))
|
return DomainResult.Failure(DomainError.InvalidRequest(detail = "Scene profile mismatch"))
|
||||||
}
|
}
|
||||||
return runCatching {
|
return runCatching {
|
||||||
val existing = dao.getScene(profileId.value, scene.id.value)
|
database.withTransaction {
|
||||||
if (requireExisting && existing == null) throw IllegalArgumentException("Scene not found")
|
val existing = dao.getScene(profileId.value, scene.id.value)
|
||||||
if (!requireExisting && existing != null) throw IllegalArgumentException("Scene already exists")
|
if (requireExisting && existing == null) throw IllegalArgumentException("Scene not found")
|
||||||
dao.upsertScene(scene.toEntity())
|
if (!requireExisting && existing != null) throw IllegalArgumentException("Scene already exists")
|
||||||
existing?.let(SceneDocumentCodec::decode)?.triggers.orEmpty()
|
dao.upsertScene(scene.toEntity())
|
||||||
.filter { previous -> scene.triggers.none { current -> current.hasSameDefinitionAs(previous) } }
|
existing?.let(SceneDocumentCodec::decode)?.triggers.orEmpty()
|
||||||
.forEach { changed ->
|
.filter { previous -> scene.triggers.none { current -> current.hasSameDefinitionAs(previous) } }
|
||||||
dao.deleteTriggerState(profileId.value, scene.id.value, changed.id.value)
|
.forEach { changed ->
|
||||||
}
|
dao.deleteTriggerState(profileId.value, scene.id.value, changed.id.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
scene
|
scene
|
||||||
}.fold({ DomainResult.Success(it) }, { DomainResult.Failure(DomainError.InvalidRequest(detail = it.message)) })
|
}.fold({ DomainResult.Success(it) }, { DomainResult.Failure(DomainError.InvalidRequest(detail = it.message)) })
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -63,7 +63,7 @@ object AutomationModule {
|
|||||||
fun provideSceneRepository(
|
fun provideSceneRepository(
|
||||||
@ApplicationContext context: Context,
|
@ApplicationContext context: Context,
|
||||||
database: AutomationDatabase,
|
database: AutomationDatabase,
|
||||||
): SceneRepository = RoomSceneRepository(context, database.automationDao())
|
): SceneRepository = RoomSceneRepository(context, database)
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
|
|||||||
+12
-6
@@ -38,18 +38,24 @@ class AutomationBootstrapper(
|
|||||||
previousProfileId?.takeIf { it != active?.id }?.let { scheduler.cancel(it) }
|
previousProfileId?.takeIf { it != active?.id }?.let { scheduler.cancel(it) }
|
||||||
previousProfileId = active?.id
|
previousProfileId = active?.id
|
||||||
if (active != null && session?.profileId == active.id) {
|
if (active != null && session?.profileId == active.id) {
|
||||||
|
var scheduleReady = false
|
||||||
while (true) {
|
while (true) {
|
||||||
isForeground.first { it }
|
isForeground.first { it }
|
||||||
when (val validity = sessionCoordinator.validateSession(active.id)) {
|
when (val validity = sessionCoordinator.validateSession(active.id)) {
|
||||||
is DomainResult.Success -> if (validity.value == SessionValidity.VALID) {
|
is DomainResult.Success -> if (validity.value == SessionValidity.VALID) {
|
||||||
scheduler.reconcile(active.id)
|
if (!scheduleReady) {
|
||||||
try {
|
scheduleReady = scheduler.reconcile(active.id) is DomainResult.Success
|
||||||
engine.evaluate(active.id)
|
}
|
||||||
} catch (cancelled: CancellationException) {
|
if (scheduleReady) {
|
||||||
throw cancelled
|
try {
|
||||||
} catch (_: Exception) {}
|
engine.evaluate(active.id)
|
||||||
|
} catch (cancelled: CancellationException) {
|
||||||
|
throw cancelled
|
||||||
|
} catch (_: Exception) {}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
scheduler.cancel(active.id)
|
scheduler.cancel(active.id)
|
||||||
|
scheduleReady = false
|
||||||
}
|
}
|
||||||
is DomainResult.Failure -> Unit
|
is DomainResult.Failure -> Unit
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-2
@@ -118,8 +118,18 @@ class DefaultAutomationEngine(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!newlyMatched) continue
|
if (!newlyMatched) continue
|
||||||
if (conditionEvaluator.evaluate(scene.conditions, snapshot, now) !is ConditionsEvaluation.Satisfied) {
|
when (conditionEvaluator.evaluate(scene.conditions, snapshot, now)) {
|
||||||
continue
|
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
|
var executionCount = if (newlyMatchedStateTriggers.isEmpty()) 0 else 1
|
||||||
if (matchedTimeTriggers.isNotEmpty()) {
|
if (matchedTimeTriggers.isNotEmpty()) {
|
||||||
|
|||||||
Reference in New Issue
Block a user