fix(alarm): arm a just-passed occurrence instead of skipping it a day
Build & Deploy PluriWave / Análisis de código (push) Successful in 35s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 1m42s

The native next-occurrence recompute required the trigger to be
strictly in the future, while the Dart side keeps an occurrence whose
trigger passed within a 90s tolerance. When the periodic resync
re-armed an alarm microseconds after its trigger (app foregrounded,
the 60s tick straddling the trigger instant), computeNextTriggerMillis
recomputed the next weekday/daily occurrence as tomorrow and, through
the shared FLAG_UPDATE_CURRENT fire PendingIntent, replaced the
in-flight fire before AlarmManager delivered it. The alarm never rang
until the screen was turned on and the Dart watchdog caught it late.

computeNextTriggerMillis now mirrors Dart's toleranciaDisparoInminente:
base is lowered by a 90s grace window so a just-passed occurrence is
armed (and delivered ~immediately) rather than pushed to the next day.
The handledFloor (lastHandledAtMillis + 60s) stays a hard lower bound,
so an already-fired occurrence can never be re-selected — no
double-fire. Dart contract tests lock the boundary the native constant
must track. Native verification is on-device (no JVM test harness).
This commit is contained in:
2026-07-11 23:02:14 +02:00
parent 812922d7f1
commit 3ebb41aa9d
2 changed files with 59 additions and 1 deletions
@@ -717,7 +717,12 @@ class AlarmScheduler(private val context: Context) {
val now = System.currentTimeMillis()
spec.snoozeUntilMillis?.let { if (it > now) return it }
if (!spec.enabled) return null
val base = maxOf(now, (spec.lastHandledAtMillis ?: 0L) + 60_000L)
// handledFloor is a HARD lower bound: an occurrence already fired
// (onAlarmFired records lastHandledAtMillis) can never be re-selected,
// so the grace window below can lower `base` toward the past without
// ever risking a double-fire.
val handledFloor = (spec.lastHandledAtMillis ?: 0L) + 60_000L
val base = maxOf(now - IMMINENT_TOLERANCE_MILLIS, handledFloor)
return when (spec.scheduleType) {
SCHEDULE_UNICA -> computeOneShot(spec, base)
SCHEDULE_DIAS_SEMANA -> computeWeekday(spec, base)
@@ -1042,6 +1047,15 @@ class AlarmScheduler(private val context: Context) {
private const val PRE_NOTICE_MILLIS = 30 * 60 * 1000L
private const val SCHEDULE_UNICA = "unica"
private const val SCHEDULE_DIAS_SEMANA = "diasSemana"
// Mirror Dart's ServicioProgramacionAlarmas.toleranciaDisparoInminente
// (90s): an occurrence whose trigger just passed within this window
// must still be armed (and delivered ~immediately) instead of being
// skipped to the next day. Without it, a re-arm landing microseconds
// after the trigger (e.g. the periodic resync straddling the trigger
// instant while the app is foregrounded) recomputes the next
// occurrence as tomorrow and cancels the in-flight fire.
private const val IMMINENT_TOLERANCE_MILLIS = 90_000L
}
}