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
+21 -4
View File
@@ -1,6 +1,7 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'estado/estado_radio.dart';
import 'estado/estado_alarmas.dart';
import 'estado/estado_idioma.dart';
@@ -23,15 +24,21 @@ import 'package:pluriwave/widgets/mini_reproductor.dart';
import 'servicios/servicio_alarmas_android.dart';
class PluriWaveApp extends StatelessWidget {
const PluriWaveApp({super.key});
const PluriWaveApp({super.key, this.prefs});
/// Single SharedPreferences instance resolved in main() (S3-R4) and
/// injected into every state/service.
final SharedPreferences? prefs;
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => EstadoRadio()),
ChangeNotifierProvider(create: (_) => EstadoAlarmas()),
ChangeNotifierProvider(create: (_) => EstadoIdioma()),
ChangeNotifierProvider(create: (_) => EstadoRadio(prefs: prefs)),
ChangeNotifierProvider(create: (_) => EstadoAlarmas(prefs: prefs)),
ChangeNotifierProvider(
create: (_) => EstadoIdioma(sharedPreferences: prefs),
),
],
child: Consumer<EstadoIdioma>(
builder:
@@ -69,6 +76,7 @@ class _PaginaPrincipalState extends State<_PaginaPrincipal> {
bool _alarmaSonandoActiva = false;
bool _onboardingInicialSolicitado = false;
String? _alarmaSonandoId;
Locale? _localeAlarmasConfigurado;
static const _paginas = [
PantallaInicio(),
@@ -89,6 +97,15 @@ class _PaginaPrincipalState extends State<_PaginaPrincipal> {
@override
void didChangeDependencies() {
super.didChangeDependencies();
// S3-R3 / Decision 3.2: keep the alarm bridge l10n in sync, once per
// locale change (this hook re-runs when Localizations changes).
final locale = Localizations.localeOf(context);
if (_localeAlarmasConfigurado != locale) {
_localeAlarmasConfigurado = locale;
context.read<EstadoAlarmas>().configurarLocalizaciones(
AppLocalizations.of(context),
);
}
final estado = context.read<EstadoRadio>();
if (identical(_estadoSuscrito, estado) && _errorSubscription != null) {
return;