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:
@@ -34,9 +34,40 @@ class ServicioAlarmas {
|
||||
final DateTime Function() _reloj;
|
||||
final _uuid = const Uuid();
|
||||
|
||||
Future<ConfiguracionAlarmas> cargar() async {
|
||||
// In-memory cache + single-writer queue (Design 3.5 / S3-R7): every
|
||||
// mutation runs serialized through [_enCola] and reads [_cache], killing
|
||||
// the read-modify-write race the old cargar()-before-each-mutation had.
|
||||
ConfiguracionAlarmas? _cache;
|
||||
String? _cacheRaw;
|
||||
Future<void> _cola = Future<void>.value();
|
||||
|
||||
Future<T> _enCola<T>(Future<T> Function() accion) {
|
||||
final resultado = _cola.then((_) => accion());
|
||||
_cola = resultado.then((_) {}, onError: (_) {});
|
||||
return resultado;
|
||||
}
|
||||
|
||||
/// Re-reads from persistent storage (refreshing the cache) so writes done
|
||||
/// outside this service — e.g. a settings import that rewrites the raw
|
||||
/// key — are always picked up.
|
||||
Future<ConfiguracionAlarmas> cargar() => _enCola(() {
|
||||
_cache = null;
|
||||
_cacheRaw = null;
|
||||
return _configActual();
|
||||
});
|
||||
|
||||
Future<ConfiguracionAlarmas> _configActual() async {
|
||||
final existente = _cache;
|
||||
if (existente != null) return existente;
|
||||
final prefs = await _resolverPrefs();
|
||||
final raw = prefs.getString(_keyConfig);
|
||||
final config = _parsear(raw);
|
||||
_cache = config;
|
||||
_cacheRaw = raw;
|
||||
return config;
|
||||
}
|
||||
|
||||
ConfiguracionAlarmas _parsear(String? raw) {
|
||||
if (raw == null || raw.trim().isEmpty) {
|
||||
return const ConfiguracionAlarmas(
|
||||
alarmas: [],
|
||||
@@ -82,8 +113,8 @@ class ServicioAlarmas {
|
||||
AlarmaMusical alarma, {
|
||||
List<RangoVacaciones>? vacaciones,
|
||||
List<ExcepcionAlarma>? excepciones,
|
||||
}) async {
|
||||
final config = await cargar();
|
||||
}) => _enCola(() async {
|
||||
final config = await _configActual();
|
||||
final ahora = _reloj();
|
||||
final alarmas = List<AlarmaMusical>.from(config.alarmas);
|
||||
final index = alarmas.indexWhere((a) => a.id == alarma.id);
|
||||
@@ -105,10 +136,10 @@ class ServicioAlarmas {
|
||||
);
|
||||
await _guardar(nuevo);
|
||||
return nuevo;
|
||||
}
|
||||
});
|
||||
|
||||
Future<ConfiguracionAlarmas> eliminarAlarma(String id) async {
|
||||
final config = await cargar();
|
||||
Future<ConfiguracionAlarmas> eliminarAlarma(String id) => _enCola(() async {
|
||||
final config = await _configActual();
|
||||
final nuevo = ConfiguracionAlarmas(
|
||||
alarmas: config.alarmas.where((a) => a.id != id).toList(),
|
||||
vacaciones: config.vacaciones,
|
||||
@@ -116,12 +147,12 @@ class ServicioAlarmas {
|
||||
);
|
||||
await _guardar(nuevo);
|
||||
return nuevo;
|
||||
}
|
||||
});
|
||||
|
||||
Future<ConfiguracionAlarmas> guardarVacaciones(
|
||||
List<RangoVacaciones> vacaciones,
|
||||
) async {
|
||||
final config = await cargar();
|
||||
) => _enCola(() async {
|
||||
final config = await _configActual();
|
||||
final normalizadas =
|
||||
vacaciones.map((v) => v.normalizado()).toList()
|
||||
..sort((a, b) => a.inicioDia.compareTo(b.inicioDia));
|
||||
@@ -136,7 +167,7 @@ class ServicioAlarmas {
|
||||
);
|
||||
await _guardar(nuevo);
|
||||
return nuevo;
|
||||
}
|
||||
});
|
||||
|
||||
RangoVacaciones crearRangoVacaciones({
|
||||
required DateTime inicio,
|
||||
@@ -155,8 +186,8 @@ class ServicioAlarmas {
|
||||
return rango.normalizado();
|
||||
}
|
||||
|
||||
Future<ConfiguracionAlarmas> recalcularTodas() async {
|
||||
final config = await cargar();
|
||||
Future<ConfiguracionAlarmas> recalcularTodas() => _enCola(() async {
|
||||
final config = await _configActual();
|
||||
final alarmas =
|
||||
config.alarmas
|
||||
.map((a) => _recalcular(a, config.vacaciones, config.excepciones))
|
||||
@@ -166,16 +197,26 @@ class ServicioAlarmas {
|
||||
vacaciones: config.vacaciones,
|
||||
excepciones: config.excepciones,
|
||||
);
|
||||
await _guardar(nuevo);
|
||||
// Dirty-guard (S3-R5): this runs every minute from the refresh timer;
|
||||
// skip the SharedPreferences write when nothing actually changed.
|
||||
final nuevoRaw = _serializar(nuevo);
|
||||
final actualRaw = _cacheRaw ?? _serializar(config);
|
||||
if (nuevoRaw == actualRaw) return config;
|
||||
await _guardar(nuevo, raw: nuevoRaw);
|
||||
return nuevo;
|
||||
}
|
||||
});
|
||||
|
||||
Future<ConfiguracionAlarmas> sincronizarEjecucionesNativas(
|
||||
Map<String, DateTime> ejecuciones,
|
||||
) async {
|
||||
) {
|
||||
if (ejecuciones.isEmpty) return cargar();
|
||||
return _enCola(() => _sincronizarEjecucionesNativasInterno(ejecuciones));
|
||||
}
|
||||
|
||||
final config = await cargar();
|
||||
Future<ConfiguracionAlarmas> _sincronizarEjecucionesNativasInterno(
|
||||
Map<String, DateTime> ejecuciones,
|
||||
) async {
|
||||
final config = await _configActual();
|
||||
final ahora = _reloj();
|
||||
var huboCambios = false;
|
||||
final alarmas =
|
||||
@@ -225,33 +266,38 @@ class ServicioAlarmas {
|
||||
return nuevo;
|
||||
}
|
||||
|
||||
Future<ConfiguracionAlarmas> saltarProxima(String alarmaId) async {
|
||||
final config = await cargar();
|
||||
final alarma = config.alarmas.firstWhere((a) => a.id == alarmaId);
|
||||
final proxima = alarma.proximaEjecucion;
|
||||
if (proxima == null) return config;
|
||||
Future<ConfiguracionAlarmas> saltarProxima(String alarmaId) =>
|
||||
_enCola(() async {
|
||||
final config = await _configActual();
|
||||
final alarma = config.alarmas.firstWhere((a) => a.id == alarmaId);
|
||||
final proxima = alarma.proximaEjecucion;
|
||||
if (proxima == null) return config;
|
||||
|
||||
final excepciones = [
|
||||
...config.excepciones,
|
||||
ExcepcionAlarma(alarmaId: alarmaId, ejecucion: proxima, tipo: 'skipNext'),
|
||||
];
|
||||
final alarmas =
|
||||
config.alarmas
|
||||
.map(
|
||||
(a) =>
|
||||
a.id == alarmaId
|
||||
? _recalcular(a, config.vacaciones, excepciones)
|
||||
: a,
|
||||
)
|
||||
.toList();
|
||||
final nuevo = ConfiguracionAlarmas(
|
||||
alarmas: alarmas,
|
||||
vacaciones: config.vacaciones,
|
||||
excepciones: excepciones,
|
||||
);
|
||||
await _guardar(nuevo);
|
||||
return nuevo;
|
||||
}
|
||||
final excepciones = [
|
||||
...config.excepciones,
|
||||
ExcepcionAlarma(
|
||||
alarmaId: alarmaId,
|
||||
ejecucion: proxima,
|
||||
tipo: 'skipNext',
|
||||
),
|
||||
];
|
||||
final alarmas =
|
||||
config.alarmas
|
||||
.map(
|
||||
(a) =>
|
||||
a.id == alarmaId
|
||||
? _recalcular(a, config.vacaciones, excepciones)
|
||||
: a,
|
||||
)
|
||||
.toList();
|
||||
final nuevo = ConfiguracionAlarmas(
|
||||
alarmas: alarmas,
|
||||
vacaciones: config.vacaciones,
|
||||
excepciones: excepciones,
|
||||
);
|
||||
await _guardar(nuevo);
|
||||
return nuevo;
|
||||
});
|
||||
|
||||
Future<ConfiguracionAlarmas> posponerEjecucion(
|
||||
String alarmaId,
|
||||
@@ -276,8 +322,8 @@ class ServicioAlarmas {
|
||||
String alarmaId,
|
||||
DateTime ejecucion,
|
||||
DateTime snoozeHasta,
|
||||
) async {
|
||||
final config = await cargar();
|
||||
) => _enCola(() async {
|
||||
final config = await _configActual();
|
||||
final ahora = _reloj();
|
||||
final alarmas =
|
||||
config.alarmas
|
||||
@@ -300,13 +346,13 @@ class ServicioAlarmas {
|
||||
);
|
||||
await _guardar(nuevo);
|
||||
return nuevo;
|
||||
}
|
||||
});
|
||||
|
||||
Future<ConfiguracionAlarmas> completarEjecucion(
|
||||
String alarmaId,
|
||||
DateTime ejecucion,
|
||||
) async {
|
||||
final config = await cargar();
|
||||
) => _enCola(() async {
|
||||
final config = await _configActual();
|
||||
final ahora = _reloj();
|
||||
final alarmas =
|
||||
config.alarmas.map((a) {
|
||||
@@ -336,7 +382,7 @@ class ServicioAlarmas {
|
||||
);
|
||||
await _guardar(nuevo);
|
||||
return nuevo;
|
||||
}
|
||||
});
|
||||
|
||||
AlarmaMusical crearAlarma({
|
||||
required String nombre,
|
||||
@@ -372,18 +418,22 @@ class ServicioAlarmas {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _guardar(ConfiguracionAlarmas config) async {
|
||||
/// Persists [config] and refreshes the in-memory cache (the cache is
|
||||
/// "invalidated" by replacing it with the just-written state).
|
||||
Future<void> _guardar(ConfiguracionAlarmas config, {String? raw}) async {
|
||||
final serializado = raw ?? _serializar(config);
|
||||
final prefs = await _resolverPrefs();
|
||||
await prefs.setString(
|
||||
_keyConfig,
|
||||
jsonEncode({
|
||||
'alarmas': config.alarmas.map((a) => a.toJson()).toList(),
|
||||
'vacaciones': config.vacaciones.map((v) => v.toJson()).toList(),
|
||||
'excepciones': config.excepciones.map((e) => e.toJson()).toList(),
|
||||
}),
|
||||
);
|
||||
await prefs.setString(_keyConfig, serializado);
|
||||
_cache = config;
|
||||
_cacheRaw = serializado;
|
||||
}
|
||||
|
||||
String _serializar(ConfiguracionAlarmas config) => jsonEncode({
|
||||
'alarmas': config.alarmas.map((a) => a.toJson()).toList(),
|
||||
'vacaciones': config.vacaciones.map((v) => v.toJson()).toList(),
|
||||
'excepciones': config.excepciones.map((e) => e.toJson()).toList(),
|
||||
});
|
||||
|
||||
AlarmaMusical _recalcular(
|
||||
AlarmaMusical alarma,
|
||||
List<RangoVacaciones> vacaciones,
|
||||
|
||||
Reference in New Issue
Block a user