feat(auto): browsable Android Auto media tree with play-by-id [size:exception]
Expose PluriWave to Android Auto (projected) as a media app: - Declare car media support (automotive_app_desc.xml + manifest meta-data) so Android Auto discovers the existing MediaBrowserService. - New navegacion_auto.dart: ConstructorArbolAuto builds the browse tree (Favoritos / Todas las emisoras / Mis emisoras, 50-item cap, stable emisora:<id> media ids), reproducirPorMediaId routes a car tap to the existing playMediaItem pipeline, FuenteEmisorasAutoLocal serves the tree cold-start-safe (local favorites/custom stations before Flutter UI runs). - PluriWaveAudioHandler overrides getChildren/getMediaItem/playFromMediaId as thin delegations; playback pipeline untouched. - EstadoRadio pushes live station snapshots to the browse source and reconciles the selected station when playback starts from the car. - Every playable item ships title + artUri; stations without logo fall back to a bundled default art (android.resource://). Tests: 52/52 green (10 new navegacion_auto, 2 new estado_radio, plus audio safety-net suites). Handler overrides and native XML are static-review-only (no Android build env). Size exception approved for a single reviewable commit.
This commit is contained in:
@@ -11,6 +11,7 @@ import '../l10n/gen/app_localizations.dart';
|
||||
import '../modelos/emisora.dart';
|
||||
import '../modelos/preset_ecualizador.dart';
|
||||
import 'controlador_reconexion.dart';
|
||||
import 'navegacion_auto.dart';
|
||||
import 'servicio_audio_session.dart';
|
||||
|
||||
/// Estado de reproducción expuesto al UI.
|
||||
@@ -35,6 +36,16 @@ void registrarHandler(PluriWaveAudioHandler handler) {
|
||||
_handlerGlobal = handler;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Android Auto browse source — registered from main.dart, mirrors
|
||||
// registrarHandler above (Design "getChildren data source registration").
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
FuenteEmisorasAuto? _fuenteNavegacionGlobal;
|
||||
|
||||
void registrarFuenteNavegacion(FuenteEmisorasAuto fuente) {
|
||||
_fuenteNavegacionGlobal = fuente;
|
||||
}
|
||||
|
||||
/// Wrapper de alto nivel para el UI.
|
||||
class ServicioAudio {
|
||||
PluriWaveAudioHandler get _handler {
|
||||
@@ -712,4 +723,84 @@ class PluriWaveAudioHandler extends BaseAudioHandler
|
||||
favicon: mediaItem.artUri?.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
// ── Android Auto browsing (thin delegation to navegacion_auto.dart's
|
||||
// already-tested pure logic — Design "getChildren data source") ─────────
|
||||
|
||||
@override
|
||||
Future<List<MediaItem>> getChildren(
|
||||
String parentMediaId, [
|
||||
Map<String, dynamic>? options,
|
||||
]) async {
|
||||
try {
|
||||
final constructor = ConstructorArbolAuto();
|
||||
if (parentMediaId == AudioService.browsableRootId) {
|
||||
return constructor.raiz();
|
||||
}
|
||||
final fuente = _fuenteNavegacionGlobal;
|
||||
if (fuente == null) return const [];
|
||||
final emisoras = await _listaParaCarpeta(fuente, parentMediaId);
|
||||
return constructor.hijos(parentMediaId, emisoras: emisoras);
|
||||
} catch (_) {
|
||||
// Spec "Browse requested before app state is loaded": never throw out
|
||||
// of a browse call, even on an unexpected failure.
|
||||
return const [];
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MediaItem?> getMediaItem(String mediaId) async {
|
||||
try {
|
||||
final fuente = _fuenteNavegacionGlobal;
|
||||
if (fuente == null) return null;
|
||||
final universo = await _universoCompleto(fuente);
|
||||
final constructor = ConstructorArbolAuto();
|
||||
final emisora = constructor.resolver(mediaId, universo);
|
||||
return emisora == null ? null : constructor.itemEmisora(emisora);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> playFromMediaId(
|
||||
String mediaId, [
|
||||
Map<String, dynamic>? extras,
|
||||
]) async {
|
||||
final fuente = _fuenteNavegacionGlobal;
|
||||
if (fuente == null) return;
|
||||
try {
|
||||
await reproducirPorMediaId(
|
||||
mediaId,
|
||||
fuente: fuente,
|
||||
reproducir: playMediaItem,
|
||||
);
|
||||
} catch (e) {
|
||||
// Spec "Unknown or stale media id": never propagate from the handler.
|
||||
developer.log(
|
||||
'[PluriWave] Error en playFromMediaId($mediaId): $e',
|
||||
name: 'ServicioAudio',
|
||||
level: 900,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Emisora>> _listaParaCarpeta(
|
||||
FuenteEmisorasAuto fuente,
|
||||
String parentId,
|
||||
) => switch (parentId) {
|
||||
ConstructorArbolAuto.idFavoritos => fuente.favoritos(),
|
||||
ConstructorArbolAuto.idMisEmisoras => fuente.misEmisoras(),
|
||||
ConstructorArbolAuto.idTodas => fuente.todas(),
|
||||
_ => Future.value(const []),
|
||||
};
|
||||
|
||||
Future<List<Emisora>> _universoCompleto(FuenteEmisorasAuto fuente) async {
|
||||
final listas = await Future.wait([
|
||||
fuente.favoritos(),
|
||||
fuente.misEmisoras(),
|
||||
fuente.todas(),
|
||||
]);
|
||||
return listas.expand((lista) => lista).toList();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user