From 1c8a97e4467b3ed58e8810612d8da156e9170e31 Mon Sep 17 00:00:00 2001 From: KremeCN <3265494849@qq.com> Date: Fri, 28 Aug 2026 02:52:37 +0800 Subject: [PATCH] fix(scene): expire stale scheduled occurrences --- .../runtime/AutomationBootstrapper.kt | 21 +++++++++++++------ .../automation/runtime/AutomationEngine.kt | 11 +++++++++- .../runtime/AutomationPollWorker.kt | 8 +++++-- .../WorkManagerAutomationScheduleStore.kt | 2 ++ 4 files changed, 33 insertions(+), 9 deletions(-) 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 c93a9ab..c25d22f 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 @@ -6,6 +6,8 @@ import androidx.lifecycle.ProcessLifecycleOwner import com.flagship.abox.manager.domain.ProviderProfileId import com.flagship.abox.manager.domain.ProviderProfileStore import com.flagship.abox.manager.domain.SessionCoordinator +import com.flagship.abox.manager.domain.SessionValidity +import com.flagship.abox.manager.domain.DomainResult import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay @@ -36,14 +38,21 @@ class AutomationBootstrapper( previousProfileId?.takeIf { it != active?.id }?.let { scheduler.cancel(it) } previousProfileId = active?.id if (active != null && session?.profileId == active.id) { - scheduler.reconcile(active.id) while (true) { isForeground.first { it } - try { - engine.evaluate(active.id) - } catch (cancelled: CancellationException) { - throw cancelled - } catch (_: Exception) {} + 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) {} + } else { + scheduler.cancel(active.id) + } + is DomainResult.Failure -> Unit + } if (isForeground.value) delay(60_000L) } } else if (active != null) { 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 fe6f836..fe676f1 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 @@ -38,6 +38,7 @@ data class ScheduledAutomationOccurrence( val sceneId: String, val triggerId: String, val occurrenceKey: String, + val scheduledAtEpochMillis: Long, ) class DefaultAutomationEngine( @@ -88,7 +89,8 @@ class DefaultAutomationEngine( val evaluation = if ( trigger is SceneTrigger.Time && scheduledOccurrence?.sceneId == scene.id.value && - scheduledOccurrence.triggerId == trigger.id.value + scheduledOccurrence.triggerId == trigger.id.value && + snapshot.observedAtEpochMillis in scheduledOccurrence.scheduledAtEpochMillis..scheduledOccurrence.expiresAtEpochMillis ) { TriggerEvaluation.Matched(scheduledOccurrence.occurrenceKey) } else { @@ -167,4 +169,11 @@ class DefaultAutomationEngine( socketStatuses = socketDevices.associateWith { socketGateway.getStatus(profileId, it) }, ) } + + private val ScheduledAutomationOccurrence.expiresAtEpochMillis: Long + get() = scheduledAtEpochMillis + SCHEDULED_OCCURRENCE_GRACE_MILLIS + + private companion object { + const val SCHEDULED_OCCURRENCE_GRACE_MILLIS = 20L * 60L * 1000L + } } diff --git a/android/feature/automation/src/main/kotlin/com/flagship/abox/manager/automation/runtime/AutomationPollWorker.kt b/android/feature/automation/src/main/kotlin/com/flagship/abox/manager/automation/runtime/AutomationPollWorker.kt index 3a55f3a..e29aa51 100644 --- a/android/feature/automation/src/main/kotlin/com/flagship/abox/manager/automation/runtime/AutomationPollWorker.kt +++ b/android/feature/automation/src/main/kotlin/com/flagship/abox/manager/automation/runtime/AutomationPollWorker.kt @@ -31,12 +31,16 @@ class AutomationPollWorker @AssistedInject constructor( return when (val validity = sessionCoordinator.validateSession(profileId)) { is DomainResult.Failure -> Result.retry() is DomainResult.Success -> if (validity.value != SessionValidity.VALID) { - Result.retry() + automationScheduler.cancel(profileId) + Result.success() } else { val scheduledOccurrence = inputData.getString(WorkManagerAutomationScheduleStore.KEY_OCCURRENCE_KEY)?.let { key -> val sceneId = inputData.getString(WorkManagerAutomationScheduleStore.KEY_SCENE_ID) val triggerId = inputData.getString(WorkManagerAutomationScheduleStore.KEY_TRIGGER_ID) - if (sceneId != null && triggerId != null) ScheduledAutomationOccurrence(sceneId, triggerId, key) else null + val scheduledAt = inputData.getLong(WorkManagerAutomationScheduleStore.KEY_SCHEDULED_AT_EPOCH_MILLIS, -1L) + if (sceneId != null && triggerId != null && scheduledAt >= 0L) { + ScheduledAutomationOccurrence(sceneId, triggerId, key, scheduledAt) + } else null } when (automationEngine.evaluate(profileId, scheduledOccurrence)) { is DomainResult.Success -> { diff --git a/android/feature/automation/src/main/kotlin/com/flagship/abox/manager/automation/runtime/WorkManagerAutomationScheduleStore.kt b/android/feature/automation/src/main/kotlin/com/flagship/abox/manager/automation/runtime/WorkManagerAutomationScheduleStore.kt index 8e0c66f..35a8994 100644 --- a/android/feature/automation/src/main/kotlin/com/flagship/abox/manager/automation/runtime/WorkManagerAutomationScheduleStore.kt +++ b/android/feature/automation/src/main/kotlin/com/flagship/abox/manager/automation/runtime/WorkManagerAutomationScheduleStore.kt @@ -56,6 +56,7 @@ class WorkManagerAutomationScheduleStore(context: Context) : AutomationScheduleS .putString(KEY_SCENE_ID, next.scene.id.value) .putString(KEY_TRIGGER_ID, next.trigger.id.value) .putString(KEY_OCCURRENCE_KEY, "${next.scheduledAt.toLocalDate()}:${next.trigger.timeOfDay}:${next.trigger.id.value}") + .putLong(KEY_SCHEDULED_AT_EPOCH_MILLIS, next.scheduledAt.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()) .build(), ) .build() @@ -84,5 +85,6 @@ class WorkManagerAutomationScheduleStore(context: Context) : AutomationScheduleS const val KEY_SCENE_ID = "scene_id" const val KEY_TRIGGER_ID = "trigger_id" const val KEY_OCCURRENCE_KEY = "occurrence_key" + const val KEY_SCHEDULED_AT_EPOCH_MILLIS = "scheduled_at_epoch_millis" } }