fix(scene): schedule time triggers independently

This commit is contained in:
2026-08-28 03:27:23 +08:00
parent 8b92c84a27
commit afb65a25c6
3 changed files with 36 additions and 51 deletions
@@ -15,13 +15,7 @@ import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
private val notificationPermissionLauncher = private val notificationPermissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted -> registerForActivityResult(ActivityResultContracts.RequestPermission()) {}
val canAskAgain = !granted && shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS)
getSharedPreferences(NOTIFICATION_PERMISSION_STATE, MODE_PRIVATE)
.edit()
.putBoolean(KEY_PERMISSION_REQUESTED, !canAskAgain)
.apply()
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -39,16 +33,6 @@ class MainActivity : ComponentActivity() {
return return
} }
val permissionState = getSharedPreferences(NOTIFICATION_PERMISSION_STATE, MODE_PRIVATE)
val canAskAgain = shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS)
if (permissionState.getBoolean(KEY_PERMISSION_REQUESTED, false) && !canAskAgain) return
permissionState.edit().putBoolean(KEY_PERMISSION_REQUESTED, true).apply()
notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
} }
private companion object {
const val NOTIFICATION_PERMISSION_STATE = "notification_permission_state"
const val KEY_PERMISSION_REQUESTED = "permission_requested"
}
} }
@@ -121,23 +121,25 @@ class DefaultAutomationEngine(
if (conditionEvaluator.evaluate(scene.conditions, snapshot, now) !is ConditionsEvaluation.Satisfied) { if (conditionEvaluator.evaluate(scene.conditions, snapshot, now) !is ConditionsEvaluation.Satisfied) {
continue continue
} }
var executionCount = if (newlyMatchedStateTriggers.isEmpty()) 0 else 1
if (matchedTimeTriggers.isNotEmpty()) { if (matchedTimeTriggers.isNotEmpty()) {
var claimedTimeTrigger = false
for ((trigger, occurrenceKey) in matchedTimeTriggers) { for ((trigger, occurrenceKey) in matchedTimeTriggers) {
when (val claim = runtimeRepository.claimOccurrence(profileId, scene.id, trigger.id, occurrenceKey)) { when (val claim = runtimeRepository.claimOccurrence(profileId, scene.id, trigger.id, occurrenceKey)) {
is DomainResult.Failure -> return claim is DomainResult.Failure -> return claim
is DomainResult.Success -> if (claim.value) claimedTimeTrigger = true is DomainResult.Success -> if (claim.value) executionCount++
} }
} }
if (!claimedTimeTrigger && newlyMatchedStateTriggers.isEmpty()) continue
} }
if (executionCount == 0) continue
sceneExecutor.execute( repeat(executionCount) {
scene = scene, sceneExecutor.execute(
executionId = SceneExecutionId("auto-${UUID.randomUUID()}"), scene = scene,
source = SceneExecutionSource.AUTOMATION, executionId = SceneExecutionId("auto-${UUID.randomUUID()}"),
).collect { update -> source = SceneExecutionSource.AUTOMATION,
if (update is com.flagship.abox.manager.domain.SceneExecutionUpdate.Completed) executed++ ).collect { update ->
if (update is com.flagship.abox.manager.domain.SceneExecutionUpdate.Completed) executed++
}
} }
} }
return DomainResult.Success(executed) return DomainResult.Success(executed)
@@ -24,51 +24,56 @@ class WorkManagerAutomationScheduleStore(context: Context) : AutomationScheduleS
.setInputData(androidx.work.Data.Builder().putString(KEY_PROFILE_ID, profileId.value).build()) .setInputData(androidx.work.Data.Builder().putString(KEY_PROFILE_ID, profileId.value).build())
.build() .build()
workManager.enqueueUniquePeriodicWork(workName(profileId), ExistingPeriodicWorkPolicy.UPDATE, request) workManager.enqueueUniquePeriodicWork(workName(profileId), ExistingPeriodicWorkPolicy.UPDATE, request)
scheduleNextTimeTrigger(profileId, scenes) workManager.cancelAllWorkByTag(timeWorkTag(profileId))
scenes.forEach { scene ->
scene.triggers.filterIsInstance<SceneTrigger.Time>().forEach { trigger ->
scheduleNextTimeTrigger(profileId, scene, trigger)
}
}
return DomainResult.Success(Unit) return DomainResult.Success(Unit)
} }
override suspend fun remove(profileId: ProviderProfileId): DomainResult<Unit> { override suspend fun remove(profileId: ProviderProfileId): DomainResult<Unit> {
workManager.cancelUniqueWork(workName(profileId)) workManager.cancelUniqueWork(workName(profileId))
workManager.cancelUniqueWork(timeWorkName(profileId)) workManager.cancelAllWorkByTag(timeWorkTag(profileId))
return DomainResult.Success(Unit) return DomainResult.Success(Unit)
} }
private fun workName(profileId: ProviderProfileId) = "automation:${profileId.value}" private fun workName(profileId: ProviderProfileId) = "automation:${profileId.value}"
private fun timeWorkName(profileId: ProviderProfileId) = "automation-time:${profileId.value}" private fun timeWorkName(profileId: ProviderProfileId, scene: Scene, trigger: SceneTrigger.Time) =
"automation-time:${profileId.value}:${scene.id.value}:${trigger.id.value}"
private fun timeWorkTag(profileId: ProviderProfileId) = "automation-time:${profileId.value}"
private fun scheduleNextTimeTrigger(profileId: ProviderProfileId, scenes: List<Scene>) { private fun scheduleNextTimeTrigger(profileId: ProviderProfileId, scene: Scene, trigger: SceneTrigger.Time) {
val now = LocalDateTime.now() val now = LocalDateTime.now()
val next = scenes.flatMap { scene -> val scheduledAt = nextOccurrence(trigger, now) ?: return
scene.triggers.filterIsInstance<SceneTrigger.Time>().mapNotNull { trigger -> val delay = Duration.between(now.atZone(ZoneId.systemDefault()).toInstant(), scheduledAt.atZone(ZoneId.systemDefault()).toInstant())
nextOccurrence(trigger, now)?.let { occurrence -> ScheduledWorkOccurrence(scene, trigger, occurrence) } val scheduledAtInstant = scheduledAt.atZone(ZoneId.systemDefault()).toInstant()
}
}.minByOrNull { it.scheduledAt } ?: run {
workManager.cancelUniqueWork(timeWorkName(profileId))
return
}
val delay = Duration.between(now.atZone(ZoneId.systemDefault()).toInstant(), next.scheduledAt.atZone(ZoneId.systemDefault()).toInstant())
val scheduledAtInstant = next.scheduledAt.atZone(ZoneId.systemDefault()).toInstant()
val maximumExpiry = scheduledAtInstant.plus(MAXIMUM_OCCURRENCE_LATENESS) val maximumExpiry = scheduledAtInstant.plus(MAXIMUM_OCCURRENCE_LATENESS)
val followingOccurrence = nextOccurrence(next.trigger, next.scheduledAt) val followingOccurrence = nextOccurrence(trigger, scheduledAt)
?.atZone(ZoneId.systemDefault()) ?.atZone(ZoneId.systemDefault())
?.toInstant() ?.toInstant()
?.minusMillis(1) ?.minusMillis(1)
val expiresAt = listOfNotNull(maximumExpiry, followingOccurrence).minOrNull() ?: maximumExpiry val expiresAt = listOfNotNull(maximumExpiry, followingOccurrence).minOrNull() ?: maximumExpiry
val request = OneTimeWorkRequestBuilder<AutomationPollWorker>() val request = OneTimeWorkRequestBuilder<AutomationPollWorker>()
.setInitialDelay(delay.toMillis().coerceAtLeast(0), TimeUnit.MILLISECONDS) .setInitialDelay(delay.toMillis().coerceAtLeast(0), TimeUnit.MILLISECONDS)
.addTag(timeWorkTag(profileId))
.setInputData( .setInputData(
androidx.work.Data.Builder() androidx.work.Data.Builder()
.putString(KEY_PROFILE_ID, profileId.value) .putString(KEY_PROFILE_ID, profileId.value)
.putString(KEY_SCENE_ID, next.scene.id.value) .putString(KEY_SCENE_ID, scene.id.value)
.putString(KEY_TRIGGER_ID, next.trigger.id.value) .putString(KEY_TRIGGER_ID, trigger.id.value)
.putString(KEY_OCCURRENCE_KEY, "${next.scheduledAt.toLocalDate()}:${next.trigger.timeOfDay}:${next.trigger.id.value}") .putString(KEY_OCCURRENCE_KEY, "${scheduledAt.toLocalDate()}:${trigger.timeOfDay}:${trigger.id.value}")
.putLong(KEY_SCHEDULED_AT_EPOCH_MILLIS, scheduledAtInstant.toEpochMilli()) .putLong(KEY_SCHEDULED_AT_EPOCH_MILLIS, scheduledAtInstant.toEpochMilli())
.putLong(KEY_EXPIRES_AT_EPOCH_MILLIS, expiresAt.toEpochMilli()) .putLong(KEY_EXPIRES_AT_EPOCH_MILLIS, expiresAt.toEpochMilli())
.build(), .build(),
) )
.build() .build()
workManager.enqueueUniqueWork(timeWorkName(profileId), androidx.work.ExistingWorkPolicy.REPLACE, request) workManager.enqueueUniqueWork(
timeWorkName(profileId, scene, trigger),
androidx.work.ExistingWorkPolicy.REPLACE,
request,
)
} }
private fun nextOccurrence(trigger: SceneTrigger.Time, now: LocalDateTime): LocalDateTime? { private fun nextOccurrence(trigger: SceneTrigger.Time, now: LocalDateTime): LocalDateTime? {
@@ -82,12 +87,6 @@ class WorkManagerAutomationScheduleStore(context: Context) : AutomationScheduleS
return null return null
} }
private data class ScheduledWorkOccurrence(
val scene: Scene,
val trigger: SceneTrigger.Time,
val scheduledAt: LocalDateTime,
)
companion object { companion object {
const val KEY_PROFILE_ID = "profile_id" const val KEY_PROFILE_ID = "profile_id"
const val KEY_SCENE_ID = "scene_id" const val KEY_SCENE_ID = "scene_id"