281 lines
12 KiB
Markdown
281 lines
12 KiB
Markdown
# Llevar PluriWave a Android Auto
|
||
|
||
Guía para un desarrollador que **nunca publicó una app en Play Store** y quiere que
|
||
PluriWave —instalada en el teléfono— se pueda **navegar y controlar desde la pantalla
|
||
del coche** vía Android Auto.
|
||
|
||
> **Alcance de esta guía.** Hablamos de **Android Auto proyectado**: la app corre en el
|
||
> móvil y se proyecta al coche. NO es *Android Automotive OS* (donde la app se instala
|
||
> dentro del sistema del vehículo). PluriWave es una app de audio → categoría
|
||
> **"media app"**. Todo lo de abajo es para esa combinación.
|
||
|
||
---
|
||
|
||
## TL;DR (lo importante primero)
|
||
|
||
1. **El 60% ya está hecho.** PluriWave usa `audio_service`, que ya expone un
|
||
`MediaBrowserService` + `MediaSession` (lo que Android Auto exige). No hay que
|
||
reescribir el motor de audio.
|
||
2. **Falta lo que un coche necesita de más que un teléfono:** un **árbol navegable**
|
||
de emisoras (para que el coche muestre una lista) y una **declaración en el manifest**
|
||
para que Android Auto descubra la app.
|
||
3. **Trabajo real de código:** ~1 archivo XML nuevo + 1 línea en el manifest +
|
||
implementar 3 métodos en `PluriWaveAudioHandler` (`getChildren`, `getMediaItem`,
|
||
`playFromMediaId`).
|
||
4. **Publicación:** Android Auto añade una **revisión extra de Google** contra las
|
||
*car app quality guidelines*. Es más estricta y más lenta que la de una app normal.
|
||
|
||
Tiempo realista para un principiante: **1–2 semanas** (código un par de días, el resto
|
||
es testing con el emulador de coche y la revisión de Google).
|
||
|
||
---
|
||
|
||
## Parte 0 — Cómo funciona (modelo mental)
|
||
|
||
Un coche con Android Auto **no ejecuta tu UI de Flutter**. En su lugar, le pide a tu app
|
||
dos cosas a través de un servicio estándar de Android:
|
||
|
||
| El coche pregunta | Tu app responde | En Android esto es |
|
||
|-------------------|-----------------|--------------------|
|
||
| "¿Qué contenido tenés para mostrar?" | Una lista de ítems (carpetas + emisoras) | `MediaBrowserService` → `getChildren()` |
|
||
| "El usuario tocó ESTE ítem, reproducilo" | Arrancás el stream | `MediaSession` → `playFromMediaId()` |
|
||
| "Mostrame play/pausa/título/carátula" | El estado actual | `PlaybackState` + `MediaItem` |
|
||
|
||
`audio_service` implementa el `MediaBrowserService` y el `MediaSession` por vos. Tu único
|
||
trabajo es **rellenar las respuestas** (la lista de emisoras y cómo reproducir cada una).
|
||
|
||
La UI del coche la dibuja **Android Auto**, no vos. Vos solo aportás datos y audio.
|
||
|
||
---
|
||
|
||
## Parte 1 — Qué ya tiene PluriWave (punto de partida)
|
||
|
||
Verificado en el código actual:
|
||
|
||
| Pieza | Dónde | Estado |
|
||
|-------|-------|--------|
|
||
| Dependencia `audio_service` `^0.18.15` | `pubspec.yaml` | ✅ |
|
||
| `MediaBrowserService` declarado en el manifest | `android/app/src/main/AndroidManifest.xml:47-54` | ✅ |
|
||
| `MediaButtonReceiver` (controles físicos/notificación) | `AndroidManifest.xml:62-68` | ✅ |
|
||
| Inicialización del handler | `lib/main.dart:38` (`AudioService.init`) | ✅ |
|
||
| Config del servicio | `lib/main.dart:21` (`AudioServiceConfig`) | ✅ |
|
||
| Handler propio | `lib/servicios/servicio_audio.dart:127` (`PluriWaveAudioHandler extends BaseAudioHandler`) | ✅ |
|
||
| Reproducir un ítem | `servicio_audio.dart:433` (`playMediaItem`) | ✅ |
|
||
| Mapear `MediaItem` ↔ `Emisora` | `servicio_audio.dart:705` (`_emisoraDesdeMediaItem`) | ✅ |
|
||
| `foregroundServiceType="mediaPlayback"` | `AndroidManifest.xml:49` | ✅ |
|
||
|
||
**Ventaja clave de la versión 0.18:** el handler corre en el **mismo isolate** que la app,
|
||
así que `getChildren()` puede leer directamente tu lista de emisoras/favoritos del estado
|
||
de la app. No hay que sincronizar entre isolates.
|
||
|
||
### Lo que NO está (el hueco a rellenar)
|
||
|
||
| Falta | Consecuencia hoy |
|
||
|-------|------------------|
|
||
| `res/xml/automotive_app_desc.xml` | Android Auto **no descubre** la app |
|
||
| `<meta-data com.google.android.gms.car.application>` en el manifest | idem |
|
||
| Override de `getChildren()` / `getMediaItem()` | El coche no tiene **ninguna lista** que mostrar |
|
||
| Override de `playFromMediaId()` | Tocar una emisora en el coche **no reproduce** nada |
|
||
| `MediaItem`s con carátula (`artUri`) por emisora | Google **rechaza** apps de media sin título+thumbnail por ítem |
|
||
|
||
Hoy PluriWave solo sabe reproducir un `MediaItem` que le pasa **su propia UI de Flutter**
|
||
(`playMediaItem`). El coche necesita el camino inverso: **pedir la lista** y **arrancar por id**.
|
||
|
||
---
|
||
|
||
## Parte 2 — Quick path (los pasos, en orden)
|
||
|
||
### Paso 1 · Declarar la app ante Android Auto
|
||
|
||
Crear `android/app/src/main/res/xml/automotive_app_desc.xml`:
|
||
|
||
```xml
|
||
<automotiveApp>
|
||
<uses name="media"/>
|
||
</automotiveApp>
|
||
```
|
||
|
||
Añadir dentro de `<application>` en `AndroidManifest.xml` (junto al resto de `<meta-data>`):
|
||
|
||
```xml
|
||
<meta-data
|
||
android:name="com.google.android.gms.car.application"
|
||
android:resource="@xml/automotive_app_desc"/>
|
||
```
|
||
|
||
> Con esto Android Auto ya "ve" la app, pero seguirá vacía hasta el Paso 2.
|
||
|
||
### Paso 2 · Construir el árbol navegable (el trabajo de fondo)
|
||
|
||
En `PluriWaveAudioHandler` (`lib/servicios/servicio_audio.dart`) implementar estos métodos.
|
||
Firmas **verificadas** contra el código de `audio_service`:
|
||
|
||
```dart
|
||
// Devuelve los hijos de una "carpeta". El root usa AudioService.browsableRootId.
|
||
Future<List<MediaItem>> getChildren(String parentMediaId,
|
||
[Map<String, dynamic>? options]);
|
||
|
||
// Metadatos de un ítem concreto (por si el coche los pide sueltos).
|
||
Future<MediaItem?> getMediaItem(String mediaId);
|
||
|
||
// El usuario tocó un ítem en la pantalla del coche → reproducirlo.
|
||
Future<void> playFromMediaId(String mediaId, [Map<String, dynamic>? extras]);
|
||
|
||
// (Opcional) búsqueda por voz "pon Radio X".
|
||
Future<List<MediaItem>> playFromSearch(String query, [Map<String, dynamic>? extras]);
|
||
```
|
||
|
||
Diseño de árbol sugerido para PluriWave:
|
||
|
||
```
|
||
root (AudioService.browsableRootId)
|
||
├── Favoritos (playable: false → carpeta)
|
||
│ ├── Emisora A (playable: true)
|
||
│ └── Emisora B (playable: true)
|
||
├── Todas las emisoras (playable: false)
|
||
│ └── ...
|
||
└── Mis emisoras (playable: false) // las custom del usuario
|
||
└── ...
|
||
```
|
||
|
||
Reglas de un `MediaItem`:
|
||
|
||
| Campo | Carpeta | Emisora reproducible |
|
||
|-------|---------|----------------------|
|
||
| `id` | id estable de la categoría | id estable de la emisora |
|
||
| `title` | nombre visible | nombre de la emisora **(obligatorio)** |
|
||
| `playable` | `false` | `true` |
|
||
| `artUri` | opcional | **carátula/logo (obligatorio para pasar la revisión)** |
|
||
|
||
Lógica a reutilizar: ya tenés `_emisoraDesdeMediaItem` (`servicio_audio.dart:705`) y
|
||
`playMediaItem` (`servicio_audio.dart:433`). En `playFromMediaId(id)` resolvés el id →
|
||
`Emisora` → construís el `MediaItem` real → llamás al mismo `playMediaItem` interno. **No
|
||
dupliques la lógica de reproducción**, enchufala.
|
||
|
||
### Paso 3 · Carátulas accesibles
|
||
|
||
`artUri` tiene que ser una URL/҇URI que el sistema pueda cargar (http(s) o `content://`).
|
||
Las emisoras que ya tienen logo remoto sirven directo. Para emisoras sin logo, definí una
|
||
carátula por defecto (asset empaquetado servido vía `content://` o un placeholder remoto).
|
||
|
||
### Paso 4 · (Opcional pero recomendado) Content style
|
||
|
||
Android Auto puede pintar los ítems como **lista** o **grid**. Se controla con hints en el
|
||
`extras`/config del root (constantes `CONTENT_STYLE_*` de la spec de MediaBrowser).
|
||
Para una app de radio, **grid** para emisoras (se ven los logos) queda mejor. Es pulido,
|
||
no bloquea la publicación.
|
||
|
||
---
|
||
|
||
## Parte 3 · Probar sin coche (DHU — Desktop Head Unit)
|
||
|
||
No necesitás un coche para testear. Google da un emulador de la pantalla del coche.
|
||
|
||
**Quick path del testeo:**
|
||
|
||
1. En **Android Studio → SDK Manager → SDK Tools**, instalá **Android Auto Desktop Head Unit**.
|
||
2. En el **teléfono**: instalá la app *Android Auto*, entrá en sus ajustes y tocá 10 veces
|
||
la versión para activar **modo desarrollador**; ahí activá **"Head unit server"**.
|
||
3. Conectá el teléfono por USB y lanzá el DHU:
|
||
```bash
|
||
cd "$ANDROID_HOME/extras/google/auto"
|
||
./desktop-head-unit # (desktop-head-unit.exe en Windows)
|
||
```
|
||
4. En la ventana del DHU deberías ver PluriWave en la sección de **media**. Navegá el árbol
|
||
y reproducí una emisora.
|
||
|
||
**Checklist de humo en el DHU:**
|
||
|
||
- [ ] La app aparece en la lista de apps de media del coche.
|
||
- [ ] Se ve el árbol (Favoritos / Todas / Mis emisoras).
|
||
- [ ] Cada emisora muestra **título + carátula**.
|
||
- [ ] Tocar una emisora **arranca el audio**.
|
||
- [ ] Play / pausa / stop responden desde la pantalla del coche.
|
||
- [ ] Al pausar en el coche, la app del teléfono refleja el mismo estado (y viceversa).
|
||
|
||
---
|
||
|
||
## Parte 4 · Publicar en Play Store (lo específico de un primer publicador)
|
||
|
||
Publicar una app **con Android Auto** no es igual que una app normal: dispara una
|
||
**revisión adicional** de Google contra las *car app quality guidelines*.
|
||
|
||
### Requisitos de calidad que Google verifica (media apps)
|
||
|
||
| Requisito | Qué significa para PluriWave |
|
||
|-----------|------------------------------|
|
||
| Integración con MediaSession | Ya lo da `audio_service` ✅ |
|
||
| Soportar play/pausa **o** stop | Ya lo tenés ✅ |
|
||
| **Título + thumbnail por cada ítem** | ← esto es lo que hay que asegurar (Paso 3) |
|
||
| Poder llegar a la vista de reproducción desde el browsing | Se cumple con el árbol bien armado |
|
||
| **Al menos 1 screenshot real, sin editar, de la experiencia en coche** | Sacala del DHU |
|
||
|
||
### Pasos en Play Console
|
||
|
||
1. **Cuenta de desarrollador** (pago único de ~25 USD, primera vez).
|
||
2. Subí primero a un **track de pruebas cerrado**, NO directo a producción.
|
||
> Importante: si el build va en un track de **testing** y no cumple, Google te avisa
|
||
> pero **igual lo aprueba** para ese track. Si el mismo build va a **producción** y no
|
||
> cumple, lo **rechaza**. Por eso: cerrado → arreglás → producción.
|
||
3. Completá la **ficha de la tienda** + el cuestionario de contenido/privacidad
|
||
(obligatorio para cualquier app nueva).
|
||
4. Subí la **screenshot de la experiencia en coche** (del DHU).
|
||
5. Enviá a revisión. La revisión de coche puede tardar **de unas horas hasta 7 días**
|
||
(a veces más), bastante más que una app solo-móvil.
|
||
|
||
### Gotchas para PluriWave concretamente
|
||
|
||
- **Muchos permisos sensibles.** El manifest pide localización, `RECORD_AUDIO`,
|
||
`SCHEDULE_EXACT_ALARM`, `SYSTEM_EXEMPTED`, etc. La revisión de coche mira con lupa; tené
|
||
a mano la justificación de cada permiso (la sección de *foreground service* de Play
|
||
Console te va a pedir el porqué de `mediaPlayback`).
|
||
- **`targetSdk`** sale de `flutter.targetSdkVersion` (`android/app/build.gradle`). Play
|
||
exige un target reciente para apps nuevas; verificá que cumpla el mínimo del año antes de
|
||
subir.
|
||
- **Streams que fallan.** Google prueba reproducir. Si una emisora del árbol está caída, da
|
||
mala impresión. Exponé en el árbol emisoras fiables (favoritos del usuario, o un set
|
||
curado) y manejá el error de stream con gracia (ya tenés `controlador_reconexion.dart`).
|
||
|
||
---
|
||
|
||
## Checklist maestro
|
||
|
||
**Código**
|
||
- [ ] `res/xml/automotive_app_desc.xml` creado (`<uses name="media"/>`).
|
||
- [ ] `<meta-data com.google.android.gms.car.application>` en el manifest.
|
||
- [ ] `getChildren()` devuelve el árbol (carpetas + emisoras).
|
||
- [ ] `getMediaItem()` resuelve un id suelto.
|
||
- [ ] `playFromMediaId()` reutiliza `playMediaItem` interno.
|
||
- [ ] Cada emisora expone `title` + `artUri`.
|
||
|
||
**Testing**
|
||
- [ ] Funciona en el DHU (navegar + reproducir + play/pausa).
|
||
- [ ] Estado sincronizado coche ↔ teléfono.
|
||
|
||
**Publicación**
|
||
- [ ] Cuenta de desarrollador creada.
|
||
- [ ] Screenshot de la experiencia en coche subida.
|
||
- [ ] Subido primero a track cerrado.
|
||
- [ ] Justificación de permisos preparada.
|
||
- [ ] Enviado a revisión.
|
||
|
||
---
|
||
|
||
## Referencias
|
||
|
||
- [Media apps for cars — overview (Android Developers)](https://developer.android.com/training/cars/media)
|
||
- [Add support for Android Auto to your media app](https://developer.android.com/training/cars/media/auto)
|
||
- [Car app quality guidelines](https://developer.android.com/docs/quality-guidelines/car-app-quality)
|
||
- [Distribute to cars (Play Console)](https://developer.android.com/training/cars/distribute)
|
||
- [audio_service (pub.dev)](https://pub.dev/packages/audio_service)
|
||
- [audio_service — repo y ejemplos (GitHub)](https://github.com/ryanheise/audio_service)
|
||
|
||
---
|
||
|
||
## Próximo paso sugerido
|
||
|
||
Empezar por el **Paso 1 + Paso 2 con un árbol mínimo** (solo "Favoritos" con 2–3 emisoras
|
||
hardcodeadas) y verlo en el **DHU**. Cuando eso reproduzca en el emulador de coche, recién
|
||
ahí ampliar el árbol y pulir carátulas. Es el bucle de feedback más corto para no
|
||
programar a ciegas.
|