fix(alarm): unfreeze snooze modal and add per-minute snooze countdown
The snooze button was fire-and-forget without error handling: if
posponerAlarma threw (e.g. native scheduleAlarm returns false on a
device without exact-alarm permission), _dismissScreen never ran. The
stuck modal also kept _alarmaSonandoActiva true, which made the next
ring get ignored. _posponer/_detener now dismiss in a finally and stop
audio defensively.
Add a native, AlarmManager-driven snooze countdown notification that
re-posts every minute ("Rings in N min", 3->2->1) while the engine is
dead. scheduleSpec drives scheduleSnoozeCountdown for snoozes (instead
of the 30-min pre-notice). Localized text and button labels travel
Dart->Kotlin as {minutes} templates, same pattern as preNoticeTemplate.
Notification actions: "snooze again" (snoozeAgain, anchored to now)
reports back via the existing snoozed event; "stop" (cancelSnooze)
records a handled occurrence for cold-start reconciliation and emits a
new snoozeCancelled event handled in EstadoAlarmas.
Adds snoozeCountdown/snoozeAgainAction keys across all 13 locales and a
test for the snoozeCancelled event. Kotlin changes are static-reviewed
only; no Android build environment available here.
This commit is contained in:
@@ -154,10 +154,17 @@ class _PantallaAlarmaSonandoState extends State<PantallaAlarmaSonando> {
|
||||
Future<void> _detener() async {
|
||||
final radio = context.read<EstadoRadio>();
|
||||
final alarmas = context.read<EstadoAlarmas>();
|
||||
await _liberarAudioLocal();
|
||||
await radio.audio.pausar();
|
||||
await alarmas.finalizarEjecucion(widget.alarma.id);
|
||||
if (mounted) _dismissScreen();
|
||||
await _silenciarAudio(radio);
|
||||
// Dismiss is run from finally so a failing reschedule/teardown can never
|
||||
// leave the ringing screen stuck open (which would also block the next
|
||||
// ring via the _alarmaSonandoActiva guard in app.dart).
|
||||
try {
|
||||
await alarmas.finalizarEjecucion(widget.alarma.id);
|
||||
} catch (e) {
|
||||
debugPrint('[PluriWave][alarmas] finalizar ejecucion fallo: $e');
|
||||
} finally {
|
||||
if (mounted) _dismissScreen();
|
||||
}
|
||||
}
|
||||
|
||||
/// Flutter-first snooze (S2-R1): tears down local audio, then routes
|
||||
@@ -166,10 +173,33 @@ class _PantallaAlarmaSonandoState extends State<PantallaAlarmaSonando> {
|
||||
Future<void> _posponer(int minutos) async {
|
||||
final radio = context.read<EstadoRadio>();
|
||||
final alarmas = context.read<EstadoAlarmas>();
|
||||
await _liberarAudioLocal();
|
||||
await radio.audio.pausar();
|
||||
await alarmas.posponerAlarma(widget.alarma, minutos);
|
||||
if (mounted) _dismissScreen();
|
||||
await _silenciarAudio(radio);
|
||||
// See _detener: the screen MUST close even if posponerAlarma throws
|
||||
// (e.g. native scheduleAlarm returns false on a device without exact
|
||||
// alarm permission). Otherwise the modal freezes and the snooze never
|
||||
// re-rings because _alarmaSonandoActiva stays true.
|
||||
try {
|
||||
await alarmas.posponerAlarma(widget.alarma, minutos);
|
||||
} catch (e) {
|
||||
debugPrint('[PluriWave][alarmas] posponer alarma fallo: $e');
|
||||
} finally {
|
||||
if (mounted) _dismissScreen();
|
||||
}
|
||||
}
|
||||
|
||||
/// Stops both audio sources (local fallback player and the radio handler)
|
||||
/// without letting either failure abort the caller's dismiss flow.
|
||||
Future<void> _silenciarAudio(EstadoRadio radio) async {
|
||||
try {
|
||||
await _liberarAudioLocal();
|
||||
} catch (e) {
|
||||
debugPrint('[PluriWave][alarmas] liberar audio local fallo: $e');
|
||||
}
|
||||
try {
|
||||
await radio.audio.pausar();
|
||||
} catch (e) {
|
||||
debugPrint('[PluriWave][alarmas] pausar radio fallo: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Dismisses the alarm screen safely in both live-app and dead-app states.
|
||||
|
||||
Reference in New Issue
Block a user