From 2624a8e1e50667d03d11c46e53b9153db0bd1bfa Mon Sep 17 00:00:00 2001 From: KremeCN <3265494849@qq.com> Date: Fri, 28 Aug 2026 01:58:16 +0800 Subject: [PATCH] fix(scene): address automation review findings --- android/feature/automation/build.gradle.kts | 2 ++ .../runtime/AutomationBootstrapper.kt | 28 +++++++++++++++---- .../abox/manager/domain/Automation.kt | 17 +++++++---- gradle/libs.versions.toml | 2 ++ 4 files changed, 37 insertions(+), 12 deletions(-) diff --git a/android/feature/automation/build.gradle.kts b/android/feature/automation/build.gradle.kts index 4fab89c..407cdf1 100644 --- a/android/feature/automation/build.gradle.kts +++ b/android/feature/automation/build.gradle.kts @@ -40,6 +40,8 @@ dependencies { kapt(libs.hilt.compiler) implementation(libs.androidx.work.runtime.ktx) implementation(libs.androidx.hilt.work) + kapt(libs.androidx.hilt.compiler) + implementation(libs.androidx.lifecycle.process) implementation(platform(libs.compose.bom)) implementation(libs.compose.ui) implementation(libs.compose.material3) 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 fca1797..f3d224e 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 @@ -1,14 +1,18 @@ package com.flagship.abox.manager.automation.runtime +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +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 kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch -import kotlinx.coroutines.delay -import com.flagship.abox.manager.domain.ProviderProfileId -import kotlinx.coroutines.CancellationException class AutomationBootstrapper( private val profileStore: ProviderProfileStore, @@ -16,20 +20,24 @@ class AutomationBootstrapper( private val scheduler: AutomationScheduler, private val engine: AutomationEngine, private val scope: CoroutineScope, -) { +) : DefaultLifecycleObserver { private var previousProfileId: ProviderProfileId? = null + private val isForeground = MutableStateFlow(false) fun start() { + ProcessLifecycleOwner.get().lifecycle.addObserver(this) scope.launch { combine( profileStore.observeActiveProfile(), sessionCoordinator.observeActiveSession(), - ) { active, session -> active to session }.collectLatest { (active, session) -> + isForeground, + ) { active, session, foreground -> Triple(active, session, foreground) } + .collectLatest { (active, session, foreground) -> 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) { + while (foreground) { try { engine.evaluate(active.id) } catch (cancelled: CancellationException) { @@ -43,4 +51,12 @@ class AutomationBootstrapper( } } } + + override fun onStart(owner: LifecycleOwner) { + isForeground.value = true + } + + override fun onStop(owner: LifecycleOwner) { + isForeground.value = false + } } diff --git a/domain/src/main/kotlin/com/flagship/abox/manager/domain/Automation.kt b/domain/src/main/kotlin/com/flagship/abox/manager/domain/Automation.kt index 76c7a47..8cfb318 100644 --- a/domain/src/main/kotlin/com/flagship/abox/manager/domain/Automation.kt +++ b/domain/src/main/kotlin/com/flagship/abox/manager/domain/Automation.kt @@ -65,12 +65,16 @@ class SceneTriggerEvaluator { private fun evaluateOne(trigger: SceneTrigger, snapshot: AutomationSnapshot, now: LocalDateTime): TriggerEvaluation? = when (trigger) { is SceneTrigger.Time -> { val time = parseTime(trigger.timeOfDay) ?: return TriggerEvaluation.Unavailable(DomainError.InvalidRequest(detail = "Invalid trigger time")) - val dayMatches = trigger.daysOfWeek.isEmpty() || now.dayOfWeek.value in trigger.daysOfWeek - val scheduled = LocalDateTime.of(now.toLocalDate(), time) - val lateness = Duration.between(scheduled, now).toMinutes() - if (dayMatches && lateness in 0..20) { - TriggerEvaluation.Matched("${now.toLocalDate()}:${trigger.timeOfDay}:${trigger.id.value}") - } else null + val scheduled = listOf(now.toLocalDate(), now.toLocalDate().minusDays(1)) + .map { date -> LocalDateTime.of(date, time) } + .firstOrNull { candidate -> + val dayMatches = trigger.daysOfWeek.isEmpty() || candidate.dayOfWeek.value in trigger.daysOfWeek + val lateness = Duration.between(candidate, now).toMinutes() + dayMatches && lateness in 0..TIME_TRIGGER_GRACE_MINUTES + } + scheduled?.let { occurrence -> + TriggerEvaluation.Matched("${occurrence.toLocalDate()}:${trigger.timeOfDay}:${trigger.id.value}") + } } is SceneTrigger.Temperature -> threshold(snapshot.readings[trigger.deviceName], trigger.operator, trigger.thresholdCelsius) { it.temperature @@ -134,6 +138,7 @@ class SceneTriggerEvaluator { private companion object { const val TEMPERATURE_HYSTERESIS = 0.5 const val HUMIDITY_HYSTERESIS = 2.0 + const val TIME_TRIGGER_GRACE_MINUTES = 20L } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fda3fcc..ca3f716 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -28,7 +28,9 @@ androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle- androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" } androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltNavigationCompose" } androidx-hilt-work = { module = "androidx.hilt:hilt-work", version.ref = "hiltNavigationCompose" } +androidx-hilt-compiler = { module = "androidx.hilt:hilt-compiler", version.ref = "hiltNavigationCompose" } androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "work" } +androidx-lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle" } androidx-compose-material3-adaptive = { module = "androidx.compose.material3.adaptive:adaptive", version.ref = "adaptive" } androidx-compose-material3-adaptive-navigation-suite = { module = "androidx.compose.material3:material3-adaptive-navigation-suite", version.ref = "adaptiveNavigationSuite" } androidx-window = { module = "androidx.window:window", version.ref = "window" }