fix(scene): harden delayed automation notifications
This commit is contained in:
@@ -15,7 +15,13 @@ import dagger.hilt.android.AndroidEntryPoint
|
|||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
private val notificationPermissionLauncher =
|
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?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -34,7 +40,8 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val permissionState = getSharedPreferences(NOTIFICATION_PERMISSION_STATE, MODE_PRIVATE)
|
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()
|
permissionState.edit().putBoolean(KEY_PERMISSION_REQUESTED, true).apply()
|
||||||
notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||||
|
|||||||
+2
-1
@@ -14,6 +14,7 @@ import com.flagship.abox.manager.domain.DomainError
|
|||||||
import com.flagship.abox.manager.domain.DomainResult
|
import com.flagship.abox.manager.domain.DomainResult
|
||||||
import com.flagship.abox.manager.domain.NotificationGateway
|
import com.flagship.abox.manager.domain.NotificationGateway
|
||||||
import com.flagship.abox.manager.domain.ProviderProfileId
|
import com.flagship.abox.manager.domain.ProviderProfileId
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
class AndroidNotificationGateway(private val context: Context) : NotificationGateway {
|
class AndroidNotificationGateway(private val context: Context) : NotificationGateway {
|
||||||
override suspend fun show(profileId: ProviderProfileId, title: String, message: String): DomainResult<Unit> {
|
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")
|
@SuppressLint("MissingPermission")
|
||||||
fun postNotification() {
|
fun postNotification() {
|
||||||
notificationManager.notify(
|
notificationManager.notify(
|
||||||
profileId.value.hashCode(),
|
UUID.randomUUID().hashCode(),
|
||||||
NotificationCompat.Builder(context, CHANNEL_ID)
|
NotificationCompat.Builder(context, CHANNEL_ID)
|
||||||
.setSmallIcon(android.R.drawable.ic_dialog_info)
|
.setSmallIcon(android.R.drawable.ic_dialog_info)
|
||||||
.setContentTitle(title)
|
.setContentTitle(title)
|
||||||
|
|||||||
+1
-6
@@ -39,6 +39,7 @@ data class ScheduledAutomationOccurrence(
|
|||||||
val triggerId: String,
|
val triggerId: String,
|
||||||
val occurrenceKey: String,
|
val occurrenceKey: String,
|
||||||
val scheduledAtEpochMillis: Long,
|
val scheduledAtEpochMillis: Long,
|
||||||
|
val expiresAtEpochMillis: Long,
|
||||||
)
|
)
|
||||||
|
|
||||||
class DefaultAutomationEngine(
|
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-2
@@ -38,8 +38,9 @@ class AutomationPollWorker @AssistedInject constructor(
|
|||||||
val sceneId = inputData.getString(WorkManagerAutomationScheduleStore.KEY_SCENE_ID)
|
val sceneId = inputData.getString(WorkManagerAutomationScheduleStore.KEY_SCENE_ID)
|
||||||
val triggerId = inputData.getString(WorkManagerAutomationScheduleStore.KEY_TRIGGER_ID)
|
val triggerId = inputData.getString(WorkManagerAutomationScheduleStore.KEY_TRIGGER_ID)
|
||||||
val scheduledAt = inputData.getLong(WorkManagerAutomationScheduleStore.KEY_SCHEDULED_AT_EPOCH_MILLIS, -1L)
|
val scheduledAt = inputData.getLong(WorkManagerAutomationScheduleStore.KEY_SCHEDULED_AT_EPOCH_MILLIS, -1L)
|
||||||
if (sceneId != null && triggerId != null && scheduledAt >= 0L) {
|
val expiresAt = inputData.getLong(WorkManagerAutomationScheduleStore.KEY_EXPIRES_AT_EPOCH_MILLIS, -1L)
|
||||||
ScheduledAutomationOccurrence(sceneId, triggerId, key, scheduledAt)
|
if (sceneId != null && triggerId != null && scheduledAt >= 0L && expiresAt >= scheduledAt) {
|
||||||
|
ScheduledAutomationOccurrence(sceneId, triggerId, key, scheduledAt, expiresAt)
|
||||||
} else null
|
} else null
|
||||||
}
|
}
|
||||||
when (automationEngine.evaluate(profileId, scheduledOccurrence)) {
|
when (automationEngine.evaluate(profileId, scheduledOccurrence)) {
|
||||||
|
|||||||
+12
-1
@@ -48,6 +48,13 @@ class WorkManagerAutomationScheduleStore(context: Context) : AutomationScheduleS
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
val delay = Duration.between(now.atZone(ZoneId.systemDefault()).toInstant(), next.scheduledAt.atZone(ZoneId.systemDefault()).toInstant())
|
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>()
|
val request = OneTimeWorkRequestBuilder<AutomationPollWorker>()
|
||||||
.setInitialDelay(delay.toMillis().coerceAtLeast(0), TimeUnit.MILLISECONDS)
|
.setInitialDelay(delay.toMillis().coerceAtLeast(0), TimeUnit.MILLISECONDS)
|
||||||
.setInputData(
|
.setInputData(
|
||||||
@@ -56,7 +63,8 @@ class WorkManagerAutomationScheduleStore(context: Context) : AutomationScheduleS
|
|||||||
.putString(KEY_SCENE_ID, next.scene.id.value)
|
.putString(KEY_SCENE_ID, next.scene.id.value)
|
||||||
.putString(KEY_TRIGGER_ID, next.trigger.id.value)
|
.putString(KEY_TRIGGER_ID, next.trigger.id.value)
|
||||||
.putString(KEY_OCCURRENCE_KEY, "${next.scheduledAt.toLocalDate()}:${next.trigger.timeOfDay}:${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(),
|
||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
@@ -86,5 +94,8 @@ class WorkManagerAutomationScheduleStore(context: Context) : AutomationScheduleS
|
|||||||
const val KEY_TRIGGER_ID = "trigger_id"
|
const val KEY_TRIGGER_ID = "trigger_id"
|
||||||
const val KEY_OCCURRENCE_KEY = "occurrence_key"
|
const val KEY_OCCURRENCE_KEY = "occurrence_key"
|
||||||
const val KEY_SCHEDULED_AT_EPOCH_MILLIS = "scheduled_at_epoch_millis"
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user