fix(alarm): scope native stop to the ringing id and intercept system back
Two exit-path holes found by adversarial review before the next build: PluriWaveAlarmService.stopAlarm() never compared the requested id to activeAlarmId, so any stop request for a DIFFERENT alarm tore down whichever ring was active: with two alarms firing close together, the second one's routine hide-notification call (via dismissAlarmNotification -> ACTION_STOP) killed the first alarm mid-ring and prematurely restored the device volume override. A mismatched id now only cancels that id's notification and returns; null keeps full-teardown semantics for internal/onDestroy callers. The ringing screen never intercepted the system back gesture: a plain route pop ran only dispose(), leaving the shared radio player ringing with no alarm UI left anywhere to stop it. Back now routes through PopScope into the same _detener() flow as the Stop button, guarded by a single-exit flag so a back-press racing a button tap cannot run the teardown twice and pop the route underneath. Also resets the shared handler gain to 1.0 on ring exit: the fade-in mutates the radio player's persistent volume, and exiting mid-ramp used to leave every later radio play at the partial ramp level.
This commit is contained in:
@@ -13,6 +13,7 @@ import '../modelos/alarma_musical.dart';
|
||||
import '../servicios/servicio_audio.dart';
|
||||
import '../tema/pluri_animate.dart';
|
||||
import '../tema/pluriwave_theme.dart';
|
||||
import '../tema/pluriwave_tokens.dart';
|
||||
import '../widgets/pluri_glass_surface.dart';
|
||||
import '../widgets/pluri_wave_scaffold.dart';
|
||||
|
||||
@@ -205,7 +206,15 @@ class _PantallaAlarmaSonandoState extends State<PantallaAlarmaSonando> {
|
||||
await _fallbackPlayer.stop();
|
||||
}
|
||||
|
||||
/// Single-exit guard: Stop, snooze and the system back gesture all funnel
|
||||
/// into the same teardown; whichever lands first wins and the rest no-op,
|
||||
/// so a back-press racing a button tap can never run the exit flow twice
|
||||
/// (a second _dismissScreen would pop the route UNDER the alarm screen).
|
||||
bool _salidaEnCurso = false;
|
||||
|
||||
Future<void> _detener() async {
|
||||
if (_salidaEnCurso) return;
|
||||
_salidaEnCurso = true;
|
||||
final radio = context.read<EstadoRadio>();
|
||||
final alarmas = context.read<EstadoAlarmas>();
|
||||
await _silenciarAudio(radio);
|
||||
@@ -225,6 +234,8 @@ class _PantallaAlarmaSonandoState extends State<PantallaAlarmaSonando> {
|
||||
/// through the canonical EstadoAlarmas.posponerAlarma, which hides the
|
||||
/// native notification (same stop path as dismiss) and re-programs Android.
|
||||
Future<void> _posponer(int minutos) async {
|
||||
if (_salidaEnCurso) return;
|
||||
_salidaEnCurso = true;
|
||||
final radio = context.read<EstadoRadio>();
|
||||
final alarmas = context.read<EstadoAlarmas>();
|
||||
// Captured BEFORE dismiss (Design D4): the ringing screen dismisses by
|
||||
@@ -264,6 +275,15 @@ class _PantallaAlarmaSonandoState extends State<PantallaAlarmaSonando> {
|
||||
} catch (e) {
|
||||
debugPrint('[PluriWave][alarmas] pausar radio fallo: $e');
|
||||
}
|
||||
try {
|
||||
// The fade-in mutates the SHARED radio handler gain; exiting mid-ramp
|
||||
// would otherwise leave every later radio play at the partial ramp
|
||||
// level until the next full ramp or app restart. 1.0 is the handler's
|
||||
// default gain; the player is already paused, so this is inaudible.
|
||||
await radio.audio.setVolumen(1.0);
|
||||
} catch (e) {
|
||||
debugPrint('[PluriWave][alarmas] restaurar ganancia radio fallo: $e');
|
||||
}
|
||||
await _restaurarVolumenMediaUnaVez();
|
||||
}
|
||||
|
||||
@@ -308,6 +328,25 @@ class _PantallaAlarmaSonandoState extends State<PantallaAlarmaSonando> {
|
||||
// the first frame after a screen-off FSI wake can stutter. The blur sigma
|
||||
// is capped here, and reduced-motion users skip the entry animation
|
||||
// entirely via pluriFadeIn.
|
||||
return PopScope(
|
||||
// System back / predictive back must behave exactly like Stop: a plain
|
||||
// route pop would run only dispose(), leaving the shared radio player
|
||||
// ringing with no alarm UI left anywhere to stop it.
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (didPop, _) {
|
||||
if (didPop) return;
|
||||
unawaited(_detener());
|
||||
},
|
||||
child: _cuerpo(context, alarma, l10n, tokens),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _cuerpo(
|
||||
BuildContext context,
|
||||
AlarmaMusical alarma,
|
||||
AppLocalizations l10n,
|
||||
PluriWaveTokens tokens,
|
||||
) {
|
||||
return PluriWaveScaffold(
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
|
||||
Reference in New Issue
Block a user