From 8b92c84a27d4f6b27983d099d732732839dc985e Mon Sep 17 00:00:00 2001 From: KremeCN <3265494849@qq.com> Date: Fri, 28 Aug 2026 03:09:57 +0800 Subject: [PATCH] fix(scene): harden delayed automation notifications --- .../com/flagship/abox/manager/MainActivity.kt | 11 +++++++++-- .../notification/AndroidNotificationGateway.kt | 3 ++- .../manager/automation/runtime/AutomationEngine.kt | 7 +------ .../automation/runtime/AutomationPollWorker.kt | 5 +++-- .../runtime/WorkManagerAutomationScheduleStore.kt | 13 ++++++++++++- 5 files changed, 27 insertions(+), 12 deletions(-) diff --git a/android/app/src/main/kotlin/com/flagship/abox/manager/MainActivity.kt b/android/app/src/main/kotlin/com/flagship/abox/manager/MainActivity.kt index 2ec1705..edecb33 100644 --- a/android/app/src/main/kotlin/com/flagship/abox/manager/MainActivity.kt +++ b/android/app/src/main/kotlin/com/flagship/abox/manager/MainActivity.kt @@ -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) diff --git a/android/feature/automation/src/main/kotlin/com/flagship/abox/manager/automation/notification/AndroidNotificationGateway.kt b/android/feature/automation/src/main/kotlin/com/flagship/abox/manager/automation/notification/AndroidNotificationGateway.kt index 58fc23f..58443cd 100644 --- a/android/feature/automation/src/main/kotlin/com/flagship/abox/manager/automation/notification/AndroidNotificationGateway.kt +++ b/android/feature/automation/src/main/kotlin/com/flagship/abox/manager/automation/notification/AndroidNotificationGateway.kt @@ -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 { @@ -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) 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 fe676f1..d7a3e3c 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 @@ -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 - } } 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 e29aa51..5d10e42 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 @@ -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)) { 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 35a8994..5953027 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 @@ -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() .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) } }