fix(scene): schedule time triggers independently
This commit is contained in:
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-3
@@ -121,17 +121,18 @@ 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
|
||||||
|
|
||||||
|
repeat(executionCount) {
|
||||||
sceneExecutor.execute(
|
sceneExecutor.execute(
|
||||||
scene = scene,
|
scene = scene,
|
||||||
executionId = SceneExecutionId("auto-${UUID.randomUUID()}"),
|
executionId = SceneExecutionId("auto-${UUID.randomUUID()}"),
|
||||||
@@ -140,6 +141,7 @@ class DefaultAutomationEngine(
|
|||||||
if (update is com.flagship.abox.manager.domain.SceneExecutionUpdate.Completed) executed++
|
if (update is com.flagship.abox.manager.domain.SceneExecutionUpdate.Completed) executed++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return DomainResult.Success(executed)
|
return DomainResult.Success(executed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+24
-25
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user