fix(alarm): arm a just-passed occurrence instead of skipping it a day
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:
@@ -717,7 +717,12 @@ class AlarmScheduler(private val context: Context) {
|
|||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
spec.snoozeUntilMillis?.let { if (it > now) return it }
|
spec.snoozeUntilMillis?.let { if (it > now) return it }
|
||||||
if (!spec.enabled) return null
|
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) {
|
return when (spec.scheduleType) {
|
||||||
SCHEDULE_UNICA -> computeOneShot(spec, base)
|
SCHEDULE_UNICA -> computeOneShot(spec, base)
|
||||||
SCHEDULE_DIAS_SEMANA -> computeWeekday(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 PRE_NOTICE_MILLIS = 30 * 60 * 1000L
|
||||||
private const val SCHEDULE_UNICA = "unica"
|
private const val SCHEDULE_UNICA = "unica"
|
||||||
private const val SCHEDULE_DIAS_SEMANA = "diasSemana"
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -167,6 +167,50 @@ void main() {
|
|||||||
expect(proxima, DateTime(2026, 5, 26, 7, 30));
|
expect(proxima, DateTime(2026, 5, 26, 7, 30));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('mantiene la ocurrencia diaria cuyo disparo acaba de pasar', () {
|
||||||
|
// Contract the native AlarmScheduler.computeNextTriggerMillis MUST
|
||||||
|
// mirror: a trigger that just passed within toleranciaDisparoInminente
|
||||||
|
// (90s) and has NOT been handled is kept (normalized to desde + 2s),
|
||||||
|
// never skipped to tomorrow. The native side lacking this window is
|
||||||
|
// what recomputed the next occurrence as tomorrow and cancelled the
|
||||||
|
// in-flight fire, so the alarm never rang until the screen was turned
|
||||||
|
// on. If this expectation changes, the native constant
|
||||||
|
// IMMINENT_TOLERANCE_MILLIS must change with it.
|
||||||
|
final alarma = AlarmaMusical(
|
||||||
|
id: 'a-inminente-diaria',
|
||||||
|
nombre: 'Diaria inminente',
|
||||||
|
hora: 22,
|
||||||
|
minuto: 35,
|
||||||
|
tipoProgramacion: TipoProgramacionAlarma.diaria,
|
||||||
|
diasSemana: const [],
|
||||||
|
);
|
||||||
|
|
||||||
|
final proxima = servicio.calcularProxima(
|
||||||
|
alarma: alarma,
|
||||||
|
desde: DateTime(2026, 7, 11, 22, 35, 30),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(proxima, DateTime(2026, 7, 11, 22, 35, 32));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('mantiene la ocurrencia por dias de semana recien pasada', () {
|
||||||
|
final alarma = AlarmaMusical(
|
||||||
|
id: 'a-inminente-semana',
|
||||||
|
nombre: 'Semana inminente',
|
||||||
|
hora: 22,
|
||||||
|
minuto: 35,
|
||||||
|
tipoProgramacion: TipoProgramacionAlarma.diasSemana,
|
||||||
|
diasSemana: const [1, 2, 3, 4, 5, 6, 7],
|
||||||
|
);
|
||||||
|
|
||||||
|
final proxima = servicio.calcularProxima(
|
||||||
|
alarma: alarma,
|
||||||
|
desde: DateTime(2026, 7, 11, 22, 35, 30),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(proxima, DateTime(2026, 7, 11, 22, 35, 32));
|
||||||
|
});
|
||||||
|
|
||||||
test('calcula siguiente por dias de semana despues de ejecucion', () {
|
test('calcula siguiente por dias de semana despues de ejecucion', () {
|
||||||
final alarma = AlarmaMusical(
|
final alarma = AlarmaMusical(
|
||||||
id: 'a7',
|
id: 'a7',
|
||||||
|
|||||||
Reference in New Issue
Block a user