fix(scene): harden delayed automation notifications

This commit is contained in:
2026-08-28 03:09:57 +08:00
parent 1c8a97e446
commit 8b92c84a27
5 changed files with 27 additions and 12 deletions
@@ -15,7 +15,13 @@ import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
private val notificationPermissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) {}
registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
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?) {
super.onCreate(savedInstanceState)
@@ -34,7 +40,8 @@ class MainActivity : ComponentActivity() {
}
val permissionState = getSharedPreferences(NOTIFICATION_PERMISSION_STATE, MODE_PRIVATE)
if (permissionState.getBoolean(KEY_PERMISSION_REQUESTED, false)) return
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)
@@ -14,6 +14,7 @@ import com.flagship.abox.manager.domain.DomainError
import com.flagship.abox.manager.domain.DomainResult
import com.flagship.abox.manager.domain.NotificationGateway
import com.flagship.abox.manager.domain.ProviderProfileId
import java.util.UUID
class AndroidNotificationGateway(private val context: Context) : NotificationGateway {
override suspend fun show(profileId: ProviderProfileId, title: String, message: String): DomainResult<Unit> {
@@ -40,7 +41,7 @@ class AndroidNotificationGateway(private val context: Context) : NotificationGat
@SuppressLint("MissingPermission")
fun postNotification() {
notificationManager.notify(
profileId.value.hashCode(),
UUID.randomUUID().hashCode(),
NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(android.R.drawable.ic_dialog_info)
.setContentTitle(title)
@@ -39,6 +39,7 @@ data class ScheduledAutomationOccurrence(
val triggerId: String,
val occurrenceKey: String,
val scheduledAtEpochMillis: Long,
val expiresAtEpochMillis: Long,
)
class DefaultAutomationEngine(
@@ -170,10 +171,4 @@ class DefaultAutomationEngine(
)
}
private val ScheduledAutomationOccurrence.expiresAtEpochMillis: Long
get() = scheduledAtEpochMillis + SCHEDULED_OCCURRENCE_GRACE_MILLIS
private companion object {
const val SCHEDULED_OCCURRENCE_GRACE_MILLIS = 20L * 60L * 1000L
}
}
@@ -38,8 +38,9 @@ class AutomationPollWorker @AssistedInject constructor(
val sceneId = inputData.getString(WorkManagerAutomationScheduleStore.KEY_SCENE_ID)
val triggerId = inputData.getString(WorkManagerAutomationScheduleStore.KEY_TRIGGER_ID)
val scheduledAt = inputData.getLong(WorkManagerAutomationScheduleStore.KEY_SCHEDULED_AT_EPOCH_MILLIS, -1L)
if (sceneId != null && triggerId != null && scheduledAt >= 0L) {
ScheduledAutomationOccurrence(sceneId, triggerId, key, scheduledAt)
val expiresAt = inputData.getLong(WorkManagerAutomationScheduleStore.KEY_EXPIRES_AT_EPOCH_MILLIS, -1L)
if (sceneId != null && triggerId != null && scheduledAt >= 0L && expiresAt >= scheduledAt) {
ScheduledAutomationOccurrence(sceneId, triggerId, key, scheduledAt, expiresAt)
} else null
}
when (automationEngine.evaluate(profileId, scheduledOccurrence)) {
@@ -48,6 +48,13 @@ class WorkManagerAutomationScheduleStore(context: Context) : AutomationScheduleS
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 followingOccurrence = nextOccurrence(next.trigger, next.scheduledAt)
?.atZone(ZoneId.systemDefault())
?.toInstant()
?.minusMillis(1)
val expiresAt = listOfNotNull(maximumExpiry, followingOccurrence).minOrNull() ?: maximumExpiry
val request = OneTimeWorkRequestBuilder<AutomationPollWorker>()
.setInitialDelay(delay.toMillis().coerceAtLeast(0), TimeUnit.MILLISECONDS)
.setInputData(
@@ -56,7 +63,8 @@ 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())
.putLong(KEY_SCHEDULED_AT_EPOCH_MILLIS, scheduledAtInstant.toEpochMilli())
.putLong(KEY_EXPIRES_AT_EPOCH_MILLIS, expiresAt.toEpochMilli())
.build(),
)
.build()
@@ -86,5 +94,8 @@ class WorkManagerAutomationScheduleStore(context: Context) : AutomationScheduleS
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"
const val KEY_EXPIRES_AT_EPOCH_MILLIS = "expires_at_epoch_millis"
val MAXIMUM_OCCURRENCE_LATENESS: Duration = Duration.ofHours(24)
}
}