feat(alarm): add true per-minute live countdown to pre-notice
Mirror the shipped snooze-countdown chain for the 30-min pre-notice notification: re-arm ACTION_PRE_NOTICE at each minute boundary via slot 9, self-stop at remaining<=1, self-heal from wall clock on missed ticks. Wire cancellation at all 5 sites (cancelAlarm, scheduleSpec no-trigger branch, snooze-transition branch, ACTION_SKIP_NEXT, ACTION_POSTPONE_NEXT) using AlarmScheduler's own requestCode formula to keep PendingIntent identity consistent.
This commit is contained in:
@@ -90,6 +90,7 @@ class AlarmScheduler(private val context: Context) {
|
|||||||
cancelPending("fire", pendingFireIntent(spec.id, PendingIntent.FLAG_NO_CREATE))
|
cancelPending("fire", pendingFireIntent(spec.id, PendingIntent.FLAG_NO_CREATE))
|
||||||
cancelPending("show", pendingShowIntent(spec.id, PendingIntent.FLAG_NO_CREATE))
|
cancelPending("show", pendingShowIntent(spec.id, PendingIntent.FLAG_NO_CREATE))
|
||||||
cancelPending("preNotice", pendingPreNoticeIntent(spec.id, PendingIntent.FLAG_NO_CREATE))
|
cancelPending("preNotice", pendingPreNoticeIntent(spec.id, PendingIntent.FLAG_NO_CREATE))
|
||||||
|
cancelPreNoticeCountdown(spec.id)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,6 +140,7 @@ class AlarmScheduler(private val context: Context) {
|
|||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
if (spec.snoozeUntilMillis != null) {
|
if (spec.snoozeUntilMillis != null) {
|
||||||
cancelPending("preNotice", pendingPreNoticeIntent(spec.id, PendingIntent.FLAG_NO_CREATE))
|
cancelPending("preNotice", pendingPreNoticeIntent(spec.id, PendingIntent.FLAG_NO_CREATE))
|
||||||
|
cancelPreNoticeCountdown(spec.id)
|
||||||
Log.d(tag, "alarm.schedule preNotice skipped for snooze id=${spec.id}")
|
Log.d(tag, "alarm.schedule preNotice skipped for snooze id=${spec.id}")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -471,6 +473,73 @@ class AlarmScheduler(private val context: Context) {
|
|||||||
cancelPending("snoozeCountdown", pending)
|
cancelPending("snoozeCountdown", pending)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-arms the pre-notice tick chain at the next minute boundary so the
|
||||||
|
* notification keeps live-updating every minute, mirroring
|
||||||
|
* [armNextSnoozeCountdownTick]. PUBLIC: called by the receiver after it
|
||||||
|
* posts/updates the pre-notice notification for [id]. Self-stops once
|
||||||
|
* [remaining] is down to the final minute, leaving the real fire alarm
|
||||||
|
* (setAlarmClock) as the sole next trigger.
|
||||||
|
*/
|
||||||
|
fun armNextPreNoticeCountdownTick(
|
||||||
|
id: String,
|
||||||
|
title: String,
|
||||||
|
snoozeMinutes: Int,
|
||||||
|
triggerAtMillis: Long,
|
||||||
|
occurrenceAtMillis: Long,
|
||||||
|
remaining: Long
|
||||||
|
) {
|
||||||
|
// On the final minute the real fire alarm takes over, so there is no
|
||||||
|
// further tick to schedule.
|
||||||
|
if (remaining <= 1L) return
|
||||||
|
val nextBoundary = triggerAtMillis - (remaining - 1L) * 60_000L
|
||||||
|
val pending = PendingIntent.getBroadcast(
|
||||||
|
appContext,
|
||||||
|
requestCode(id, 9),
|
||||||
|
Intent(appContext, PluriWaveAlarmReceiver::class.java).apply {
|
||||||
|
action = PluriWaveAlarmReceiver.ACTION_PRE_NOTICE
|
||||||
|
putExtra(PluriWaveAlarmReceiver.EXTRA_ALARM_ID, id)
|
||||||
|
putExtra(PluriWaveAlarmReceiver.EXTRA_ALARM_TITLE, title)
|
||||||
|
putExtra(PluriWaveAlarmReceiver.EXTRA_SNOOZE_MINUTES, snoozeMinutes)
|
||||||
|
putExtra(PluriWaveAlarmReceiver.EXTRA_TRIGGER_AT, triggerAtMillis)
|
||||||
|
putExtra(PluriWaveAlarmReceiver.EXTRA_OCCURRENCE_AT, occurrenceAtMillis)
|
||||||
|
},
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
try {
|
||||||
|
alarmManager.setExactAndAllowWhileIdle(
|
||||||
|
AlarmManager.RTC_WAKEUP,
|
||||||
|
nextBoundary,
|
||||||
|
pending
|
||||||
|
)
|
||||||
|
Log.d(tag, "alarm.preNoticeCountdown next tick id=$id at=$nextBoundary")
|
||||||
|
} catch (error: SecurityException) {
|
||||||
|
Log.w(tag, "alarm.preNoticeCountdown tick SecurityException id=$id", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancels the pending pre-notice tick alarm (slot 9) for [id], mirroring
|
||||||
|
* [cancelSnoozeCountdown]. PUBLIC: called both internally (cancelAlarm,
|
||||||
|
* scheduleSpec no-trigger branch, snooze-transition branch) and by the
|
||||||
|
* receiver (ACTION_SKIP_NEXT, ACTION_POSTPONE_NEXT) so every site that can
|
||||||
|
* end the pre-notice chain uses the SAME requestCode formula (31*hash+9)
|
||||||
|
* as [armNextPreNoticeCountdownTick] above. Using the receiver's own
|
||||||
|
* requestCode formula here would produce a different value and the
|
||||||
|
* PendingIntent cancel would silently fail, leaking a repeating alarm.
|
||||||
|
*/
|
||||||
|
fun cancelPreNoticeCountdown(id: String) {
|
||||||
|
val pending = PendingIntent.getBroadcast(
|
||||||
|
appContext,
|
||||||
|
requestCode(id, 9),
|
||||||
|
Intent(appContext, PluriWaveAlarmReceiver::class.java).apply {
|
||||||
|
action = PluriWaveAlarmReceiver.ACTION_PRE_NOTICE
|
||||||
|
},
|
||||||
|
PendingIntent.FLAG_NO_CREATE or PendingIntent.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
cancelPending("preNoticeCountdown", pending)
|
||||||
|
}
|
||||||
|
|
||||||
private fun postSnoozeCountdownNotification(spec: NativeAlarmSpec, remaining: Long) {
|
private fun postSnoozeCountdownNotification(spec: NativeAlarmSpec, remaining: Long) {
|
||||||
ensurePreNoticeChannel()
|
ensurePreNoticeChannel()
|
||||||
val text = AlarmNotificationStrings.snoozeCountdownText(appContext, remaining)
|
val text = AlarmNotificationStrings.snoozeCountdownText(appContext, remaining)
|
||||||
@@ -559,6 +628,7 @@ class AlarmScheduler(private val context: Context) {
|
|||||||
cancelPending("show", pendingShowIntent(id, PendingIntent.FLAG_NO_CREATE))
|
cancelPending("show", pendingShowIntent(id, PendingIntent.FLAG_NO_CREATE))
|
||||||
cancelPending("preNotice", pendingPreNoticeIntent(id, PendingIntent.FLAG_NO_CREATE))
|
cancelPending("preNotice", pendingPreNoticeIntent(id, PendingIntent.FLAG_NO_CREATE))
|
||||||
cancelSnoozeCountdown(id)
|
cancelSnoozeCountdown(id)
|
||||||
|
cancelPreNoticeCountdown(id)
|
||||||
NotificationManagerCompat.from(appContext).cancel(
|
NotificationManagerCompat.from(appContext).cancel(
|
||||||
PluriWaveAlarmReceiver.notificationIdForAlarm(id)
|
PluriWaveAlarmReceiver.notificationIdForAlarm(id)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ class PluriWaveAlarmReceiver : BroadcastReceiver() {
|
|||||||
}
|
}
|
||||||
ACTION_POSTPONE_NEXT -> {
|
ACTION_POSTPONE_NEXT -> {
|
||||||
NotificationManagerCompat.from(context).cancel(notificationIdForAlarm(alarmId))
|
NotificationManagerCompat.from(context).cancel(notificationIdForAlarm(alarmId))
|
||||||
|
AlarmScheduler(context).cancelPreNoticeCountdown(alarmId)
|
||||||
val occurrenceAt = AlarmScheduler(context).postponeNext(alarmId, snoozeMinutes)
|
val occurrenceAt = AlarmScheduler(context).postponeNext(alarmId, snoozeMinutes)
|
||||||
?: intent.getLongExtra(EXTRA_OCCURRENCE_AT, 0L)
|
?: intent.getLongExtra(EXTRA_OCCURRENCE_AT, 0L)
|
||||||
val launch = Intent(context, MainActivity::class.java).apply {
|
val launch = Intent(context, MainActivity::class.java).apply {
|
||||||
@@ -76,6 +77,7 @@ class PluriWaveAlarmReceiver : BroadcastReceiver() {
|
|||||||
}
|
}
|
||||||
ACTION_SKIP_NEXT -> {
|
ACTION_SKIP_NEXT -> {
|
||||||
NotificationManagerCompat.from(context).cancel(notificationIdForAlarm(alarmId))
|
NotificationManagerCompat.from(context).cancel(notificationIdForAlarm(alarmId))
|
||||||
|
AlarmScheduler(context).cancelPreNoticeCountdown(alarmId)
|
||||||
AlarmScheduler(context).skipNext(alarmId)
|
AlarmScheduler(context).skipNext(alarmId)
|
||||||
val launch = Intent(context, MainActivity::class.java).apply {
|
val launch = Intent(context, MainActivity::class.java).apply {
|
||||||
this.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
this.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||||
@@ -197,14 +199,30 @@ class PluriWaveAlarmReceiver : BroadcastReceiver() {
|
|||||||
} catch (error: SecurityException) {
|
} catch (error: SecurityException) {
|
||||||
Log.e(TAG, "alarm.notification preNotice SecurityException id=$alarmId", error)
|
Log.e(TAG, "alarm.notification preNotice SecurityException id=$alarmId", error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Re-arm the next minute tick so the countdown keeps live-updating
|
||||||
|
// until the real alarm fires. Reuses the SAME [remaining] computed
|
||||||
|
// above for the notification text to avoid a second clock read that
|
||||||
|
// could drift and cause an off-by-one between displayed text and the
|
||||||
|
// next-boundary math.
|
||||||
|
AlarmScheduler(context).armNextPreNoticeCountdownTick(
|
||||||
|
id = alarmId,
|
||||||
|
title = title,
|
||||||
|
snoozeMinutes = snoozeMinutes,
|
||||||
|
triggerAtMillis = triggerAtMillis,
|
||||||
|
occurrenceAtMillis = occurrenceAtMillis,
|
||||||
|
remaining = remaining
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Computes the number of minutes remaining until [triggerAtMillis],
|
* Computes the number of minutes remaining until [triggerAtMillis] using
|
||||||
* clamped to a minimum of 1. Handles Doze-delayed wakeups and clock drift.
|
* ceiling rounding (consistent with [AlarmScheduler]'s snooze-countdown
|
||||||
|
* ceilMinutes), clamped to a minimum of 1. Handles Doze-delayed wakeups
|
||||||
|
* and clock drift.
|
||||||
*/
|
*/
|
||||||
private fun computeRemainingMinutes(triggerAtMillis: Long): Long =
|
private fun computeRemainingMinutes(triggerAtMillis: Long): Long =
|
||||||
maxOf(1L, (triggerAtMillis - System.currentTimeMillis()) / 60_000L)
|
maxOf(1L, (triggerAtMillis - System.currentTimeMillis() + 59_999L) / 60_000L)
|
||||||
|
|
||||||
private fun ensureChannel(context: Context) {
|
private fun ensureChannel(context: Context) {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||||
|
|||||||
Reference in New Issue
Block a user