fix(radio): persist the last-played station across restarts
EstadoRadio.emisoraActual only ever reflected in-memory state (_emisoraSeleccionada or the live audio service), so stopping playback and reopening the app left the Escuchar hero empty even though the user had a station selected right before closing it. Persist the station whenever it changes (reproducir(), and the Android-Auto out-of-band reconciliation path) and restore it as _emisoraSeleccionada on the next cold start, only when nothing is already selected. This never touches the audio service directly: no playback starts and estadoStream/estaSonando stay at their stopped default, matching how every consumer already gates "is it playing" on the playback-status stream rather than on emisoraActual itself.
This commit is contained in:
@@ -173,6 +173,10 @@ class EstadoRadio extends ChangeNotifier {
|
||||
static const _keyEmisoraPreferida = 'emisora_preferida_uuid_v1';
|
||||
static const _keyOrdenListas = 'orden_listas_emisoras_v1';
|
||||
static const _keyTimerSuenoPresets = 'timer_sueno_presets_segundos_v1';
|
||||
// Issue 4 (feedback-pruebas): last-played station, so the Escuchar hero
|
||||
// keeps showing "what I was listening to" (stopped, not playing) after a
|
||||
// full app restart instead of going empty.
|
||||
static const _keyUltimaEmisora = 'ultima_emisora_v1';
|
||||
static const _timerSuenoPresetsDefecto = <int>[
|
||||
180,
|
||||
300,
|
||||
@@ -300,6 +304,50 @@ class EstadoRadio extends ChangeNotifier {
|
||||
_cargarEmisorasCustom(),
|
||||
]);
|
||||
await _normalizarEmisoraPreferida();
|
||||
await _restaurarUltimaEmisora();
|
||||
}
|
||||
|
||||
/// Issue 4 (feedback-pruebas): restores the last-played station as a
|
||||
/// STOPPED `emisoraActual` on a cold start. Only fills the gap — if
|
||||
/// something is ALREADY selected (a real play already ran concurrently),
|
||||
/// this is a no-op. Never touches `audio`: no playback starts, no network
|
||||
/// request is made, `estadoStream`/`estaSonando` stay at their fresh
|
||||
/// "detenido" default, exactly like every other consumer of
|
||||
/// `emisoraActual` already expects (they gate "is it playing" on the
|
||||
/// separate playback-status stream, never on `emisoraActual != null`).
|
||||
Future<void> _restaurarUltimaEmisora() async {
|
||||
if (_emisoraSeleccionada != null || audio.emisoraActual != null) return;
|
||||
try {
|
||||
final prefs = await _resolverPrefs();
|
||||
final raw = prefs.getString(_keyUltimaEmisora);
|
||||
if (raw == null) return;
|
||||
final mapa = jsonDecode(raw) as Map<String, dynamic>;
|
||||
_emisoraSeleccionada = Emisora.fromMap(mapa);
|
||||
} catch (e) {
|
||||
registrarSaltoPersistencia(
|
||||
subsistema: 'ultima_emisora',
|
||||
detalle: 'restaurar',
|
||||
razon: e.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Best-effort remembers [emisora] as the last used station (issue 4) so
|
||||
/// [_restaurarUltimaEmisora] can bring it back after a restart. Fire-and-
|
||||
/// forget, same treatment [reproducir] already gives other non-critical
|
||||
/// side effects (e.g. `radio.registrarClick`) — a failed write here must
|
||||
/// never block or fail actual playback.
|
||||
Future<void> _persistirUltimaEmisora(Emisora emisora) async {
|
||||
try {
|
||||
final prefs = await _resolverPrefs();
|
||||
await prefs.setString(_keyUltimaEmisora, jsonEncode(emisora.toMap()));
|
||||
} catch (e) {
|
||||
registrarSaltoPersistencia(
|
||||
subsistema: 'ultima_emisora',
|
||||
detalle: 'persistir ${emisora.uuid}',
|
||||
razon: e.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Escucha el stream de estado del audio y gestiona errores de reproducción.
|
||||
@@ -321,6 +369,9 @@ class EstadoRadio extends ChangeNotifier {
|
||||
final actual = audio.emisoraActual;
|
||||
if (actual != null && actual.uuid != _emisoraSeleccionada?.uuid) {
|
||||
_emisoraSeleccionada = actual;
|
||||
// Issue 4: an Android-Auto-initiated selection is a real station
|
||||
// change too — remember it the same way `reproducir` does.
|
||||
unawaited(_persistirUltimaEmisora(actual));
|
||||
}
|
||||
notifyListeners();
|
||||
});
|
||||
@@ -508,6 +559,10 @@ class EstadoRadio extends ChangeNotifier {
|
||||
}
|
||||
_emisoraSeleccionada = emisora;
|
||||
notifyListeners();
|
||||
// Issue 4: remembers the station the user just picked so it survives a
|
||||
// restart — fire-and-forget, same treatment as `radio.registrarClick`
|
||||
// below (a persistence failure here must never block playback).
|
||||
unawaited(_persistirUltimaEmisora(emisora));
|
||||
try {
|
||||
await audio.reproducir(emisora);
|
||||
if (revision != _revisionReproduccion) return;
|
||||
|
||||
+284
-229
@@ -412,259 +412,314 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
group(
|
||||
'EstadoRadio — emisoras custom: lectura tolerante y guardia de '
|
||||
'degradacion (persistence-resilience)',
|
||||
() {
|
||||
test(
|
||||
'entradas invalidas se omiten sin perder las validas ni fabricar '
|
||||
'uuid (D5 parcial)',
|
||||
() async {
|
||||
final archivo = await _crearArchivoCustomRaw(
|
||||
jsonEncode([
|
||||
{'uuid': 'custom-1', 'nombre': 'Valida Uno', 'url': 'http://a'},
|
||||
{'uuid': 'custom-2', 'nombre': 'Valida Dos', 'url': 'http://b'},
|
||||
// falta 'url' (campo requerido) -> Emisora.fromMap lanza.
|
||||
{'uuid': 'custom-3', 'nombre': 'Sin url'},
|
||||
// falta 'uuid' -> Emisora.fromMap lanza.
|
||||
{'nombre': 'Sin uuid', 'url': 'http://d'},
|
||||
]),
|
||||
);
|
||||
final estado = EstadoRadio(
|
||||
audio: FakeServicioAudio(),
|
||||
favoritos: FakeServicioFavoritos(),
|
||||
radio: FakeServicioRadio(),
|
||||
servicioEcualizador: FakeServicioEcualizador(),
|
||||
resolverArchivoCustom: () async => archivo,
|
||||
iniciarAutomaticamente: false,
|
||||
);
|
||||
|
||||
await estado.inicializar();
|
||||
|
||||
expect(estado.emisorasCustom, hasLength(2));
|
||||
expect(estado.emisorasCustom.map((e) => e.uuid).toSet(), {
|
||||
'custom-1',
|
||||
'custom-2',
|
||||
});
|
||||
},
|
||||
group('EstadoRadio — emisoras custom: lectura tolerante y guardia de '
|
||||
'degradacion (persistence-resilience)', () {
|
||||
test('entradas invalidas se omiten sin perder las validas ni fabricar '
|
||||
'uuid (D5 parcial)', () async {
|
||||
final archivo = await _crearArchivoCustomRaw(
|
||||
jsonEncode([
|
||||
{'uuid': 'custom-1', 'nombre': 'Valida Uno', 'url': 'http://a'},
|
||||
{'uuid': 'custom-2', 'nombre': 'Valida Dos', 'url': 'http://b'},
|
||||
// falta 'url' (campo requerido) -> Emisora.fromMap lanza.
|
||||
{'uuid': 'custom-3', 'nombre': 'Sin url'},
|
||||
// falta 'uuid' -> Emisora.fromMap lanza.
|
||||
{'nombre': 'Sin uuid', 'url': 'http://d'},
|
||||
]),
|
||||
);
|
||||
final estado = EstadoRadio(
|
||||
audio: FakeServicioAudio(),
|
||||
favoritos: FakeServicioFavoritos(),
|
||||
radio: FakeServicioRadio(),
|
||||
servicioEcualizador: FakeServicioEcualizador(),
|
||||
resolverArchivoCustom: () async => archivo,
|
||||
iniciarAutomaticamente: false,
|
||||
);
|
||||
|
||||
test(
|
||||
'si resolver la ruta del archivo custom falla, la inicializacion '
|
||||
'sobrevive y las cargas hermanas no se pierden (D5 IO-fail)',
|
||||
() async {
|
||||
final estado = EstadoRadio(
|
||||
audio: FakeServicioAudio(),
|
||||
favoritos: FakeServicioFavoritos(),
|
||||
radio: FakeServicioRadio(),
|
||||
servicioEcualizador: FakeServicioEcualizador(),
|
||||
resolverArchivoCustom:
|
||||
() async => throw const FileSystemException('sin storage'),
|
||||
iniciarAutomaticamente: false,
|
||||
);
|
||||
await estado.inicializar();
|
||||
|
||||
// Path resolution failing must be treated as an IO-fail, not
|
||||
// escape _cargarEmisorasCustom: it runs inside _init()'s
|
||||
// Future.wait, so an uncaught throw would also reject the
|
||||
// sibling loads (populares/favoritos/grupos).
|
||||
await estado.inicializar();
|
||||
expect(estado.emisorasCustom, hasLength(2));
|
||||
expect(estado.emisorasCustom.map((e) => e.uuid).toSet(), {
|
||||
'custom-1',
|
||||
'custom-2',
|
||||
});
|
||||
});
|
||||
|
||||
expect(estado.emisorasCustom, isEmpty);
|
||||
},
|
||||
test('si resolver la ruta del archivo custom falla, la inicializacion '
|
||||
'sobrevive y las cargas hermanas no se pierden (D5 IO-fail)', () async {
|
||||
final estado = EstadoRadio(
|
||||
audio: FakeServicioAudio(),
|
||||
favoritos: FakeServicioFavoritos(),
|
||||
radio: FakeServicioRadio(),
|
||||
servicioEcualizador: FakeServicioEcualizador(),
|
||||
resolverArchivoCustom:
|
||||
() async => throw const FileSystemException('sin storage'),
|
||||
iniciarAutomaticamente: false,
|
||||
);
|
||||
|
||||
test(
|
||||
'JSON invalido al nivel superior pone en cuarentena el archivo '
|
||||
'original (D5 parse-fail)',
|
||||
() async {
|
||||
final archivo = await _crearArchivoCustomRaw('{bad');
|
||||
final estado = EstadoRadio(
|
||||
audio: FakeServicioAudio(),
|
||||
favoritos: FakeServicioFavoritos(),
|
||||
radio: FakeServicioRadio(),
|
||||
servicioEcualizador: FakeServicioEcualizador(),
|
||||
resolverArchivoCustom: () async => archivo,
|
||||
iniciarAutomaticamente: false,
|
||||
);
|
||||
// Path resolution failing must be treated as an IO-fail, not
|
||||
// escape _cargarEmisorasCustom: it runs inside _init()'s
|
||||
// Future.wait, so an uncaught throw would also reject the
|
||||
// sibling loads (populares/favoritos/grupos).
|
||||
await estado.inicializar();
|
||||
|
||||
await estado.inicializar();
|
||||
expect(estado.emisorasCustom, isEmpty);
|
||||
});
|
||||
|
||||
expect(estado.emisorasCustom, isEmpty);
|
||||
final sidecar = File('${archivo.path}.corrupt');
|
||||
expect(await sidecar.exists(), isTrue);
|
||||
expect(await sidecar.readAsString(), '{bad');
|
||||
expect(await archivo.exists(), isFalse);
|
||||
},
|
||||
test('JSON invalido al nivel superior pone en cuarentena el archivo '
|
||||
'original (D5 parse-fail)', () async {
|
||||
final archivo = await _crearArchivoCustomRaw('{bad');
|
||||
final estado = EstadoRadio(
|
||||
audio: FakeServicioAudio(),
|
||||
favoritos: FakeServicioFavoritos(),
|
||||
radio: FakeServicioRadio(),
|
||||
servicioEcualizador: FakeServicioEcualizador(),
|
||||
resolverArchivoCustom: () async => archivo,
|
||||
iniciarAutomaticamente: false,
|
||||
);
|
||||
|
||||
test(
|
||||
'agregar tras la cuarentena escribe solo la nueva emisora y no '
|
||||
'toca el sidecar (D5, autoridad de escritura restaurada)',
|
||||
() async {
|
||||
final archivo = await _crearArchivoCustomRaw('{bad');
|
||||
final estado = EstadoRadio(
|
||||
audio: FakeServicioAudio(),
|
||||
favoritos: FakeServicioFavoritos(),
|
||||
radio: FakeServicioRadio(),
|
||||
servicioEcualizador: FakeServicioEcualizador(),
|
||||
resolverArchivoCustom: () async => archivo,
|
||||
iniciarAutomaticamente: false,
|
||||
);
|
||||
await estado.inicializar();
|
||||
final sidecar = File('${archivo.path}.corrupt');
|
||||
final sidecarPrevio = await sidecar.readAsString();
|
||||
await estado.inicializar();
|
||||
|
||||
final nueva = emisoraDemo(uuid: 'nueva-1', nombre: 'Nueva');
|
||||
await estado.agregarEmisoraCustom(nueva);
|
||||
expect(estado.emisorasCustom, isEmpty);
|
||||
final sidecar = File('${archivo.path}.corrupt');
|
||||
expect(await sidecar.exists(), isTrue);
|
||||
expect(await sidecar.readAsString(), '{bad');
|
||||
expect(await archivo.exists(), isFalse);
|
||||
});
|
||||
|
||||
expect(estado.emisorasCustom.map((e) => e.uuid), ['nueva-1']);
|
||||
final contenidoVivo =
|
||||
jsonDecode(await archivo.readAsString()) as List;
|
||||
expect(contenidoVivo, hasLength(1));
|
||||
expect((contenidoVivo.single as Map)['uuid'], 'nueva-1');
|
||||
expect(await sidecar.readAsString(), sidecarPrevio);
|
||||
},
|
||||
test('agregar tras la cuarentena escribe solo la nueva emisora y no '
|
||||
'toca el sidecar (D5, autoridad de escritura restaurada)', () async {
|
||||
final archivo = await _crearArchivoCustomRaw('{bad');
|
||||
final estado = EstadoRadio(
|
||||
audio: FakeServicioAudio(),
|
||||
favoritos: FakeServicioFavoritos(),
|
||||
radio: FakeServicioRadio(),
|
||||
servicioEcualizador: FakeServicioEcualizador(),
|
||||
resolverArchivoCustom: () async => archivo,
|
||||
iniciarAutomaticamente: false,
|
||||
);
|
||||
await estado.inicializar();
|
||||
final sidecar = File('${archivo.path}.corrupt');
|
||||
final sidecarPrevio = await sidecar.readAsString();
|
||||
|
||||
final nueva = emisoraDemo(uuid: 'nueva-1', nombre: 'Nueva');
|
||||
await estado.agregarEmisoraCustom(nueva);
|
||||
|
||||
expect(estado.emisorasCustom.map((e) => e.uuid), ['nueva-1']);
|
||||
final contenidoVivo = jsonDecode(await archivo.readAsString()) as List;
|
||||
expect(contenidoVivo, hasLength(1));
|
||||
expect((contenidoVivo.single as Map)['uuid'], 'nueva-1');
|
||||
expect(await sidecar.readAsString(), sidecarPrevio);
|
||||
});
|
||||
|
||||
test('fallo de IO al leer suprime la escritura y no se restaura con un '
|
||||
'alta explicita (D5 IO-fail)', () async {
|
||||
final espia = _ArchivoEspia(
|
||||
path: '/fake/emisoras_custom.json',
|
||||
exists: () async => true,
|
||||
readAsString:
|
||||
() async =>
|
||||
throw const FileSystemException('fallo simulado de lectura'),
|
||||
);
|
||||
final estado = EstadoRadio(
|
||||
audio: FakeServicioAudio(),
|
||||
favoritos: FakeServicioFavoritos(),
|
||||
radio: FakeServicioRadio(),
|
||||
servicioEcualizador: FakeServicioEcualizador(),
|
||||
resolverArchivoCustom: () async => espia,
|
||||
iniciarAutomaticamente: false,
|
||||
);
|
||||
|
||||
test(
|
||||
'fallo de IO al leer suprime la escritura y no se restaura con un '
|
||||
'alta explicita (D5 IO-fail)',
|
||||
() async {
|
||||
final espia = _ArchivoEspia(
|
||||
path: '/fake/emisoras_custom.json',
|
||||
exists: () async => true,
|
||||
readAsString:
|
||||
() async => throw const FileSystemException(
|
||||
'fallo simulado de lectura',
|
||||
),
|
||||
);
|
||||
final estado = EstadoRadio(
|
||||
audio: FakeServicioAudio(),
|
||||
favoritos: FakeServicioFavoritos(),
|
||||
radio: FakeServicioRadio(),
|
||||
servicioEcualizador: FakeServicioEcualizador(),
|
||||
resolverArchivoCustom: () async => espia,
|
||||
iniciarAutomaticamente: false,
|
||||
);
|
||||
await estado.inicializar();
|
||||
expect(estado.emisorasCustom, isEmpty);
|
||||
|
||||
await estado.inicializar();
|
||||
expect(estado.emisorasCustom, isEmpty);
|
||||
|
||||
await estado.agregarEmisoraCustom(
|
||||
emisoraDemo(uuid: 'nueva-x', nombre: 'X'),
|
||||
);
|
||||
|
||||
expect(
|
||||
estado.emisorasCustom.map((e) => e.uuid),
|
||||
contains('nueva-x'),
|
||||
);
|
||||
expect(espia.writeAsStringCalls, 0);
|
||||
},
|
||||
await estado.agregarEmisoraCustom(
|
||||
emisoraDemo(uuid: 'nueva-x', nombre: 'X'),
|
||||
);
|
||||
|
||||
test(
|
||||
'si ya existe un sidecar .corrupt no lo pisa, solo limpia el '
|
||||
'archivo vivo (D5)',
|
||||
() async {
|
||||
final archivo = await _crearArchivoCustomRaw('{bad-nuevo');
|
||||
final sidecar = File('${archivo.path}.corrupt');
|
||||
await sidecar.writeAsString('contenido-previo-X');
|
||||
expect(estado.emisorasCustom.map((e) => e.uuid), contains('nueva-x'));
|
||||
expect(espia.writeAsStringCalls, 0);
|
||||
});
|
||||
|
||||
final estado = EstadoRadio(
|
||||
audio: FakeServicioAudio(),
|
||||
favoritos: FakeServicioFavoritos(),
|
||||
radio: FakeServicioRadio(),
|
||||
servicioEcualizador: FakeServicioEcualizador(),
|
||||
resolverArchivoCustom: () async => archivo,
|
||||
iniciarAutomaticamente: false,
|
||||
);
|
||||
test('si ya existe un sidecar .corrupt no lo pisa, solo limpia el '
|
||||
'archivo vivo (D5)', () async {
|
||||
final archivo = await _crearArchivoCustomRaw('{bad-nuevo');
|
||||
final sidecar = File('${archivo.path}.corrupt');
|
||||
await sidecar.writeAsString('contenido-previo-X');
|
||||
|
||||
await estado.inicializar();
|
||||
|
||||
expect(await sidecar.readAsString(), 'contenido-previo-X');
|
||||
expect(await archivo.exists(), isFalse);
|
||||
expect(estado.emisorasCustom, isEmpty);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
group(
|
||||
'EstadoRadio — Android Auto: snapshot en vivo y reconciliación '
|
||||
'(android-auto-media)',
|
||||
() {
|
||||
test(
|
||||
'empuja un snapshot actualizado a la fuente registrada cuando '
|
||||
'cambian favoritos/custom/populares',
|
||||
() async {
|
||||
final fuenteAuto = _FuenteEmisorasAutoEspia();
|
||||
final archivo = await _crearArchivoCustom([
|
||||
emisoraDemo(uuid: 'custom-auto-1', nombre: 'Custom Auto'),
|
||||
]);
|
||||
final emisoraFav = emisoraDemo(uuid: 'fav-auto-1', nombre: 'Fav Auto');
|
||||
final estado = EstadoRadio(
|
||||
audio: FakeServicioAudio(),
|
||||
favoritos: FakeServicioFavoritos(),
|
||||
radio: FakeServicioRadio(
|
||||
populares: [emisoraDemo(uuid: 'pop-auto-1', nombre: 'Pop Auto')],
|
||||
),
|
||||
servicioEcualizador: FakeServicioEcualizador(),
|
||||
resolverArchivoCustom: () async => archivo,
|
||||
fuenteAuto: fuenteAuto,
|
||||
iniciarAutomaticamente: false,
|
||||
);
|
||||
|
||||
await estado.inicializar();
|
||||
|
||||
expect(
|
||||
fuenteAuto.ultimoMisEmisoras?.map((e) => e.uuid),
|
||||
contains('custom-auto-1'),
|
||||
);
|
||||
expect(
|
||||
fuenteAuto.ultimoTodas?.map((e) => e.uuid),
|
||||
contains('pop-auto-1'),
|
||||
);
|
||||
expect(
|
||||
fuenteAuto.ultimoGrupos?.map((g) => g.id),
|
||||
contains(GrupoFavoritos.sinAsignarId),
|
||||
);
|
||||
|
||||
await estado.toggleFavorito(emisoraFav);
|
||||
|
||||
expect(
|
||||
fuenteAuto.ultimoFavoritos?.map((e) => e.uuid),
|
||||
contains('fav-auto-1'),
|
||||
);
|
||||
},
|
||||
final estado = EstadoRadio(
|
||||
audio: FakeServicioAudio(),
|
||||
favoritos: FakeServicioFavoritos(),
|
||||
radio: FakeServicioRadio(),
|
||||
servicioEcualizador: FakeServicioEcualizador(),
|
||||
resolverArchivoCustom: () async => archivo,
|
||||
iniciarAutomaticamente: false,
|
||||
);
|
||||
|
||||
test(
|
||||
'reconcilia _emisoraSeleccionada cuando la selección viene desde '
|
||||
'el auto (no via reproducir())',
|
||||
() async {
|
||||
final audio = _AudioControlado();
|
||||
final estado = EstadoRadio(
|
||||
audio: audio,
|
||||
favoritos: FakeServicioFavoritos(),
|
||||
radio: FakeServicioRadio(),
|
||||
servicioEcualizador: FakeServicioEcualizador(),
|
||||
resolverArchivoCustom: _archivoCustomVacio,
|
||||
iniciarAutomaticamente: false,
|
||||
);
|
||||
await estado.inicializar();
|
||||
final desdeCoche = emisoraDemo(
|
||||
uuid: 'auto-selected',
|
||||
nombre: 'Desde el auto',
|
||||
);
|
||||
await estado.inicializar();
|
||||
|
||||
audio.seleccionarDesdeAuto(desdeCoche);
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
expect(await sidecar.readAsString(), 'contenido-previo-X');
|
||||
expect(await archivo.exists(), isFalse);
|
||||
expect(estado.emisorasCustom, isEmpty);
|
||||
});
|
||||
});
|
||||
|
||||
expect(estado.emisoraActual?.uuid, desdeCoche.uuid);
|
||||
},
|
||||
group('EstadoRadio — Android Auto: snapshot en vivo y reconciliación '
|
||||
'(android-auto-media)', () {
|
||||
test('empuja un snapshot actualizado a la fuente registrada cuando '
|
||||
'cambian favoritos/custom/populares', () async {
|
||||
final fuenteAuto = _FuenteEmisorasAutoEspia();
|
||||
final archivo = await _crearArchivoCustom([
|
||||
emisoraDemo(uuid: 'custom-auto-1', nombre: 'Custom Auto'),
|
||||
]);
|
||||
final emisoraFav = emisoraDemo(uuid: 'fav-auto-1', nombre: 'Fav Auto');
|
||||
final estado = EstadoRadio(
|
||||
audio: FakeServicioAudio(),
|
||||
favoritos: FakeServicioFavoritos(),
|
||||
radio: FakeServicioRadio(
|
||||
populares: [emisoraDemo(uuid: 'pop-auto-1', nombre: 'Pop Auto')],
|
||||
),
|
||||
servicioEcualizador: FakeServicioEcualizador(),
|
||||
resolverArchivoCustom: () async => archivo,
|
||||
fuenteAuto: fuenteAuto,
|
||||
iniciarAutomaticamente: false,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
await estado.inicializar();
|
||||
|
||||
expect(
|
||||
fuenteAuto.ultimoMisEmisoras?.map((e) => e.uuid),
|
||||
contains('custom-auto-1'),
|
||||
);
|
||||
expect(
|
||||
fuenteAuto.ultimoTodas?.map((e) => e.uuid),
|
||||
contains('pop-auto-1'),
|
||||
);
|
||||
expect(
|
||||
fuenteAuto.ultimoGrupos?.map((g) => g.id),
|
||||
contains(GrupoFavoritos.sinAsignarId),
|
||||
);
|
||||
|
||||
await estado.toggleFavorito(emisoraFav);
|
||||
|
||||
expect(
|
||||
fuenteAuto.ultimoFavoritos?.map((e) => e.uuid),
|
||||
contains('fav-auto-1'),
|
||||
);
|
||||
});
|
||||
|
||||
test('reconcilia _emisoraSeleccionada cuando la selección viene desde '
|
||||
'el auto (no via reproducir())', () async {
|
||||
final audio = _AudioControlado();
|
||||
final estado = EstadoRadio(
|
||||
audio: audio,
|
||||
favoritos: FakeServicioFavoritos(),
|
||||
radio: FakeServicioRadio(),
|
||||
servicioEcualizador: FakeServicioEcualizador(),
|
||||
resolverArchivoCustom: _archivoCustomVacio,
|
||||
iniciarAutomaticamente: false,
|
||||
);
|
||||
await estado.inicializar();
|
||||
final desdeCoche = emisoraDemo(
|
||||
uuid: 'auto-selected',
|
||||
nombre: 'Desde el auto',
|
||||
);
|
||||
|
||||
audio.seleccionarDesdeAuto(desdeCoche);
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
|
||||
expect(estado.emisoraActual?.uuid, desdeCoche.uuid);
|
||||
});
|
||||
});
|
||||
|
||||
group('EstadoRadio — última emisora reproducida (feedback-pruebas #4)', () {
|
||||
test('la última emisora reproducida sobrevive a una nueva instancia '
|
||||
'(reinicio) como emisoraActual DETENIDA -- no arranca audio, no '
|
||||
'reproduce, sólo queda seleccionada', () async {
|
||||
final emisora = emisoraDemo(uuid: 'last-1', nombre: 'Ultima FM');
|
||||
final estadoUno = EstadoRadio(
|
||||
audio: FakeServicioAudio(),
|
||||
favoritos: FakeServicioFavoritos(),
|
||||
radio: FakeServicioRadio(),
|
||||
servicioEcualizador: FakeServicioEcualizador(),
|
||||
resolverArchivoCustom: _archivoCustomVacio,
|
||||
iniciarAutomaticamente: false,
|
||||
);
|
||||
await estadoUno.inicializar();
|
||||
await estadoUno.reproducir(emisora);
|
||||
await estadoUno.detenerReproduccion();
|
||||
// Lets the fire-and-forget persistence write settle before
|
||||
// spinning up the "restart" instance.
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
|
||||
final audioDos = FakeServicioAudio();
|
||||
final estadoDos = EstadoRadio(
|
||||
audio: audioDos,
|
||||
favoritos: FakeServicioFavoritos(),
|
||||
radio: FakeServicioRadio(),
|
||||
servicioEcualizador: FakeServicioEcualizador(),
|
||||
resolverArchivoCustom: _archivoCustomVacio,
|
||||
iniciarAutomaticamente: false,
|
||||
);
|
||||
|
||||
await estadoDos.inicializar();
|
||||
|
||||
expect(estadoDos.emisoraActual?.uuid, emisora.uuid);
|
||||
expect(estadoDos.emisoraActual?.nombre, emisora.nombre);
|
||||
expect(
|
||||
audioDos.estaSonando,
|
||||
isFalse,
|
||||
reason: 'restoring the last station must never auto-start audio',
|
||||
);
|
||||
});
|
||||
|
||||
test('sin ninguna emisora previamente reproducida, emisoraActual sigue '
|
||||
'siendo null tras inicializar (instalación nueva)', () async {
|
||||
final estado = EstadoRadio(
|
||||
audio: FakeServicioAudio(),
|
||||
favoritos: FakeServicioFavoritos(),
|
||||
radio: FakeServicioRadio(),
|
||||
servicioEcualizador: FakeServicioEcualizador(),
|
||||
resolverArchivoCustom: _archivoCustomVacio,
|
||||
iniciarAutomaticamente: false,
|
||||
);
|
||||
|
||||
await estado.inicializar();
|
||||
|
||||
expect(estado.emisoraActual, isNull);
|
||||
});
|
||||
|
||||
test('una emisora seleccionada desde el auto (fuera de reproducir()) '
|
||||
'también se recuerda para la próxima instancia', () async {
|
||||
final audio = _AudioControlado();
|
||||
final estadoUno = EstadoRadio(
|
||||
audio: audio,
|
||||
favoritos: FakeServicioFavoritos(),
|
||||
radio: FakeServicioRadio(),
|
||||
servicioEcualizador: FakeServicioEcualizador(),
|
||||
resolverArchivoCustom: _archivoCustomVacio,
|
||||
iniciarAutomaticamente: false,
|
||||
);
|
||||
await estadoUno.inicializar();
|
||||
final desdeCoche = emisoraDemo(
|
||||
uuid: 'auto-remembered',
|
||||
nombre: 'Recordada desde el auto',
|
||||
);
|
||||
audio.seleccionarDesdeAuto(desdeCoche);
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
|
||||
final estadoDos = EstadoRadio(
|
||||
audio: FakeServicioAudio(),
|
||||
favoritos: FakeServicioFavoritos(),
|
||||
radio: FakeServicioRadio(),
|
||||
servicioEcualizador: FakeServicioEcualizador(),
|
||||
resolverArchivoCustom: _archivoCustomVacio,
|
||||
iniciarAutomaticamente: false,
|
||||
);
|
||||
await estadoDos.inicializar();
|
||||
|
||||
expect(estadoDos.emisoraActual?.uuid, desdeCoche.uuid);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Spy [FuenteEmisorasAuto] that only records the last snapshot pushed by
|
||||
|
||||
Reference in New Issue
Block a user