fix(auto): fall back to on-brand artwork when a station or track has none

Stations and tracks with no artwork showed empty tiles in the car. The
browse tree's itemEmisora/_itemLocal already fell back to the rotating
station_art_* drawable via artUriPara/artUriLocal, but the "now playing"
MediaItem built when actually playing something (car tap, phone-initiated
play, folder-queue advance, direct local-track tap) did not, so the car's
now-playing screen still went blank.

Reuse the SAME artUriPara/artUriLocal fallback (already the project's one
selection scheme, mirroring PluriStationArtFallback) at every "now playing"
construction site: reproducirPorMediaId, ServicioAudio.reproducir (now via
the extracted, unit-tested mediaItemParaEmisora), construirMediaItemColaLocal
and reproducirPistaLocal.

Guard the reverse direction too: emisoraDesdeMediaItem (extracted from the
handler's private method, now unit-tested) only reflects artUri back into
Emisora.favicon when it passes faviconUsable, so the phone UI's
CachedNetworkImage widgets never attempt a doomed fetch of the car's
android.resource:// fallback URI -- they keep falling back to
PluriStationArtFallback exactly as before.
This commit is contained in:
2026-07-31 00:47:26 +02:00
parent 6822432a51
commit 1b0bea5492
4 changed files with 298 additions and 25 deletions
+98
View File
@@ -2189,6 +2189,28 @@ void main() {
},
);
test(
'item 3: el MediaItem de una pista local tocada directamente usa el '
'fallback de marca (artUriLocal), no queda sin artUri',
() async {
final fuente = _FakeFuenteMusicaLocalAuto(
uriPorDocId: const {'doc1': 'content://provider/doc1'},
);
MediaItem? recibido;
await reproducirPistaLocal(
'pista:doc1',
fuente: fuente,
reproducir: (item) async {
recibido = item;
},
);
expect(recibido!.artUri, isNotNull);
expect(recibido!.artUri.toString(), artUriLocal('doc1'));
},
);
test(
'id obsoleto/desconocido (uriContenidoDePista devuelve null) no '
'llama a reproducir ni lanza excepción',
@@ -2494,6 +2516,27 @@ void main() {
expect(item, isNull);
},
);
test(
'item 3: el MediaItem de una pista en cola de "Reproducir carpeta" '
'usa el fallback de marca (artUriLocal), no queda sin artUri (antes '
'una pista en cola no tenía NINGÚN arte)',
() async {
final fuente = _FakeFuenteMusicaLocalAuto(
uriPorDocId: const {'doc1': 'content://provider/doc1'},
);
const nodo = NodoLocal(
documentId: 'doc1',
nombre: 'ignorado.mp3',
esDirectorio: false,
);
final item = await construirMediaItemColaLocal(nodo, fuente: fuente);
expect(item!.artUri, isNotNull);
expect(item.artUri.toString(), artUriLocal('doc1'));
},
);
});
group('ConstructorArbolAuto.itemEmisora', () {
@@ -2866,6 +2909,61 @@ void main() {
expect(llamadas, 0);
},
);
test(
'item 3: emisora SIN favicon usable -- el MediaItem "en reproducción" '
'usa el fallback de marca (artUriPara), no queda con artUri null '
'(antes se perdía el arte al reproducir desde el auto)',
() async {
final emisora = _emisora(
uuid: 'uuid-sin-arte',
nombre: 'Radio sin logo',
favicon: null,
);
final fuente = _FakeFuenteEmisorasAuto(
porUuidResultado: {emisora.uuid: emisora},
);
MediaItem? recibido;
await reproducirPorMediaId(
'emisora:${emisora.uuid}',
fuente: fuente,
reproducir: (item) async {
recibido = item;
},
);
expect(recibido, isNotNull);
expect(recibido!.artUri, isNotNull);
expect(recibido!.artUri.toString(), artUriPara(emisora));
},
);
test(
'item 3: emisora CON favicon http(s) usable -- el MediaItem "en '
'reproducción" sigue usando ese favicon real, no el fallback',
() async {
final emisora = _emisora(
uuid: 'uuid-con-arte',
nombre: 'Radio con logo',
favicon: 'https://cdn.example.com/logo.png',
);
final fuente = _FakeFuenteEmisorasAuto(
porUuidResultado: {emisora.uuid: emisora},
);
MediaItem? recibido;
await reproducirPorMediaId(
'emisora:${emisora.uuid}',
fuente: fuente,
reproducir: (item) async {
recibido = item;
},
);
expect(recibido!.artUri.toString(), emisora.favicon);
},
);
});
// ── Item 1: Android Auto subfolder visibility ───────────────────────────