Files
pluriwave/lib/servicios/servicio_ecualizador.dart
T
FreeTLab 45b7fc8741
Build & Deploy PluriWave / Análisis de código (push) Successful in 36s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 1m39s
fix(eq): keep valid presets when stored maps are partially corrupt
Convert the 4 EQ persistence readers (device presets, matrix presets,
device names, per-station presets) to per-entry tolerant parsing via
the shared persistencia_tolerante helper, so one corrupt entry no
longer discards every sibling preset. The principal-preset reader
gains diagnostic logging on its existing fallback path. No degraded
flag or quarantine here (unlike alarms/stations) since EQ writes are
explicit-only and presets are trivially re-creatable.
2026-07-11 13:00:15 +02:00

375 lines
13 KiB
Dart

import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import '../modelos/preset_ecualizador.dart';
import 'persistencia_tolerante.dart';
class ConfiguracionEcualizador {
const ConfiguracionEcualizador({
required this.principal,
required this.porEmisora,
this.activo = true,
this.eqMultiDeviceEnabled = false,
this.presetsDispositivo = const {},
this.presetsMatriz = const {},
this.nombresDispositivos = const {},
});
final PresetEcualizador principal;
final Map<String, PresetEcualizador> porEmisora;
final bool activo;
/// Feature toggle: when false all multi-device logic is bypassed.
final bool eqMultiDeviceEnabled;
/// Per-device presets: deviceId → PresetEcualizador.
final Map<String, PresetEcualizador> presetsDispositivo;
/// Matrix presets: "stationUuid:deviceId" → PresetEcualizador.
final Map<String, PresetEcualizador> presetsMatriz;
/// Custom display names for devices: deviceId → custom name.
final Map<String, String> nombresDispositivos;
}
class ServicioEcualizador {
ServicioEcualizador({SharedPreferences? prefs}) : _prefs = prefs;
static const _keyPresetPrincipal = 'eq_preset_principal_v1';
static const _keyPresetsPorEmisora = 'eq_presets_por_emisora_v1';
static const _keyActivo = 'eq_activo_v1';
static const _keyMultiDeviceEnabled = 'eq_multi_device_enabled_v1';
static const _keyPresetsPorDispositivo = 'eq_preset_por_dispositivo_v1';
static const _keyPresetsMatriz = 'eq_presets_matriz_v1';
static const _keyNombresDispositivos = 'eq_nombres_dispositivos_v1';
/// bt-device-identity ADR-5: guard flag for the one-time placeholder purge.
static const _keyPlaceholderPurgaHecha = 'eq_placeholder_purge_done_v1';
/// bt-device-identity ADR-3: the exact OS placeholder MAC, in the
/// `bt_a2dp:`-prefixed id shape used across all device-keyed SP maps.
static const _placeholderMacLiteral = 'bt_a2dp:02:00:00:00:00:00';
final SharedPreferences? _prefs;
/// Injected startup instance (S3-R4); getInstance() is only a fallback.
Future<SharedPreferences> _resolverPrefs() async =>
_prefs ?? SharedPreferences.getInstance();
Future<ConfiguracionEcualizador> cargar() async {
await migrarClavesPlaceholder();
final prefs = await _resolverPrefs();
final principal = _leerPresetPrincipal(prefs);
final porEmisora = _leerPresetsPorEmisora(prefs);
final presetsDispositivo = _leerMapa(prefs, _keyPresetsPorDispositivo);
final presetsMatriz = _leerMapa(prefs, _keyPresetsMatriz);
final nombresDispositivos = _leerMapaStrings(prefs, _keyNombresDispositivos);
return ConfiguracionEcualizador(
principal: principal,
porEmisora: porEmisora,
activo: prefs.getBool(_keyActivo) ?? true,
eqMultiDeviceEnabled: prefs.getBool(_keyMultiDeviceEnabled) ?? false,
presetsDispositivo: presetsDispositivo,
presetsMatriz: presetsMatriz,
nombresDispositivos: nombresDispositivos,
);
}
/// One-time guarded migration (bt-device-identity ADR-5): purges entries
/// keyed by the exact OS placeholder MAC ([_placeholderMacLiteral]) from
/// all three device-keyed SP maps. Idempotent — short-circuits via
/// [_keyPlaceholderPurgaHecha] after the first successful run. Every other
/// entry (including near-miss keys) is preserved byte-for-byte.
Future<void> migrarClavesPlaceholder() async {
final prefs = await _resolverPrefs();
if (prefs.getBool(_keyPlaceholderPurgaHecha) ?? false) return;
final presetsPorDispositivo = _leerMapa(prefs, _keyPresetsPorDispositivo);
if (presetsPorDispositivo.remove(_placeholderMacLiteral) != null) {
await _guardarMapa(
prefs,
_keyPresetsPorDispositivo,
presetsPorDispositivo,
);
}
// Matrix keys are "stationUuid:deviceId"; split on the FIRST colon only
// (station UUIDs are RFC4122 and contain no colons — multi-device-eq
// ADR-3), since deviceId itself may contain colons (e.g. a MAC-based id).
final presetsMatriz = _leerMapa(prefs, _keyPresetsMatriz);
final clavesMatrizAPurgar = presetsMatriz.keys.where((clave) {
final separador = clave.indexOf(':');
if (separador == -1) return false;
final deviceIdSegmento = clave.substring(separador + 1);
return deviceIdSegmento == _placeholderMacLiteral;
}).toList();
if (clavesMatrizAPurgar.isNotEmpty) {
for (final clave in clavesMatrizAPurgar) {
presetsMatriz.remove(clave);
}
await _guardarMapa(prefs, _keyPresetsMatriz, presetsMatriz);
}
final nombresDispositivos = _leerMapaStrings(
prefs,
_keyNombresDispositivos,
);
if (nombresDispositivos.remove(_placeholderMacLiteral) != null) {
await _guardarMapaStrings(
prefs,
_keyNombresDispositivos,
nombresDispositivos,
);
}
await prefs.setBool(_keyPlaceholderPurgaHecha, true);
}
Future<void> guardarPrincipal(PresetEcualizador preset) async {
final prefs = await _resolverPrefs();
await prefs.setString(_keyPresetPrincipal, jsonEncode(preset.toJson()));
}
Future<void> guardarPorEmisora(String uuid, PresetEcualizador preset) async {
final prefs = await _resolverPrefs();
final mapa = _leerPresetsPorEmisora(prefs);
mapa[uuid] = preset;
await _guardarPresetsPorEmisora(prefs, mapa);
}
Future<void> guardarActivo(bool activo) async {
final prefs = await _resolverPrefs();
await prefs.setBool(_keyActivo, activo);
}
Future<void> eliminarPorEmisora(String uuid) async {
final prefs = await _resolverPrefs();
final mapa = _leerPresetsPorEmisora(prefs);
mapa.remove(uuid);
await _guardarPresetsPorEmisora(prefs, mapa);
}
Future<void> guardarConfiguracion(ConfiguracionEcualizador config) async {
final prefs = await _resolverPrefs();
await prefs.setString(
_keyPresetPrincipal,
jsonEncode(config.principal.toJson()),
);
await _guardarPresetsPorEmisora(prefs, config.porEmisora);
await prefs.setBool(_keyActivo, config.activo);
await prefs.setBool(_keyMultiDeviceEnabled, config.eqMultiDeviceEnabled);
await _guardarMapa(
prefs,
_keyPresetsPorDispositivo,
config.presetsDispositivo,
);
await _guardarMapa(prefs, _keyPresetsMatriz, config.presetsMatriz);
await _guardarMapaStrings(
prefs,
_keyNombresDispositivos,
config.nombresDispositivos,
);
}
/// Persists the multi-device feature toggle.
Future<void> guardarToggleMultiDispositivo(bool habilitado) async {
final prefs = await _resolverPrefs();
await prefs.setBool(_keyMultiDeviceEnabled, habilitado);
}
/// Saves a per-device preset.
Future<void> guardarPresetDispositivo(
String deviceId,
PresetEcualizador preset,
) async {
final prefs = await _resolverPrefs();
final mapa = _leerMapa(prefs, _keyPresetsPorDispositivo);
mapa[deviceId] = preset;
await _guardarMapa(prefs, _keyPresetsPorDispositivo, mapa);
}
/// Saves a matrix (stationUuid:deviceId) preset entry.
Future<void> guardarPresetMatriz(
String clave,
PresetEcualizador preset,
) async {
final prefs = await _resolverPrefs();
final mapa = _leerMapa(prefs, _keyPresetsMatriz);
mapa[clave] = preset;
await _guardarMapa(prefs, _keyPresetsMatriz, mapa);
}
/// Removes a per-device preset entry.
Future<void> eliminarPresetDispositivo(String deviceId) async {
final prefs = await _resolverPrefs();
final mapa = _leerMapa(prefs, _keyPresetsPorDispositivo);
mapa.remove(deviceId);
await _guardarMapa(prefs, _keyPresetsPorDispositivo, mapa);
}
/// Removes a matrix preset entry.
Future<void> eliminarPresetMatriz(String clave) async {
final prefs = await _resolverPrefs();
final mapa = _leerMapa(prefs, _keyPresetsMatriz);
mapa.remove(clave);
await _guardarMapa(prefs, _keyPresetsMatriz, mapa);
}
/// Returns the persisted device custom names map.
Future<Map<String, String>> cargarNombresDispositivos() async {
final prefs = await _resolverPrefs();
return _leerMapaStrings(prefs, _keyNombresDispositivos);
}
/// Persists the device custom names map.
Future<void> guardarNombresDispositivos(Map<String, String> nombres) async {
final prefs = await _resolverPrefs();
await _guardarMapaStrings(prefs, _keyNombresDispositivos, nombres);
}
/// Reads a `Map<String, PresetEcualizador>` from a SharedPreferences JSON
/// key with per-entry tolerance (persistence-resilience D6): a value that
/// fails to parse is skipped and logged, its sibling entries survive and
/// every surviving key (including the matrix's colon-delimited
/// `stationUuid:deviceId` keys) is preserved byte-for-byte. A top-level
/// decode failure still degrades the whole map to empty (now logged too);
/// no flag/quarantine here — EQ presets are explicit-only writes and
/// trivially re-creatable (D6, intentional asymmetry vs. Alarms/Stations).
Map<String, PresetEcualizador> _leerMapa(
SharedPreferences prefs,
String key,
) {
final raw = prefs.getString(key);
if (raw == null || raw.isEmpty) return {};
try {
final data = Map<String, dynamic>.from(jsonDecode(raw) as Map);
final resultado = parseMapaTolerante<PresetEcualizador>(
data,
(valor) => PresetEcualizador.desdeJson(
Map<String, dynamic>.from(valor as Map),
),
subsistema: 'ecualizador',
coleccion: key,
);
return resultado.validas;
} catch (e) {
registrarSaltoPersistencia(
subsistema: 'ecualizador',
detalle: key,
razon: e.toString(),
);
return {};
}
}
Future<void> _guardarMapa(
SharedPreferences prefs,
String key,
Map<String, PresetEcualizador> mapa,
) async {
final serializado = mapa.map((k, v) => MapEntry(k, v.toJson()));
await prefs.setString(key, jsonEncode(serializado));
}
/// Reads a `Map<String, String>` from a SharedPreferences JSON key with
/// per-entry tolerance (persistence-resilience D6): a value that is not a
/// String is skipped and logged, sibling entries and keys survive
/// untouched. A top-level decode failure still degrades the whole map to
/// empty (now logged too); no flag/quarantine here (D6).
Map<String, String> _leerMapaStrings(SharedPreferences prefs, String key) {
final raw = prefs.getString(key);
if (raw == null || raw.isEmpty) return {};
try {
final data = Map<String, dynamic>.from(jsonDecode(raw) as Map);
final resultado = parseMapaTolerante<String>(
data,
(valor) => valor as String,
subsistema: 'ecualizador',
coleccion: key,
);
return resultado.validas;
} catch (e) {
registrarSaltoPersistencia(
subsistema: 'ecualizador',
detalle: key,
razon: e.toString(),
);
return {};
}
}
Future<void> _guardarMapaStrings(
SharedPreferences prefs,
String key,
Map<String, String> mapa,
) async {
await prefs.setString(key, jsonEncode(mapa));
}
/// Reads the single principal-preset key. Single-value (not a collection)
/// so there is nothing to skip per-entry; a decode failure still falls
/// back to [PresetEcualizador.flat] (unchanged behavior) but is now
/// logged too (persistence-resilience D6, diagnostics).
PresetEcualizador _leerPresetPrincipal(SharedPreferences prefs) {
final raw = prefs.getString(_keyPresetPrincipal);
if (raw == null || raw.isEmpty) {
return PresetEcualizador.flat;
}
try {
return PresetEcualizador.desdeJson(
Map<String, dynamic>.from(jsonDecode(raw) as Map),
);
} catch (e) {
registrarSaltoPersistencia(
subsistema: 'ecualizador',
detalle: _keyPresetPrincipal,
razon: e.toString(),
);
return PresetEcualizador.flat;
}
}
/// Reads the per-station preset map with per-entry tolerance
/// (persistence-resilience D6) via the same shared helper as [_leerMapa];
/// a station whose preset value fails to parse is skipped and logged, its
/// siblings survive under their original uuid keys.
Map<String, PresetEcualizador> _leerPresetsPorEmisora(
SharedPreferences prefs,
) {
final raw = prefs.getString(_keyPresetsPorEmisora);
if (raw == null || raw.isEmpty) {
return {};
}
try {
final data = Map<String, dynamic>.from(jsonDecode(raw) as Map);
final resultado = parseMapaTolerante<PresetEcualizador>(
data,
(valor) => PresetEcualizador.desdeJson(
Map<String, dynamic>.from(valor as Map),
),
subsistema: 'ecualizador',
coleccion: _keyPresetsPorEmisora,
);
return resultado.validas;
} catch (e) {
registrarSaltoPersistencia(
subsistema: 'ecualizador',
detalle: _keyPresetsPorEmisora,
razon: e.toString(),
);
return {};
}
}
Future<void> _guardarPresetsPorEmisora(
SharedPreferences prefs,
Map<String, PresetEcualizador> mapa,
) async {
final serializado = mapa.map(
(uuid, preset) => MapEntry(uuid, preset.toJson()),
);
await prefs.setString(_keyPresetsPorEmisora, jsonEncode(serializado));
}
}