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
+107 -57
View File
@@ -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,
+18 -11
View File
@@ -126,6 +126,10 @@ class EjecucionAlarmaNativa {
abstract class PuertoAlarmasAndroid {
Stream<EventoAlarmaAndroid> get eventosAlarma;
/// Provides the UI localizations used to localize the alarm/station names
/// sent to the native scheduler.
void configurarLocalizaciones(AppLocalizations l10n);
Future<void> programar(AlarmaMusical alarma);
Future<void> cancelar(String alarmaId);
Future<void> ocultarNotificacionAlarma(String alarmaId);
@@ -145,22 +149,24 @@ class ServicioAlarmasAndroid implements PuertoAlarmasAndroid {
ServicioAlarmasAndroid({
MethodChannel channel = const MethodChannel('pluriwave/alarm_scheduler'),
}) : _channel = channel {
_instalarHandler(_channel);
_instalarHandler();
}
final MethodChannel _channel;
static final _eventosController =
StreamController<EventoAlarmaAndroid>.broadcast();
static bool _handlerInstalado = false;
static AppLocalizations? _l10n;
static AppLocalizations get _textos {
// Instance state (S3-R2): each bridge owns its controller and l10n so
// independent instances never share events through globals.
final _eventosController = StreamController<EventoAlarmaAndroid>.broadcast();
AppLocalizations? _l10n;
AppLocalizations get _textos {
final actual = _l10n;
if (actual != null) return actual;
return lookupAppLocalizations(const Locale('es'));
}
static void configurarLocalizaciones(AppLocalizations l10n) {
@override
void configurarLocalizaciones(AppLocalizations l10n) {
_l10n = l10n;
}
@@ -334,10 +340,11 @@ class ServicioAlarmasAndroid implements PuertoAlarmasAndroid {
return _channel.invokeMethod<void>(method, args);
}
static void _instalarHandler(MethodChannel channel) {
if (_handlerInstalado) return;
_handlerInstalado = true;
channel.setMethodCallHandler((call) async {
// Installed once per instance from the constructor. Creating a second
// instance over the SAME channel re-binds the platform handler to the
// newest instance (production has exactly one instance per channel).
void _instalarHandler() {
_channel.setMethodCallHandler((call) async {
if (call.method != 'alarmFired') return;
final args = call.arguments;
if (args is Map) {
+50 -5
View File
@@ -9,6 +9,7 @@ import '../l10n/display_names.dart';
import '../l10n/gen/app_localizations.dart';
import '../modelos/emisora.dart';
import '../modelos/preset_ecualizador.dart';
import 'servicio_audio_session.dart';
/// Estado de reproducción expuesto al UI.
enum EstadoReproduccion { detenido, cargando, reproduciendo, pausado, error }
@@ -110,9 +111,12 @@ class ServicioAudio {
// ─────────────────────────────────────────────────────────────────────────────
// AudioHandler
// ─────────────────────────────────────────────────────────────────────────────
class PluriWaveAudioHandler extends BaseAudioHandler with SeekHandler {
class PluriWaveAudioHandler extends BaseAudioHandler
with SeekHandler
implements ObjetivoAudioInterrumpible {
static const _timeoutCambioFuente = Duration(seconds: 12);
static const _timeoutCierrePlayer = Duration(seconds: 3);
static const _factorAtenuacion = 0.3;
AndroidEqualizer _eq = AndroidEqualizer();
late AudioPlayer _player = _crearPlayer();
@@ -130,6 +134,15 @@ class PluriWaveAudioHandler extends BaseAudioHandler with SeekHandler {
double get volumen => _volumen;
AppLocalizations? _l10n;
/// Intent-to-play flag (Designs 3.1/7.2): reflects the LAST explicit
/// intent (play/pause/stop, including audio-session interruptions, which
/// pause through [pausar]). The S7 reconnect state machine reads it to
/// distinguish a network stall from an intentional pause.
bool _intencionReproducir = false;
/// Ducked state requested by the audio session (transient focus loss).
bool _atenuado = false;
AndroidEqualizer? get ecualizador => _eq;
bool _eqDisponible = false;
bool get ecualizadorDisponible => _eqDisponible;
@@ -278,6 +291,7 @@ class PluriWaveAudioHandler extends BaseAudioHandler with SeekHandler {
@override
Future<void> playMediaItem(MediaItem mediaItem) async {
_intencionReproducir = true;
final revision = ++_revisionFuente;
_colaCambioFuente = _colaCambioFuente
.catchError((_) {})
@@ -349,7 +363,7 @@ class PluriWaveAudioHandler extends BaseAudioHandler with SeekHandler {
_eqDisponible = false;
_androidAudioSessionId = null;
_player = _crearPlayer();
await _player.setVolume(_volumen);
await _player.setVolume(_volumenEfectivo);
_conectarStreamsPlayer();
}
@@ -450,17 +464,48 @@ class PluriWaveAudioHandler extends BaseAudioHandler with SeekHandler {
Future<void> setVolumen(double vol) async {
_volumen = vol.clamp(0.0, 1.0);
await _player.setVolume(_volumen);
await _player.setVolume(_volumenEfectivo);
}
double get _volumenEfectivo =>
_atenuado ? _volumen * _factorAtenuacion : _volumen;
// ── ObjetivoAudioInterrumpible (audio-session seam, S3-R1) ───────────────
@override
bool get intencionReproducir => _intencionReproducir;
@override
bool get estaReproduciendo => playbackState.value.playing;
@override
Future<void> pausar() => pause();
@override
Future<void> reanudar() => play();
@override
Future<void> setAtenuado(bool atenuado) async {
if (_atenuado == atenuado) return;
_atenuado = atenuado;
await _player.setVolume(_volumenEfectivo);
}
@override
Future<void> play() => _player.play();
Future<void> play() {
_intencionReproducir = true;
return _player.play();
}
@override
Future<void> pause() => _player.pause();
Future<void> pause() {
_intencionReproducir = false;
return _player.pause();
}
@override
Future<void> stop() async {
_intencionReproducir = false;
_revisionFuente++;
await _player.stop();
emisoraActual = null;
+112
View File
@@ -0,0 +1,112 @@
import 'dart:async';
import 'dart:developer' as developer;
import 'package:audio_session/audio_session.dart';
import 'package:flutter/foundation.dart';
/// Minimal playback contract this service needs from the audio handler
/// (Design 3.1). `PluriWaveAudioHandler` implements it; tests use a fake.
abstract class ObjetivoAudioInterrumpible {
/// Intent-to-play flag (Designs 3.1/7.2): true while the user wants audio
/// playing. The S7 reconnect logic reads the same flag, so an interruption
/// pause also disarms reconnection attempts.
bool get intencionReproducir;
bool get estaReproduciendo;
Future<void> pausar();
Future<void> reanudar();
/// Temporarily lowers ("ducks") the output volume without pausing.
Future<void> setAtenuado(bool atenuado);
}
/// Wrapper around `package:audio_session` (S3-R1): configures the session
/// for music playback and translates interruption / becoming-noisy events
/// into pause, duck and auto-resume calls on the audio handler.
class ServicioAudioSession {
ServicioAudioSession({
required ObjetivoAudioInterrumpible objetivo,
Future<AudioSession> Function()? obtenerSesion,
}) : _objetivo = objetivo,
_obtenerSesion = obtenerSesion ?? (() => AudioSession.instance);
final ObjetivoAudioInterrumpible _objetivo;
final Future<AudioSession> Function() _obtenerSesion;
StreamSubscription<AudioInterruptionEvent>? _interrupcionesSub;
StreamSubscription<void>? _ruidoSub;
/// True when WE paused because of an interruption; only then does an
/// interruption end with shouldResume restart playback.
bool _pausadoPorInterrupcion = false;
Future<void> configurar() async {
try {
final sesion = await _obtenerSesion();
await sesion.configure(
const AudioSessionConfiguration.music().copyWith(
androidWillPauseWhenDucked: true,
),
);
await _interrupcionesSub?.cancel();
await _ruidoSub?.cancel();
_interrupcionesSub = sesion.interruptionEventStream.listen(
(evento) => unawaited(manejarInterrupcion(evento)),
);
_ruidoSub = sesion.becomingNoisyEventStream.listen(
(_) => unawaited(manejarDesconexionSalida()),
);
} catch (e) {
developer.log(
'[PluriWave] No se pudo configurar la sesion de audio: $e',
name: 'ServicioAudioSession',
level: 900,
);
}
}
@visibleForTesting
Future<void> manejarInterrupcion(AudioInterruptionEvent evento) async {
if (evento.begin) {
switch (evento.type) {
case AudioInterruptionType.duck:
await _objetivo.setAtenuado(true);
case AudioInterruptionType.pause:
case AudioInterruptionType.unknown:
if (_objetivo.estaReproduciendo || _objetivo.intencionReproducir) {
_pausadoPorInterrupcion = true;
await _objetivo.pausar();
}
}
return;
}
switch (evento.type) {
case AudioInterruptionType.duck:
await _objetivo.setAtenuado(false);
case AudioInterruptionType.pause:
// Transient loss ended and the OS says we may resume.
if (_pausadoPorInterrupcion) {
_pausadoPorInterrupcion = false;
await _objetivo.reanudar();
}
case AudioInterruptionType.unknown:
// Permanent focus loss: never auto-resume.
_pausadoPorInterrupcion = false;
}
}
@visibleForTesting
Future<void> manejarDesconexionSalida() async {
// Headphones unplugged: hard pause, never auto-resume afterwards.
_pausadoPorInterrupcion = false;
if (_objetivo.estaReproduciendo) {
await _objetivo.pausar();
}
}
Future<void> dispose() async {
await _interrupcionesSub?.cancel();
await _ruidoSub?.cancel();
}
}
+11 -3
View File
@@ -27,12 +27,20 @@ class ContenidoAyudaPluri {
}
class ServicioContenidoApp {
ServicioContenidoApp({SharedPreferences? prefs}) : _prefs = prefs;
static const _keyOnboardingVisto = 'pluri_onboarding_visto_v1';
static const _keyVersionVista = 'pluri_ultima_version_novedades_v1';
static const _versiones = ['0.1.47'];
final SharedPreferences? _prefs;
/// Injected startup instance (S3-R4); getInstance() is only a fallback.
Future<SharedPreferences> _resolverPrefs() async =>
_prefs ?? SharedPreferences.getInstance();
Future<bool> debeMostrarInicio() async {
final prefs = await SharedPreferences.getInstance();
final prefs = await _resolverPrefs();
final info = await PackageInfo.fromPlatform();
final versionActual = info.version;
return !(prefs.getBool(_keyOnboardingVisto) ?? false) ||
@@ -40,7 +48,7 @@ class ServicioContenidoApp {
}
Future<void> marcarVisto() async {
final prefs = await SharedPreferences.getInstance();
final prefs = await _resolverPrefs();
final info = await PackageInfo.fromPlatform();
await prefs.setBool(_keyOnboardingVisto, true);
await prefs.setString(_keyVersionVista, info.version);
@@ -51,7 +59,7 @@ class ServicioContenidoApp {
bool soloPendientes = false,
}) async {
final info = await PackageInfo.fromPlatform();
final prefs = await SharedPreferences.getInstance();
final prefs = await _resolverPrefs();
final ultimaVista = prefs.getString(_keyVersionVista);
final idioma = _idiomaSoportado(codigoIdioma);
final mostrarOnboarding =
+14 -6
View File
@@ -17,12 +17,20 @@ class ConfiguracionEcualizador {
}
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';
final SharedPreferences? _prefs;
/// Injected startup instance (S3-R4); getInstance() is only a fallback.
Future<SharedPreferences> _resolverPrefs() async =>
_prefs ?? SharedPreferences.getInstance();
Future<ConfiguracionEcualizador> cargar() async {
final prefs = await SharedPreferences.getInstance();
final prefs = await _resolverPrefs();
final principal = _leerPresetPrincipal(prefs);
final porEmisora = _leerPresetsPorEmisora(prefs);
return ConfiguracionEcualizador(
@@ -33,31 +41,31 @@ class ServicioEcualizador {
}
Future<void> guardarPrincipal(PresetEcualizador preset) async {
final prefs = await SharedPreferences.getInstance();
final prefs = await _resolverPrefs();
await prefs.setString(_keyPresetPrincipal, jsonEncode(preset.toJson()));
}
Future<void> guardarPorEmisora(String uuid, PresetEcualizador preset) async {
final prefs = await SharedPreferences.getInstance();
final prefs = await _resolverPrefs();
final mapa = _leerPresetsPorEmisora(prefs);
mapa[uuid] = preset;
await _guardarPresetsPorEmisora(prefs, mapa);
}
Future<void> guardarActivo(bool activo) async {
final prefs = await SharedPreferences.getInstance();
final prefs = await _resolverPrefs();
await prefs.setBool(_keyActivo, activo);
}
Future<void> eliminarPorEmisora(String uuid) async {
final prefs = await SharedPreferences.getInstance();
final prefs = await _resolverPrefs();
final mapa = _leerPresetsPorEmisora(prefs);
mapa.remove(uuid);
await _guardarPresetsPorEmisora(prefs, mapa);
}
Future<void> guardarConfiguracion(ConfiguracionEcualizador config) async {
final prefs = await SharedPreferences.getInstance();
final prefs = await _resolverPrefs();
await prefs.setString(
_keyPresetPrincipal,
jsonEncode(config.principal.toJson()),
+12 -5
View File
@@ -82,9 +82,11 @@ class ServicioGrabacionRadio {
http.Client? cliente,
Future<Directory> Function()? resolverDirectorioBase,
DateTime Function()? reloj,
SharedPreferences? prefs,
}) : _clienteExterno = cliente,
_resolverDirectorioBase = resolverDirectorioBase,
_reloj = reloj ?? DateTime.now;
_reloj = reloj ?? DateTime.now,
_prefs = prefs;
static const _claveDirectorio = 'grabacion_radio_directorio';
static const _claveMaxBytes = 'grabacion_radio_max_bytes_v1';
@@ -93,6 +95,11 @@ class ServicioGrabacionRadio {
final http.Client? _clienteExterno;
final Future<Directory> Function()? _resolverDirectorioBase;
final DateTime Function() _reloj;
final SharedPreferences? _prefs;
/// Injected startup instance (S3-R4); getInstance() is only a fallback.
Future<SharedPreferences> _resolverPrefs() async =>
_prefs ?? SharedPreferences.getInstance();
final _estadoController = StreamController<EstadoGrabacionRadio>.broadcast();
AppLocalizations? _l10n;
@@ -123,7 +130,7 @@ class ServicioGrabacionRadio {
Future<void> inicializar() async {
try {
final prefs = await SharedPreferences.getInstance();
final prefs = await _resolverPrefs();
_directorioConfigurado = prefs.getString(_claveDirectorio);
_maxBytes = prefs.getInt(_claveMaxBytes) ?? maxBytesPorDefecto;
} catch (_) {
@@ -151,7 +158,7 @@ class ServicioGrabacionRadio {
}
_directorioConfigurado = normalizado;
try {
final prefs = await SharedPreferences.getInstance();
final prefs = await _resolverPrefs();
await prefs.setString(_claveDirectorio, normalizado);
} catch (_) {}
_emitir(_estado);
@@ -160,7 +167,7 @@ class ServicioGrabacionRadio {
Future<void> limpiarDirectorioConfigurado() async {
_directorioConfigurado = null;
try {
final prefs = await SharedPreferences.getInstance();
final prefs = await _resolverPrefs();
await prefs.remove(_claveDirectorio);
} catch (_) {}
_emitir(_estado);
@@ -172,7 +179,7 @@ class ServicioGrabacionRadio {
}
_maxBytes = bytes;
try {
final prefs = await SharedPreferences.getInstance();
final prefs = await _resolverPrefs();
await prefs.setInt(_claveMaxBytes, bytes);
} catch (_) {}
_emitir(_estado);