Tres fallos reportados en uso real, con sus causas raiz verificadas en codigo.
1. Ecualizador: el estado no tenia dueño unico
El handler arrancaba con `_ecualizadorActivo = true` a fuego. El valor
persistido solo llegaba por EstadoEcualizador.cargarPersistido(), alcanzable
unicamente desde el arbol de widgets, que un arranque headless de Android Auto
nunca construye. Resultado: el coche reproducia con el EQ forzado a ON mientras
disco e interfaz decian OFF.
Ahora registrarHandler siembra el flag desde disco en todos los motores y
setEcualizadorActivo persiste por su cuenta, asi que un toggle desde el coche o
la notificacion sobrevive sin EstadoEcualizador. _resincronizarConHandler pasa
a ser adopcion pura de interfaz.
Ademas mapearGananciaNativa enviaba 0 dB al punto MEDIO del rango nativo. Con
un getBandLevelRange() asimetrico, un preset plano metia varios dB de boost
real: la causa del "suena muy alto con el boton apagado". Reescrito para
escalar cada lado contra su propio extremo, de modo que 0 dB es siempre 0.
El dispatch de customAction no tenia ningun test. Se extrae decidirToggleEq y
se cubre contra el handler real. El efecto nativo se re-asierta al reactivarse
el reproductor, porque AudioEffect.setEnabled de just_audio es un no-op
mientras la plataforma esta desacoplada.
2. Musica Local no aparecia en el arbol de Android Auto
hayCarpetaConfigurada() consultaba MethodChannel('pluriwave/file_actions'),
registrado solo en MainActivity.configureFlutterEngine. Sin Activity no hay
handler, invokeMethod lanza MissingPluginException y el catch la confundia con
"permiso revocado", omitiendo el nodo. No dependia del entitlement.
La logica SAF sale a packages/pluriwave_file_actions, un paquete plugin local.
El motor headless que crea audio_service ejecuta GeneratedPluginRegistrant en
su constructor, asi que el canal queda registrado en ambos motores. Repuntar el
manifest a una subclase de AudioService no era viable: AudioServicePlugin
enlaza por ComponentName explicito y la app perderia el audio.
EstadoCarpetaLocal de tres valores separa "sin carpeta" de "canal no
disponible"; la raiz decide por la URI persistida y el subarbol muestra un item
explicativo en vez de una carpeta vacia. La invalidacion del arbol cacheado se
dispara al reanudar con el coche ya suscrito; el guardia anterior miraba
View.maybeOf, que bajo runApp siempre existe, por lo que se gastaba en el
arranque headless y no volvia a dispararse.
3. El paywall bloqueaba las compras
restorePurchases() de in_app_purchase_android emite siempre, y con lista vacia
si no hay nada que restaurar. El `if (compras.isEmpty) return;` se la tragaba,
noEncontrada nunca se emitia y la rama que limpia _compraEnCurso estaba muerta
en produccion. Como comprar y restaurar comparten ese flag, un usuario sin
compras que pulsaba restaurar se quedaba sin poder comprar.
Suite completa: 1366 pasan, 2 omitidos. 17 tests nuevos, todos nacidos rojos y
verificados por mutacion. flutter analyze mantiene los 5 avisos preexistentes.
488 lines
18 KiB
Dart
488 lines
18 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
import '../modelos/preset_ecualizador.dart';
|
|
import 'persistencia_tolerante.dart';
|
|
import 'servicio_dispositivo_audio.dart'
|
|
show idAltavozInterno, prefijoDispositivoOtro;
|
|
|
|
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';
|
|
|
|
/// Guard flag for the one-time `builtin_speaker` collision purge.
|
|
static const _keyColisionBasePurgaHecha = 'eq_builtin_speaker_purge_done_v1';
|
|
|
|
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();
|
|
await migrarColisionAltavozInterno();
|
|
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;
|
|
await _purgarDeviceId(prefs, _placeholderMacLiteral);
|
|
await prefs.setBool(_keyPlaceholderPurgaHecha, true);
|
|
}
|
|
|
|
/// One-time guarded migration: purges entries keyed by [idAltavozInterno].
|
|
///
|
|
/// `MainActivity.deviceToMap` used to hand the phone-speaker id to ANY output
|
|
/// type it could not name (LE Audio, car bus, dock). Dart then created a
|
|
/// device entry under that id, and from then on every playback through the
|
|
/// phone's own speaker matched it — permanently flagging whatever the user
|
|
/// had renamed it to as the active device. The native id is fixed; this
|
|
/// clears what the collision already persisted. Idempotent via
|
|
/// [_keyColisionBasePurgaHecha]; every other entry survives byte-for-byte.
|
|
Future<void> migrarColisionAltavozInterno() async {
|
|
final prefs = await _resolverPrefs();
|
|
if (prefs.getBool(_keyColisionBasePurgaHecha) ?? false) return;
|
|
await _purgarDeviceId(prefs, idAltavozInterno);
|
|
await _purgarPrefijoDispositivo(prefs, prefijoDispositivoOtro);
|
|
await prefs.setBool(_keyColisionBasePurgaHecha, true);
|
|
}
|
|
|
|
/// Removes every device-keyed entry whose id starts with [prefijo].
|
|
///
|
|
/// Used for the `other:` namespace: a build that ranked every unlisted
|
|
/// `AudioDeviceInfo` type above the phone speaker selected permanent internal
|
|
/// sinks (TYPE_FM on Xiaomi, TYPE_BUILTIN_SPEAKER_SAFE elsewhere) as the
|
|
/// active output and persisted rows for them. Genuine external outputs in
|
|
/// this namespace are re-registered on their next connection, so clearing the
|
|
/// whole prefix costs nothing and needs no per-type knowledge here.
|
|
Future<void> _purgarPrefijoDispositivo(
|
|
SharedPreferences prefs,
|
|
String prefijo,
|
|
) async {
|
|
final presetsPorDispositivo = _leerMapa(prefs, _keyPresetsPorDispositivo);
|
|
final dispositivos =
|
|
presetsPorDispositivo.keys
|
|
.where((clave) => clave.startsWith(prefijo))
|
|
.toList();
|
|
|
|
final nombres = _leerMapaStrings(prefs, _keyNombresDispositivos);
|
|
final nombresAPurgar =
|
|
nombres.keys.where((clave) => clave.startsWith(prefijo)).toList();
|
|
|
|
final matriz = _leerMapa(prefs, _keyPresetsMatriz);
|
|
final matrizAPurgar =
|
|
matriz.keys.where((clave) {
|
|
final separador = clave.indexOf(':');
|
|
if (separador == -1) return false;
|
|
return clave.substring(separador + 1).startsWith(prefijo);
|
|
}).toList();
|
|
|
|
for (final clave in dispositivos) {
|
|
presetsPorDispositivo.remove(clave);
|
|
}
|
|
for (final clave in nombresAPurgar) {
|
|
nombres.remove(clave);
|
|
}
|
|
for (final clave in matrizAPurgar) {
|
|
matriz.remove(clave);
|
|
}
|
|
|
|
if (dispositivos.isNotEmpty) {
|
|
await _guardarMapa(
|
|
prefs,
|
|
_keyPresetsPorDispositivo,
|
|
presetsPorDispositivo,
|
|
);
|
|
}
|
|
if (nombresAPurgar.isNotEmpty) {
|
|
await _guardarMapaStrings(prefs, _keyNombresDispositivos, nombres);
|
|
}
|
|
if (matrizAPurgar.isNotEmpty) {
|
|
await _guardarMapa(prefs, _keyPresetsMatriz, matriz);
|
|
}
|
|
}
|
|
|
|
/// Removes every trace of [deviceId] from the three device-keyed SP maps.
|
|
///
|
|
/// Shared by the guarded migrations and by [eliminarDispositivo]; each map is
|
|
/// only rewritten when the key was actually present.
|
|
Future<void> _purgarDeviceId(SharedPreferences prefs, String deviceId) async {
|
|
final presetsPorDispositivo = _leerMapa(prefs, _keyPresetsPorDispositivo);
|
|
if (presetsPorDispositivo.remove(deviceId) != 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;
|
|
return clave.substring(separador + 1) == deviceId;
|
|
}).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(deviceId) != null) {
|
|
await _guardarMapaStrings(
|
|
prefs,
|
|
_keyNombresDispositivos,
|
|
nombresDispositivos,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Forgets a device completely: its preset, its custom name and every matrix
|
|
/// entry that targets it. Used by the "remove device" action so the user can
|
|
/// clear stale or duplicate entries from the known-devices list.
|
|
Future<void> eliminarDispositivo(String deviceId) async {
|
|
final prefs = await _resolverPrefs();
|
|
await _purgarDeviceId(prefs, deviceId);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
/// The persisted equalizer on/off flag, or `null` when the user has never
|
|
/// touched the toggle.
|
|
///
|
|
/// Deliberately narrower than [cargar] (eq-estado-unico item A): it reads
|
|
/// ONE key and runs none of the migrations, because its caller is
|
|
/// `registrarHandler`, on the audio bootstrap path of EVERY engine —
|
|
/// including the headless one Android Auto starts, where there is no
|
|
/// widget tree and `EstadoEcualizador` never exists. It must stay cheap
|
|
/// and it must never mutate anything.
|
|
///
|
|
/// `null` is preserved rather than collapsed to a default so that
|
|
/// `estadoEqInicial` — not this service — owns the "never persisted"
|
|
/// policy in exactly one place.
|
|
Future<bool?> leerActivo() async {
|
|
final prefs = await _resolverPrefs();
|
|
return prefs.getBool(_keyActivo);
|
|
}
|
|
|
|
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));
|
|
}
|
|
}
|