From 7eaa87b462008a35cbae86241f81aa7a3485feb2 Mon Sep 17 00:00:00 2001 From: freetlab Date: Sat, 11 Jul 2026 11:08:32 +0200 Subject: [PATCH] fix(alarm): start the fade-in when pre-started audio is already playing The ringing screen only confirmed the native-to-Flutter handoff from its playback-state listener, but app.dart pre-starts the station before pushing the screen, so `reproduciendo` could be emitted before the listener subscribed and no further event ever arrived. That branch only cancelled the fallback timer: the gated fade-in never started and the native alarm player was never told to stop, so the alarm blared at the alarm-stream volume with no 5%-to-target ramp. Previously this was a timing race the stream usually lost; gating the ramp on the confirmation made the failure deterministic. The already-playing branch now confirms the handoff explicitly (idempotent with the listener), and the ramp re-imposes its 5% start volume immediately instead of waiting for the first periodic tick. Adds the regression test mounting in the real pre-started path. --- lib/pantallas/pantalla_alarma_sonando.dart | 11 +++++++++ .../pantalla_alarma_sonando_test.dart | 24 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/lib/pantallas/pantalla_alarma_sonando.dart b/lib/pantallas/pantalla_alarma_sonando.dart index a8667f9..2411803 100644 --- a/lib/pantallas/pantalla_alarma_sonando.dart +++ b/lib/pantallas/pantalla_alarma_sonando.dart @@ -88,8 +88,15 @@ class _PantallaAlarmaSonandoState extends State { _fallbackTimer = Timer(const Duration(seconds: 12), () { if (mounted) _iniciarFallback(); }); + // Pre-started audio can reach `reproduciendo` BEFORE the listener above + // subscribes (app.dart starts the station before pushing this screen), + // in which case no further state event ever arrives. The handoff + // confirmation and the fade-in must not depend on catching that + // already-missed event, so this branch confirms explicitly too + // (idempotent — the listener firing as well is harmless). if (widget.audioPrearrancado && radio.audio.estaSonando) { _fallbackTimer?.cancel(); + await _confirmarAudioFlutterListo(); } } @@ -111,6 +118,10 @@ class _PantallaAlarmaSonandoState extends State { unawaited(_aplicarVolumenGlobal(volumenObjetivo)); return; } + // Re-impose the ramp's start volume immediately: the first periodic tick + // only lands after _fadeStep, and by now the player may have been + // recreated or re-leveled since the pre-start set it to 5%. + unawaited(_aplicarVolumenGlobal(inicio)); final duracionTotalMs = segundosFade * 1000; final pasos = (duracionTotalMs / _fadeStep.inMilliseconds).ceil(); var pasoActual = 0; diff --git a/test/pantallas/pantalla_alarma_sonando_test.dart b/test/pantallas/pantalla_alarma_sonando_test.dart index 205945b..6945fb5 100644 --- a/test/pantallas/pantalla_alarma_sonando_test.dart +++ b/test/pantallas/pantalla_alarma_sonando_test.dart @@ -229,6 +229,30 @@ void main() { }); }); + group('handoff con audio prearrancado ya reproduciendo (regresion)', () { + testWidgets('si la radio ya esta reproduciendo al montar, la confirmacion ' + 'del handoff y el fade-in arrancan igual', (tester) async { + // Real production path: app.dart pre-starts the station BEFORE the + // screen mounts, so `reproduciendo` is emitted before the state + // listener subscribes and no further state event ever arrives. The + // handoff confirmation (native stop) and the Dart ramp must not + // depend on catching that already-missed event. + final entorno = await _montarPantalla(tester); + + expect( + entorno.android.detenidas, + contains('ring1'), + reason: 'el nativo debe recibir el stop del handoff aunque ' + '"reproduciendo" haya llegado antes del mount', + ); + expect( + entorno.audio.volumenesAplicados, + [0.05, 0.85], + reason: 'el fade-in debe arrancar tambien en el camino ya-sonando', + ); + }); + }); + group('restore de volumen de medios con dispose como unico llamador', () { testWidgets('desmontar la pantalla sin detener ni posponer restaura el ' 'volumen exactamente una vez', (tester) async {