feat(audio): audio session integration and runtime robustness

- Integrate audio_session (new servicio_audio_session.dart): incoming calls pause the radio and resume on end, headphone unplug pauses without auto-resume, permanent focus loss never auto-resumes, duck lowers volume
- Add play-intent flag to ServicioAudio so interruption handling and future reconnect logic can distinguish user pause from system-driven stops
- Eliminate read-modify-write race in ServicioAlarmas with an in-memory cache and single-writer queue across all mutations; recalcularTodas persists only when state actually changed
- Convert ServicioAlarmasAndroid static StreamController/handler to injectable instance fields, restoring test isolation
- Inject a single cached SharedPreferences from main.dart across services and state (removes 23 inline getInstance() calls)
- Move configurarLocalizaciones out of MiniReproductor.build() (was running on every rebuild during playback)
- Bound the alarm fire-dedup set (cap 200 entries, 24h pruning)
- 12 new tests (89 total green), flutter analyze clean
This commit is contained in:
2026-06-11 16:25:09 +02:00
parent f3e9487215
commit 079e19f0ee
21 changed files with 1059 additions and 151 deletions
+22 -2
View File
@@ -13,14 +13,34 @@ import 'visualizador_audio.dart';
/// Barra inferior persistente con controles básicos de reproducción.
/// Toca la barra para abrir PantallaReproductor completa.
class MiniReproductor extends StatelessWidget {
class MiniReproductor extends StatefulWidget {
const MiniReproductor({super.key});
@override
State<MiniReproductor> createState() => _MiniReproductorState();
}
class _MiniReproductorState extends State<MiniReproductor> {
Locale? _localeConfigurado;
@override
void didChangeDependencies() {
super.didChangeDependencies();
// S3-R3: configure localizations once per locale change — never from
// build(), which re-runs on every playback notification.
final locale = Localizations.localeOf(context);
if (_localeConfigurado != locale) {
_localeConfigurado = locale;
context.read<EstadoRadio>().configurarLocalizaciones(
AppLocalizations.of(context),
);
}
}
@override
Widget build(BuildContext context) {
final estado = context.watch<EstadoRadio>();
final l10n = AppLocalizations.of(context);
estado.configurarLocalizaciones(l10n);
final emisora = estado.emisoraActual;
if (emisora == null) return const SizedBox.shrink();