docs(auto): android auto research guide and sdd artifacts for android-auto-media
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
# Apply Progress: android-auto-media
|
||||
|
||||
**Batch**: 1 of 1 (all 31 tasks attempted in a single batch)
|
||||
**Mode**: Strict TDD (Dart layers) + static-review-only (Kotlin/manifest/XML/PNG, per tasks.md)
|
||||
|
||||
## Summary
|
||||
|
||||
All tasks implemented. 31/31 marked `[x]` in `tasks.md`. New/changed test files: 31 targeted tests green (10 in `navegacion_auto_test.dart`, 21 in `estado_radio_test.dart`), plus 21 pre-existing tests in touched sibling files re-verified green as a safety net.
|
||||
|
||||
## TDD Cycle Evidence
|
||||
|
||||
| Task | Test File | Layer | Safety Net | RED | GREEN | TRIANGULATE | REFACTOR |
|
||||
|------|-----------|-------|------------|-----|-------|-------------|----------|
|
||||
| 1.1 | `test/servicios/navegacion_auto_test.dart` | Unit | N/A (new file) | ✅ Written | ✅ Passed | ➖ Structural | ➖ None needed |
|
||||
| 1.2–1.3 | `navegacion_auto_test.dart` (raiz) | Unit | N/A (new) | ✅ Written | ✅ Passed | ➖ Single scenario (3-folder shape is fixed) | ✅ Clean |
|
||||
| 1.4–1.6 | `navegacion_auto_test.dart` (itemEmisora) | Unit | N/A (new) | ✅ Written | ✅ Passed | ✅ 3 cases (remote favicon, null favicon, empty favicon) | ✅ Clean |
|
||||
| 1.7–1.8 | `navegacion_auto_test.dart` (hijos) | Unit | N/A (new) | ✅ Written | ✅ Passed | ✅ 3 cases (60→50 cap+order, empty, unknown parentId) | ✅ Clean |
|
||||
| 1.9–1.10 | `navegacion_auto_test.dart` (resolver) | Unit | N/A (new) | ✅ Written | ✅ Passed | ✅ 4 cases (known, no-prefix, empty-uuid, unmatched) | ✅ Clean |
|
||||
| 1.11–1.12 | `navegacion_auto_test.dart` (reproducirPorMediaId) | Unit | N/A (new) | ✅ Written | ✅ Passed | ✅ 2 cases (known uuid delegates, unknown uuid no-ops) | ✅ Clean |
|
||||
| 1.13 | `navegacion_auto_test.dart` (full file) | Unit | ✅ 10/10 before+after | — | — | — | ✅ Naming/doc pass, no duplicated art-fallback logic |
|
||||
| 2.1–2.2 | — (static-review-only per tasks.md; covered indirectly by Phase 1 fakes exercising the interface contract) | N/A | N/A | N/A | N/A | N/A | N/A |
|
||||
| 3.1–3.4 | — (static-review-only per tasks.md; thin delegation, verified by compiling through `servicio_audio_*_test.dart` safety-net runs) | N/A | ✅ 19/19 before, 19/19 after | N/A | N/A | N/A | N/A |
|
||||
| 4.1–4.2 | `test/estado/estado_radio_test.dart` (snapshot push) | Unit | ✅ 19/19 baseline | ✅ Written | ✅ Passed | ➖ Single integration scenario across 3 mutation sites (favorites/custom/populares) | ✅ Clean |
|
||||
| 4.3–4.4 | `estado_radio_test.dart` (reconcile) | Unit | ✅ 19/19 baseline | ✅ Written | ✅ Passed | ➖ Single scenario (car-initiated selection) | ✅ Clean |
|
||||
| 4.5 | `estado_radio_test.dart` (full file) | Unit | ✅ 21/21 before+after | — | — | — | ✅ Doc comments added, no behavior change |
|
||||
| 5.1 | — (static-review-only per tasks.md; no `main.dart`/`app.dart` test pattern exists) | N/A | N/A | N/A | N/A | N/A | N/A |
|
||||
| 6.1–6.3 | — (static-review-only per tasks.md; no Android build env) | N/A | N/A | N/A | N/A | N/A | N/A |
|
||||
| 7.1 | Targeted run (see deviation note in tasks.md) | Unit | — | — | ✅ 52/52 combined | — | — |
|
||||
| 7.2 | Manual static review (see deviation note in tasks.md) | — | — | — | — | — | — |
|
||||
| 7.3 | This note | — | — | — | — | — | — |
|
||||
|
||||
### Test Summary
|
||||
- **Total tests written**: 12 (10 in `navegacion_auto_test.dart` + 2 in `estado_radio_test.dart`)
|
||||
- **Total tests passing**: 52 (31 new/touched + 21 pre-existing safety-net, all green)
|
||||
- **Layers used**: Unit (12 new)
|
||||
- **Approval tests**: None — no refactoring-of-existing-behavior tasks (4.1–4.4 add new optional-param behavior, existing callers unaffected: `fuenteAuto` defaults to `null`)
|
||||
- **Pure functions created**: `ConstructorArbolAuto.raiz/hijos/itemEmisora/resolver`, `reproducirPorMediaId` — all pure or side-effect-isolated via injected callback
|
||||
|
||||
## Files Changed
|
||||
|
||||
| File | Action | What Was Done |
|
||||
|------|--------|----------------|
|
||||
| `lib/servicios/navegacion_auto.dart` | Created | `FuenteEmisorasAuto` interface (+ default no-op `actualizarSnapshot`), `ConstructorArbolAuto` pure builder, `reproducirPorMediaId` routing seam, `FuenteEmisorasAutoLocal` cold-start-safe data source |
|
||||
| `lib/servicios/servicio_audio.dart` | Modified | `registrarFuenteNavegacion()` + module field (mirrors `registrarHandler`); `PluriWaveAudioHandler` overrides `getChildren`/`getMediaItem`/`playFromMediaId`, all delegating to `navegacion_auto.dart`'s tested pure logic, all swallowing errors per spec |
|
||||
| `lib/estado/estado_radio.dart` | Modified | Optional `fuenteAuto` ctor param; pushes live snapshot at 5 `notifyListeners()` sites across `cargarPopulares`, `cargarFavoritos`, `_cargarEmisorasCustom`/`_leerContenidoCustom`; reconciles `_emisoraSeleccionada` in `_escucharErroresReproduccion` |
|
||||
| `lib/main.dart` | Modified | Builds `FuenteEmisorasAutoLocal`, calls `registrarFuenteNavegacion`, passes it to `PluriWaveApp` |
|
||||
| `lib/app.dart` | Modified | `PluriWaveApp` accepts optional `fuenteAuto`, forwards it to `EstadoRadio` |
|
||||
| `android/app/src/main/res/xml/automotive_app_desc.xml` | Created | `<automotiveApp><uses name="media"/></automotiveApp>` |
|
||||
| `android/app/src/main/AndroidManifest.xml` | Modified | Added `com.google.android.gms.car.application` meta-data inside `<application>` |
|
||||
| `android/app/src/main/res/drawable/default_station_art.png` | Created | 192×192 PNG copied from `mipmap-xxxhdpi/ic_launcher.png` (no Android build env to generate a bespoke asset — see Deviations) |
|
||||
| `test/servicios/navegacion_auto_test.dart` | Created | 10 tests: raiz, itemEmisora (favicon + fallback), hijos (cap/order/empty/unknown), resolver (known/malformed/unmatched), reproducirPorMediaId (known/unknown) |
|
||||
| `test/estado/estado_radio_test.dart` | Modified | Added `_FuenteEmisorasAutoEspia` spy, `_AudioControlado.seleccionarDesdeAuto()` helper, 2 new tests (snapshot push, reconciliation) |
|
||||
|
||||
## Deviations from Design
|
||||
|
||||
1. **`actualizarSnapshot` promoted to the abstract `FuenteEmisorasAuto` interface** (design's code block only listed it as `FuenteEmisorasAutoLocal`-specific). Necessary because task 4.2 requires `EstadoRadio` to call `fuenteAuto?.actualizarSnapshot(...)` through the `FuenteEmisorasAuto?`-typed field — Dart's `implements` contract requires the method on the interface for that call to type-check. Given a default no-op body on the abstract class, this is additive and behavior-neutral for any implementation that doesn't need it.
|
||||
2. **Default artwork PNG is a placeholder**, not a bespoke "no logo" graphic: copied from the existing `mipmap-xxxhdpi/ic_launcher.png` launcher asset since there is no Android build/design-asset environment available here. Filename/path/dimensions are correct and Android-loadable; the actual pixel content should be swapped for a proper station-art placeholder before shipping (noted in task 6.3 static review).
|
||||
3. **Content-style extra keys** use the documented Android Auto browse hint keys (`android.media.browse.CONTENT_STYLE_BROWSABLE_HINT` / `..._PLAYABLE_HINT`) since Design left the exact key names unspecified ("Non-blocking polish"); this is standard, well-known Android Auto convention.
|
||||
|
||||
## Issues Found
|
||||
|
||||
None — all safety-net tests (pre-existing tests in every touched file) stayed green throughout.
|
||||
|
||||
## Status
|
||||
|
||||
31/31 tasks complete. Ready for `sdd-verify`. A real `flutter analyze` + full `flutter test` + DHU manual verification pass is recommended before merge, per the noted execution-constraint deviations on 7.1/7.2.
|
||||
@@ -0,0 +1,99 @@
|
||||
# Design: Android Auto (Projected) Media Browsing
|
||||
|
||||
## Technical Approach
|
||||
|
||||
Reuse the existing `PluriWaveAudioHandler` (main isolate, `audio_service 0.18`) and add only the browse layer Android Auto needs: three overrides (`getChildren`, `getMediaItem`, `playFromMediaId`) fed by a dedicated, cold-start-safe data source, plus the manifest declaration. Playback still flows through the untouched internal `playMediaItem` (servicio_audio.dart:433). A pure tree builder makes the logic unit-testable without a running car or platform. Realises capability `android-auto-media`; phone playback path is unchanged.
|
||||
|
||||
## Architecture Decisions
|
||||
|
||||
### Decision: getChildren data source (cold-start safe)
|
||||
|
||||
**Choice**: Introduce `FuenteEmisorasAuto` — a small browse-source abstraction registered into the handler via `registrarFuenteNavegacion(...)` (mirrors `registrarHandler`). Production impl reads **local** data directly: favourites from `ServicioFavoritos` (SQLite) and custom stations from the JSON file — both loadable without the network or the widget tree. `EstadoRadio`, when alive, pushes its in-memory lists as a **live snapshot** the source prefers; on a cold Auto bind it falls back to a direct local read.
|
||||
**Alternatives considered**: Inject `EstadoRadio` directly into the handler; a callback registered by `EstadoRadio` on init.
|
||||
**Rationale**: `ChangeNotifierProvider.create:` is **lazy** (app.dart:41) — a headless Auto bind runs `main()` but may never build `EstadoRadio`, and its `_init()` loads network `populares`. Depending on it would give the car an empty or blocked tree. Favourites+custom are local and reliable; the snapshot keeps car and phone identical when both are live. Provider architecture stays intact.
|
||||
|
||||
### Decision: media-id scheme
|
||||
|
||||
**Choice**: Folders use bare stable constants (`root` = `AudioService.browsableRootId`, `favoritos`, `todas`, `mis_emisoras`); stations use `emisora:<uuid>`.
|
||||
**Alternatives considered**: Reuse the stream URL as id (as the phone MediaItem does); numeric SQLite `id`.
|
||||
**Rationale**: The `emisora:` prefix is collision-free against folder ids and against the raw-URL ids the app uses internally; `uuid` is the stable cross-source key (`Emisora.==` is uuid-based). `playFromMediaId` parses the uuid, looks it up in the source, and builds the real `MediaItem` (id = `emisora.url`, `extras['uuid']`) exactly like the phone.
|
||||
|
||||
### Decision: default artwork delivery
|
||||
|
||||
**Choice**: Stations with a favicon use it (http(s), already loadable). Logo-less stations get `android.resource://es.freetimelab.pluriwave/drawable/default_station_art` — a bundled `res/drawable` PNG.
|
||||
**Alternatives considered**: content:// via the configured FileProvider (copy asset → build URI); remote placeholder URL; folder-art-only.
|
||||
**Rationale**: `android.resource://` is loaded by `ContentResolver` with **no per-URI grant**, works offline, and cannot 404 — unlike FileProvider content URIs (need `FLAG_GRANT_READ_URI_PERMISSION` for the system art loader) or a remote URL (offline/quality-gate risk). Flutter `assets/` are **not** reachable via `android.resource`, so the PNG lives in `res/drawable`. FileProvider (files-path root → segment `files`) remains the documented fallback if a loader rejects `android.resource`.
|
||||
|
||||
### Decision: which stations surface & ordering
|
||||
|
||||
**Choice**: `Favoritos` (SQLite), `Mis emisoras` (custom file), `Todas` = top `populares` snapshot when available. Each folder sorted by `ordenarEmisoras(_, ordenListas)` and **capped at 50**.
|
||||
**Rationale**: User-curated/local lists are reliable in a car (Google tests playback); a capped list avoids driver-distraction and Auto list limits. `Todas` degrades gracefully to empty-but-valid on cold bind.
|
||||
|
||||
### Decision: playback coherence with EstadoRadio
|
||||
|
||||
**Choice**: `playFromMediaId` delegates to internal `playMediaItem`. `EstadoRadio` already subscribes to `audio.estadoStream` and `emisoraActual => _emisoraSeleccionada ?? audio.emisoraActual`; extend that listener to reconcile `_emisoraSeleccionada = audio.emisoraActual` on a car-initiated change so it does not **shadow** the car's station.
|
||||
**Rationale**: Reuse over duplication; the one-line reconcile keeps the mini-player/current-station display correct when playback starts from the car.
|
||||
|
||||
### Decision: content style (optional)
|
||||
|
||||
**Choice**: Set `CONTENT_STYLE_*` extras — grid (2) for playable stations, list (1) for root folders. Non-blocking polish.
|
||||
|
||||
## Data Flow
|
||||
|
||||
Car (MediaBrowser) ──getChildren──▶ Handler ──▶ FuenteEmisorasAuto
|
||||
│ ├─ live snapshot (EstadoRadio, if alive)
|
||||
│ └─ local read (SQLite favs + custom file) ← cold bind
|
||||
Car (tap) ──playFromMediaId(emisora:uuid)──▶ Handler ──lookup──▶ Emisora ──▶ playMediaItem (unchanged)
|
||||
│
|
||||
EstadoRadio ◀── audio.estadoStream ── PlaybackState ───┘ (reconciles _emisoraSeleccionada)
|
||||
|
||||
## File Changes
|
||||
|
||||
| File | Action | Description |
|
||||
|------|--------|-------------|
|
||||
| `android/app/src/main/res/xml/automotive_app_desc.xml` | Create | `<automotiveApp><uses name="media"/></automotiveApp>` |
|
||||
| `android/app/src/main/AndroidManifest.xml` | Modify | Add `com.google.android.gms.car.application` meta-data |
|
||||
| `android/app/src/main/res/drawable/default_station_art.png` | Create | Bundled default station artwork |
|
||||
| `lib/servicios/navegacion_auto.dart` | Create | `FuenteEmisorasAuto` + local impl, id constants, pure `ConstructorArbolAuto` (tree/leaf builder, art fallback) |
|
||||
| `lib/servicios/servicio_audio.dart` | Modify | Override `getChildren`/`getMediaItem`/`playFromMediaId`; `registrarFuenteNavegacion`; delegate to `playMediaItem` |
|
||||
| `lib/estado/estado_radio.dart` | Modify | Push live snapshot to source; reconcile `_emisoraSeleccionada` on car-initiated playback |
|
||||
| `lib/main.dart` | Modify | Build + register the local browse source |
|
||||
| `test/servicios/navegacion_auto_test.dart` | Create | Tree, id resolution, art fallback, routing tests |
|
||||
|
||||
## Interfaces / Contracts
|
||||
|
||||
```dart
|
||||
abstract class FuenteEmisorasAuto {
|
||||
Future<List<Emisora>> favoritos();
|
||||
Future<List<Emisora>> misEmisoras();
|
||||
Future<List<Emisora>> todas(); // populares snapshot; may be empty (cold)
|
||||
Future<Emisora?> porUuid(String uuid);
|
||||
}
|
||||
|
||||
class ConstructorArbolAuto { // pure, no platform
|
||||
List<MediaItem> raiz(); // 3 folder MediaItems (playable:false)
|
||||
List<MediaItem> hijos(String parentId, {required List<Emisora> emisoras});
|
||||
MediaItem itemEmisora(Emisora e); // id 'emisora:<uuid>', title, artUri fallback
|
||||
Emisora? resolver(String id, List<Emisora> universo);
|
||||
static const idFavoritos = 'favoritos', idTodas = 'todas', idMisEmisoras = 'mis_emisoras';
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
| Layer | What to Test | Approach |
|
||||
|-------|-------------|----------|
|
||||
| Unit | Root returns 3 folders (ids/titles/`playable:false`) | Fake `FuenteEmisorasAuto`; assert `getChildren(root)` |
|
||||
| Unit | Leaf ids `emisora:<uuid>`, title+artUri always set; favicon vs default art fallback | `ConstructorArbolAuto.itemEmisora` |
|
||||
| Unit | `getMediaItem`/`resolver` maps id→Emisora; unknown→null | Pure builder assertions |
|
||||
| Unit | `playFromMediaId` builds MediaItem (id=url, extras uuid) and delegates to `playMediaItem` | Spy/seam over `playMediaItem` (existing test pattern) |
|
||||
| Manual (DHU) | Discovery, real art render, playback, play/pause car↔phone sync, grid/list | User-side, not `flutter test` |
|
||||
|
||||
## Migration / Rollout
|
||||
|
||||
No migration. Additive: revert deletes the XML, the meta-data line, the drawable, the new file, and the three overrides — phone path untouched, zero residual state.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- [ ] Confirm the system art loader accepts `android.resource://`; else switch logo-less default to FileProvider content URI (`content://…/files/auto/default_station_art.png`).
|
||||
- [ ] `Todas` on a cold bind shows only if a snapshot exists — accept empty folder, or trigger a lightweight local populares cache? (defer)
|
||||
@@ -0,0 +1,72 @@
|
||||
# Proposal: Android Auto (Projected) Media Browsing
|
||||
|
||||
## Intent
|
||||
|
||||
PluriWave can play radio on the phone but is invisible and uncontrollable from a car head unit. A driver cannot browse stations or start playback safely while driving. Android Auto (projected) needs a media app to expose a browsable tree plus play-by-id, and to declare itself so Auto discovers it. The audio engine (`audio_service ^0.18.15`, `PluriWaveAudioHandler`) already provides `MediaBrowserService` + `MediaSession`; the browse/discovery layer is missing. Closing this gap turns PluriWave into a car-usable media app without touching the audio pipeline.
|
||||
|
||||
## Scope
|
||||
|
||||
### In Scope
|
||||
- Add `android/app/src/main/res/xml/automotive_app_desc.xml` (`<uses name="media"/>`).
|
||||
- Add `<meta-data com.google.android.gms.car.application>` to `AndroidManifest.xml`.
|
||||
- Override `getChildren`, `getMediaItem`, `playFromMediaId` in `PluriWaveAudioHandler`.
|
||||
- Build a browse tree: Favoritos / Todas las emisoras / Mis emisoras (folders + playable stations).
|
||||
- Ensure every playable `MediaItem` has `title` + `artUri`; provide a default artwork for stations without a logo.
|
||||
- `playFromMediaId` resolves id → `Emisora` → reuses internal `playMediaItem` (no duplicated playback logic).
|
||||
- Strict-TDD Dart tests for tree building and id resolution.
|
||||
|
||||
### Out of Scope
|
||||
- Play Console distribution / car-app review steps (separate user documentation).
|
||||
- DHU / Desktop Head Unit manual testing (user-side).
|
||||
- iOS CarPlay.
|
||||
- Voice search (`playFromSearch`) and grid/list content-style polish (deferred; non-blocking).
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `android-auto-media`: PluriWave MUST expose a browsable station tree and play-by-media-id to Android Auto projected, with every playable item carrying title + artwork, reusing the existing audio handler.
|
||||
|
||||
### Modified Capabilities
|
||||
- None.
|
||||
|
||||
## Approach
|
||||
|
||||
Reuse, don't rebuild. Handler already runs in the main isolate, so `getChildren` reads station/favorites state directly from Provider states (`lib/estado/`). Map categories → folder `MediaItem`s (`playable: false`), stations → playable `MediaItem`s with stable ids. `playFromMediaId` reuses `_emisoraDesdeMediaItem` (servicio_audio.dart:705) and `playMediaItem` (servicio_audio.dart:433). Root is `AudioService.browsableRootId`. Start with a minimal tree (Favoritos), then widen. Dart-only + XML/manifest — no build required for logic tests.
|
||||
|
||||
## Affected Areas
|
||||
|
||||
| Area | Impact | Description |
|
||||
|------|--------|-------------|
|
||||
| `android/app/src/main/res/xml/automotive_app_desc.xml` | New | Auto media declaration |
|
||||
| `android/app/src/main/AndroidManifest.xml` | Modified | Add car application meta-data |
|
||||
| `lib/servicios/servicio_audio.dart` | Modified | Override getChildren/getMediaItem/playFromMediaId + default artwork |
|
||||
| `lib/estado/` | Read | Source of stations/favorites for the tree |
|
||||
| `test/` (Dart) | New | Tree + id-resolution tests |
|
||||
|
||||
## Risks
|
||||
|
||||
| Risk | Likelihood | Mitigation |
|
||||
|------|------------|------------|
|
||||
| Breaking the audio pipeline | Low | New browse methods only; reuse `playMediaItem`, never modify it |
|
||||
| Stations without artwork fail Google quality gate | Med | Bundled default `artUri` for logo-less stations |
|
||||
| Dead station streams give bad car impression | Med | Surface curated/favorite stations; existing `controlador_reconexion.dart` handles failures |
|
||||
| `artUri` not loadable by system (asset scheme) | Med | Use http(s) logos or `content://`-served default asset |
|
||||
|
||||
## Rollback Plan
|
||||
|
||||
Additive change. Revert the PR: delete `automotive_app_desc.xml`, remove the manifest meta-data line, and drop the three overridden methods + tests. Phone playback path is untouched, so reverting leaves the app exactly as today with zero residual state.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `audio_service ^0.18.15` (already present).
|
||||
- Default station artwork asset (to add).
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] Android Auto discovers PluriWave as a media app.
|
||||
- [ ] Browse tree shows Favoritos / Todas las emisoras / Mis emisoras.
|
||||
- [ ] Every playable item shows title + artwork.
|
||||
- [ ] Selecting a station plays it via reused `playMediaItem`.
|
||||
- [ ] Play/pause/stop state stays in sync car ↔ phone.
|
||||
- [ ] Dart tests for tree building + id resolution pass under `flutter test`.
|
||||
- [ ] Audio pipeline behavior unchanged on phone.
|
||||
@@ -0,0 +1,107 @@
|
||||
# Android Auto Media Specification
|
||||
|
||||
## Purpose
|
||||
|
||||
PluriWave MUST expose a browsable station tree and play-by-media-id interface to Android Auto (projected) via the existing `PluriWaveAudioHandler` (`audio_service` MediaBrowserService/MediaSession), so a driver can browse and play stations from the car head unit without rebuilding the audio pipeline.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Android Auto Discovery Declaration
|
||||
|
||||
The app MUST declare itself as a media app to Android Auto so the car head unit discovers it.
|
||||
|
||||
#### Scenario: Android Auto scans installed apps
|
||||
|
||||
- GIVEN PluriWave is installed on the phone
|
||||
- WHEN Android Auto scans installed apps for car-app support
|
||||
- THEN it finds `res/xml/automotive_app_desc.xml` declaring `<uses name="media"/>`
|
||||
- AND the `AndroidManifest.xml` `<application>` block declares `<meta-data android:name="com.google.android.gms.car.application" android:resource="@xml/automotive_app_desc"/>`
|
||||
- AND PluriWave appears in the car's media app list
|
||||
|
||||
### Requirement: Browsable Media Tree
|
||||
|
||||
`getChildren` MUST return a browsable tree rooted at `AudioService.browsableRootId`, organized into non-playable folders (Favoritos, Todas las emisoras, Mis emisoras) containing playable station items.
|
||||
|
||||
#### Scenario: Car requests the root
|
||||
|
||||
- GIVEN the car head unit connects and requests the root (`AudioService.browsableRootId`)
|
||||
- WHEN `getChildren` is called with the root id
|
||||
- THEN it returns three folder `MediaItem`s (Favoritos, Todas las emisoras, Mis emisoras), each with `playable: false`
|
||||
|
||||
#### Scenario: Car requests a folder with no stations
|
||||
|
||||
- GIVEN the user has zero favorite stations
|
||||
- WHEN `getChildren` is called with the Favoritos folder id
|
||||
- THEN it returns an empty list, not an error
|
||||
|
||||
#### Scenario: Browse requested before app state is loaded
|
||||
|
||||
- GIVEN the audio handler starts cold and station/favorites Provider state has not finished loading
|
||||
- WHEN `getChildren` is called (root or any folder)
|
||||
- THEN it returns a valid, possibly empty, list without throwing and without blocking or crashing the service
|
||||
|
||||
### Requirement: Playable Item Metadata
|
||||
|
||||
Every playable `MediaItem` (station) MUST include a non-empty `title` and a loadable `artUri`; stations without a logo MUST fall back to a bundled default artwork.
|
||||
|
||||
#### Scenario: Station has a remote logo
|
||||
|
||||
- GIVEN a station has a valid remote logo URL
|
||||
- WHEN it is mapped to a `MediaItem`
|
||||
- THEN `title` is the station name and `artUri` is the station's logo URL
|
||||
|
||||
#### Scenario: Station has no logo
|
||||
|
||||
- GIVEN a station has no logo (null or empty)
|
||||
- WHEN it is mapped to a `MediaItem`
|
||||
- THEN `artUri` is set to the bundled default artwork asset instead of being empty or null
|
||||
|
||||
### Requirement: Media Item Resolution by ID
|
||||
|
||||
`getMediaItem` MUST resolve a single station id to its full `MediaItem`, returning `null` for unknown ids instead of throwing.
|
||||
|
||||
#### Scenario: Known station id
|
||||
|
||||
- GIVEN a valid station id that exists in current state
|
||||
- WHEN `getMediaItem(id)` is called
|
||||
- THEN it returns the corresponding `MediaItem` with `title` and `artUri` populated
|
||||
|
||||
#### Scenario: Unknown station id
|
||||
|
||||
- GIVEN an id that does not match any known station or folder
|
||||
- WHEN `getMediaItem(id)` is called
|
||||
- THEN it returns `null` without throwing
|
||||
|
||||
### Requirement: Play by Media ID Reuses Existing Playback Path
|
||||
|
||||
`playFromMediaId` MUST resolve the id to an `Emisora` and invoke the existing internal `playMediaItem` path; it MUST NOT duplicate playback or reconnection logic.
|
||||
|
||||
#### Scenario: User selects a station in the car
|
||||
|
||||
- GIVEN the user taps a playable station item on the car head unit
|
||||
- WHEN `playFromMediaId(id)` is called
|
||||
- THEN the id is resolved to an `Emisora` via the existing resolution logic (`_emisoraDesdeMediaItem`)
|
||||
- AND playback starts through the existing `playMediaItem` internal path
|
||||
- AND no separate or duplicated playback logic is executed
|
||||
|
||||
#### Scenario: Unknown or stale media id
|
||||
|
||||
- GIVEN `playFromMediaId` is called with an id that no longer resolves to a known station
|
||||
- WHEN resolution fails
|
||||
- THEN playback does not start and no unhandled exception propagates from the handler
|
||||
|
||||
### Requirement: Playback State Synchronization
|
||||
|
||||
Play/pause/stop state changes MUST remain synchronized between the car head unit and the phone UI, regardless of which side initiated the change.
|
||||
|
||||
#### Scenario: User pauses from the car
|
||||
|
||||
- GIVEN a station is playing and projected to the car
|
||||
- WHEN the user pauses from the car head unit
|
||||
- THEN the phone UI reflects the paused state via the shared `MediaSession`/`PlaybackState`
|
||||
|
||||
#### Scenario: User pauses from the phone
|
||||
|
||||
- GIVEN a station is playing and projected to the car
|
||||
- WHEN the user pauses from the phone UI
|
||||
- THEN the car head unit reflects the paused state via the same shared session
|
||||
@@ -0,0 +1,222 @@
|
||||
# Tasks: android-auto-media
|
||||
|
||||
Strict TDD active for Dart layers. Behavioral task = RED (failing test) -> GREEN (minimal impl) -> REFACTOR (cleanup, still green). Kotlin/manifest/XML/PNG tasks have no `flutter test` coverage (no Android build env) — marked **[static-review-only]**; `flutter build`/`flutter run` MUST NOT be executed. "Parallel" tasks have no file overlap with concurrently-listed siblings; "Sequential" tasks depend on a prior task's output.
|
||||
|
||||
Affected files: `lib/servicios/navegacion_auto.dart`, `lib/servicios/servicio_audio.dart`, `lib/estado/estado_radio.dart`, `lib/main.dart`, `android/app/src/main/res/xml/automotive_app_desc.xml`, `android/app/src/main/AndroidManifest.xml`, `android/app/src/main/res/drawable/default_station_art.png`, `test/servicios/navegacion_auto_test.dart`, `test/estado/estado_radio_test.dart`.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — Pure tree builder & routing (`lib/servicios/navegacion_auto.dart`)
|
||||
|
||||
### 1.1 [x] Define `FuenteEmisorasAuto` abstract interface + id constants (Sequential — blocks all of Phase 1)
|
||||
- Satisfies: Design "Interfaces/Contracts"; Spec "Browsable Media Tree", "Media Item Resolution by ID".
|
||||
- File: `lib/servicios/navegacion_auto.dart` (new).
|
||||
- Declare `abstract class FuenteEmisorasAuto` with `favoritos()`, `misEmisoras()`, `todas()`, `porUuid(String uuid)` per design contract.
|
||||
- Declare `ConstructorArbolAuto` class skeleton with `static const idFavoritos/idTodas/idMisEmisoras` and `AudioService.browsableRootId` usage.
|
||||
|
||||
### 1.2 [x] [RED] `raiz()` returns 3 non-playable folder `MediaItem`s (Sequential — depends on 1.1)
|
||||
- Satisfies: Spec "Browsable Media Tree" / Scenario "Car requests the root".
|
||||
- File: `test/servicios/navegacion_auto_test.dart` (new).
|
||||
- Assert `ConstructorArbolAuto().raiz()` returns exactly 3 items with ids `idFavoritos`, `idTodas`, `idMisEmisoras`, each `playable: false` and a non-empty `title`.
|
||||
- Run: fails (`raiz()` not implemented).
|
||||
|
||||
### 1.3 [x] [GREEN] Implement `raiz()` (Sequential — depends on 1.2)
|
||||
- File: `lib/servicios/navegacion_auto.dart`.
|
||||
- Return the 3 folder `MediaItem`s; set `CONTENT_STYLE_*` list extra per Design "content style" decision.
|
||||
- Run: 1.2 passes.
|
||||
|
||||
### 1.4 [x] [RED] `itemEmisora()` sets id `emisora:<uuid>`, title, favicon-as-artUri (Sequential — depends on 1.1)
|
||||
- Satisfies: Spec "Playable Item Metadata" / Scenario "Station has a remote logo"; Design "media-id scheme".
|
||||
- File: `test/servicios/navegacion_auto_test.dart`.
|
||||
- Build an `Emisora` with a non-empty `favicon`; assert `itemEmisora(e).id == 'emisora:${e.uuid}'`, `title == e.nombre`, `artUri.toString() == e.favicon`, `playable: true`.
|
||||
- Run: fails.
|
||||
|
||||
### 1.5 [x] [RED] `itemEmisora()` falls back to default art when logo is null/empty (Parallel with 1.4 — same file, sequence fixed within phase)
|
||||
- Satisfies: Spec "Playable Item Metadata" / Scenario "Station has no logo"; Design "default artwork delivery".
|
||||
- File: `test/servicios/navegacion_auto_test.dart`.
|
||||
- Build an `Emisora` with `favicon: null` and one with `favicon: ''`; assert both resolve `artUri` to `android.resource://es.freetimelab.pluriwave/drawable/default_station_art`.
|
||||
- Run: fails.
|
||||
|
||||
### 1.6 [x] [GREEN] Implement `itemEmisora()` incl. art fallback (Sequential — depends on 1.4, 1.5)
|
||||
- File: `lib/servicios/navegacion_auto.dart`.
|
||||
- Build `MediaItem` with `id: 'emisora:${e.uuid}'`, `title`, `artUri: Uri.parse(e.favicon?.isNotEmpty == true ? e.favicon! : defaultArtUri)`, `playable: true`, grid content-style extra.
|
||||
- Run: 1.4, 1.5 pass.
|
||||
|
||||
### 1.7 [x] [RED] `hijos(parentId, emisoras)` maps a list to leaf items, capped at 50, sorted via `ordenarEmisoras` (Sequential — depends on 1.6)
|
||||
- Satisfies: Spec "Browsable Media Tree" / Scenario "Car requests a folder with no stations"; Design "which stations surface & ordering".
|
||||
- File: `test/servicios/navegacion_auto_test.dart`.
|
||||
- Assert: 60-item input for `idFavoritos` returns exactly 50 items, in `ordenarEmisoras` order; empty input returns `[]` (not throwing); unknown `parentId` returns `[]`.
|
||||
- Run: fails.
|
||||
|
||||
### 1.8 [x] [GREEN] Implement `hijos()` (Sequential — depends on 1.7)
|
||||
- File: `lib/servicios/navegacion_auto.dart`.
|
||||
- Apply `ordenarEmisoras(emisoras, ordenListas)`, `.take(50)`, map via `itemEmisora`; unknown `parentId` returns `const []`.
|
||||
- Run: 1.7 passes.
|
||||
|
||||
### 1.9 [x] [RED] `resolver(id, universo)` maps `emisora:<uuid>` to the matching `Emisora`; unknown id returns `null` (Sequential — depends on 1.1)
|
||||
- Satisfies: Spec "Media Item Resolution by ID" (both scenarios).
|
||||
- File: `test/servicios/navegacion_auto_test.dart`.
|
||||
- Assert known uuid resolves; assert non-`emisora:` id, malformed id, and unmatched uuid all return `null` without throwing.
|
||||
- Run: fails.
|
||||
|
||||
### 1.10 [x] [GREEN] Implement `resolver()` (Sequential — depends on 1.9)
|
||||
- File: `lib/servicios/navegacion_auto.dart`.
|
||||
- Strip `emisora:` prefix, `firstWhereOrNull` on `universo` by `uuid`; return `null` on any mismatch.
|
||||
- Run: 1.9 passes.
|
||||
|
||||
### 1.11 [x] [RED] Routing seam: id resolves and delegates to an injected `reproducir` callback with a phone-shaped `MediaItem` (id=url, extras.uuid) (Sequential — depends on 1.10)
|
||||
- Satisfies: Spec "Play by Media ID Reuses Existing Playback Path" (both scenarios); Design "playback coherence" testing row ("spy/seam over playMediaItem").
|
||||
- File: `test/servicios/navegacion_auto_test.dart`.
|
||||
- Add a top-level testable function/method, e.g. `Future<void> reproducirPorMediaId(String id, {required FuenteEmisorasAuto fuente, required Future<void> Function(MediaItem) reproducir})`, in `navegacion_auto.dart`.
|
||||
- Test: fake `FuenteEmisorasAuto.porUuid` returns a known `Emisora`; assert the spy `reproducir` callback receives a `MediaItem` with `id == emisora.url`, `extras['uuid'] == emisora.uuid`.
|
||||
- Test: fake `porUuid` returns `null` (stale/unknown id); assert `reproducir` is never called and no exception propagates.
|
||||
- Run: fails.
|
||||
|
||||
### 1.12 [x] [GREEN] Implement `reproducirPorMediaId()` (Sequential — depends on 1.11)
|
||||
- File: `lib/servicios/navegacion_auto.dart`.
|
||||
- Parse uuid from id, `await fuente.porUuid(uuid)`; if found, build the phone-shaped `MediaItem` and `await reproducir(item)`; if null, return without calling `reproducir` or throwing.
|
||||
- Run: 1.11 passes.
|
||||
|
||||
### 1.13 [x] [REFACTOR] Cleanup pass on `navegacion_auto.dart` (Sequential — depends on 1.3-1.12 green)
|
||||
- Re-read top to bottom: naming consistency (`raiz`/`hijos`/`itemEmisora`/`resolver`/`reproducirPorMediaId`), doc comments, no duplicated art-fallback logic.
|
||||
- Full `navegacion_auto_test.dart` suite stays green.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — Local data source implementation
|
||||
|
||||
### 2.1 [x] Implement `FuenteEmisorasAutoLocal` (Sequential — depends on Phase 1 interface, 1.1) [static-review-only for the IO itself; interface contract already covered by Phase 1 fakes]
|
||||
- Satisfies: Design "getChildren data source (cold-start safe)".
|
||||
- File: `lib/servicios/navegacion_auto.dart`.
|
||||
- `favoritos()` reads via `ServicioFavoritos().obtenerTodos()`.
|
||||
- `misEmisoras()` mirrors `EstadoRadio._cargarEmisorasCustom()`'s tolerant JSON read (reuse `parseListaTolerante`/`Emisora.fromMap` and the existing custom-file path resolution) — must not throw on missing/corrupt file, return `[]` instead.
|
||||
- `todas()` returns `[]` by default (no live snapshot yet — populated by Phase 3's `EstadoRadio` push); `porUuid()` searches across all three lists.
|
||||
- All methods must never throw on cold start (no network, no Provider tree) — swallow IO errors to empty results, per Spec "Browse requested before app state is loaded".
|
||||
- Manual check: run `flutter analyze` — no static errors. No dedicated unit test (wraps already-tested `ServicioFavoritos`/tolerant-parse paths); covered indirectly by Phase 1 fakes exercising the interface contract.
|
||||
|
||||
### 2.2 [x] Add live-snapshot mutable buffer to `FuenteEmisorasAutoLocal` (Sequential — depends on 2.1)
|
||||
- Satisfies: Design "live snapshot the source prefers".
|
||||
- File: `lib/servicios/navegacion_auto.dart`.
|
||||
- Add `void actualizarSnapshot({List<Emisora>? favoritos, List<Emisora>? misEmisoras, List<Emisora>? todas})` that overrides the fields returned by `favoritos()`/`misEmisoras()`/`todas()` when set (non-null), falling back to the local reads otherwise.
|
||||
- `flutter analyze` clean; behavior exercised end-to-end in Phase 4's `EstadoRadio` tests (this method is a plain setter, no isolated test required).
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — Handler wiring (`lib/servicios/servicio_audio.dart`) [static-review-only — thin delegation to Phase 1's already-tested pure logic; no existing pattern instantiates `PluriWaveAudioHandler` in `flutter test`]
|
||||
|
||||
### 3.1 [x] Add `registrarFuenteNavegacion()` + handler field (Sequential — depends on Phase 1, 2.1)
|
||||
- Satisfies: Design "getChildren data source" registration mirroring `registrarHandler`.
|
||||
- File: `lib/servicios/servicio_audio.dart`.
|
||||
- Add a module-level `FuenteEmisorasAuto? _fuenteNavegacionGlobal;` and `void registrarFuenteNavegacion(FuenteEmisorasAuto fuente)`, mirroring the existing `registrarHandler` pattern (lines 32-36).
|
||||
- Store a reference the handler reads in the three overrides below.
|
||||
|
||||
### 3.2 [x] Override `getChildren` (Sequential — depends on 3.1)
|
||||
- Satisfies: Spec "Browsable Media Tree" (all 3 scenarios).
|
||||
- File: `lib/servicios/servicio_audio.dart`, class `PluriWaveAudioHandler`.
|
||||
- `parentMediaId == AudioService.browsableRootId` → `ConstructorArbolAuto().raiz()`.
|
||||
- Otherwise → resolve the matching emisora list from the registered `FuenteEmisorasAuto` (`favoritos`/`misEmisoras`/`todas` by folder id) and delegate to `ConstructorArbolAuto().hijos(...)`.
|
||||
- Wrap the whole body in try/catch returning `[]` on any error — never throw, per Spec "Browse requested before app state is loaded".
|
||||
|
||||
### 3.3 [x] Override `getMediaItem` (Sequential — depends on 3.1, Phase 1's `resolver`)
|
||||
- Satisfies: Spec "Media Item Resolution by ID".
|
||||
- File: `lib/servicios/servicio_audio.dart`.
|
||||
- Gather the union of all three lists from the registered source, call `ConstructorArbolAuto().resolver(id, universo)`, map to `itemEmisora` if found, else return `null`. No throw on error.
|
||||
|
||||
### 3.4 [x] Override `playFromMediaId` (Sequential — depends on 3.1, 1.12)
|
||||
- Satisfies: Spec "Play by Media ID Reuses Existing Playback Path" (both scenarios).
|
||||
- File: `lib/servicios/servicio_audio.dart`.
|
||||
- One-liner delegation: `await reproducirPorMediaId(mediaId, fuente: _fuenteNavegacion, reproducir: playMediaItem);` wrapped in try/catch that swallows and logs (never propagates), per Spec "Unknown or stale media id" scenario.
|
||||
- Manual check: `flutter analyze` clean. Logic already covered by 1.11/1.12; this override is not independently unit-tested (no handler-instantiation test pattern exists in this repo).
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 — `EstadoRadio` wiring (`lib/estado/estado_radio.dart`)
|
||||
|
||||
### 4.1 [x] [RED] Live snapshot is pushed to the registered `FuenteEmisorasAuto` on favorites/custom/populares changes (Sequential — depends on Phase 2's `actualizarSnapshot`)
|
||||
- Satisfies: Design "live snapshot the source prefers".
|
||||
- File: `test/estado/estado_radio_test.dart`.
|
||||
- Inject a spy `FuenteEmisorasAuto` (records `actualizarSnapshot` calls) into `EstadoRadio` (constructor param, default unused in existing tests).
|
||||
- Assert: after `cargarPopulares()` / favorites toggle / custom-station add, the spy received an updated snapshot reflecting the new lists.
|
||||
- Run: fails (no push exists yet).
|
||||
|
||||
### 4.2 [x] [GREEN] Wire the snapshot push (Sequential — depends on 4.1)
|
||||
- File: `lib/estado/estado_radio.dart`.
|
||||
- Add optional `FuenteEmisorasAuto? fuenteAuto` constructor param; call `fuenteAuto?.actualizarSnapshot(favoritos: ..., misEmisoras: ..., todas: ...)` at the end of the existing `notifyListeners()` call sites that mutate those lists (`_cargarEmisorasCustom`, favorites load/toggle, `cargarPopulares`).
|
||||
- Run: 4.1 passes; full `estado_radio_test.dart` suite stays green (param optional, default `null`, zero behavior change for existing callers).
|
||||
|
||||
### 4.3 [x] [RED] Car-initiated playback reconciles `_emisoraSeleccionada` (Sequential — depends on 4.2)
|
||||
- Satisfies: Design "playback coherence with EstadoRadio"; Spec "Playback State Synchronization" / Scenario "User pauses from the car" (precondition: car-selected station must be reflected on the phone).
|
||||
- File: `test/estado/estado_radio_test.dart`.
|
||||
- Using the existing `_AudioControlado` fake: simulate a car-initiated selection by setting `audio.emisoraActual` to a station the `EstadoRadio` did not select via `reproducir()`, then push an `estadoStream` event.
|
||||
- Assert `estado.emisoraActual` reflects the car-selected station after the stream event (i.e., `_emisoraSeleccionada` was reconciled to `audio.emisoraActual`).
|
||||
- Run: fails (today's `_escucharErroresReproduccion` listener does not reconcile).
|
||||
|
||||
### 4.4 [x] [GREEN] Reconcile `_emisoraSeleccionada` in the state listener (Sequential — depends on 4.3)
|
||||
- File: `lib/estado/estado_radio.dart`, method `_escucharErroresReproduccion()` (current L284-297).
|
||||
- Inside the `audio.estadoStream.listen` callback, if `audio.emisoraActual != null && audio.emisoraActual!.uuid != _emisoraSeleccionada?.uuid`, set `_emisoraSeleccionada = audio.emisoraActual` before `notifyListeners()`.
|
||||
- Run: 4.3 passes; full `estado_radio_test.dart` suite (including existing error-path assertions at L458-464) stays green.
|
||||
|
||||
### 4.5 [x] [REFACTOR] Cleanup `estado_radio.dart` changes (Sequential — depends on 4.2, 4.4 green)
|
||||
- Confirm naming/comment clarity for the new `fuenteAuto` param and the reconcile branch; no behavior change.
|
||||
- Full suite stays green.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5 — App wiring (`lib/main.dart`) [static-review-only — no existing `main.dart` unit-test pattern]
|
||||
|
||||
### 5.1 [x] Build and register the local browse source at startup (Sequential — depends on Phase 2, Phase 3.1, Phase 4.2)
|
||||
- Satisfies: Design "Data Flow" (cold-bind local read available before `EstadoRadio` builds).
|
||||
- File: `lib/main.dart`.
|
||||
- After `registrarHandler(handler);` (L42), construct `final fuenteAuto = FuenteEmisorasAutoLocal();` and call `registrarFuenteNavegacion(fuenteAuto);`.
|
||||
- Pass `fuenteAuto` into `PluriWaveApp(prefs: prefs, fuenteAuto: fuenteAuto)` (or the app's existing DI seam) so `EstadoRadio` receives it as the `fuenteAuto` constructor param added in 4.2.
|
||||
- Manual check: `flutter analyze` clean; existing widget/app tests (if any construct `PluriWaveApp`) stay green with the new optional param defaulting sensibly.
|
||||
|
||||
---
|
||||
|
||||
## Phase 6 — Android native declaration [static-review-only — no Android build env, `flutter build` MUST NOT run]
|
||||
|
||||
### 6.1 [x] Create `automotive_app_desc.xml` (Sequential — independent of Dart phases)
|
||||
- Satisfies: Spec "Android Auto Discovery Declaration".
|
||||
- File: `android/app/src/main/res/xml/automotive_app_desc.xml` (new).
|
||||
- Content: `<automotiveApp><uses name="media"/></automotiveApp>`.
|
||||
- Static review: valid XML, correct namespace-free root element per Android Auto docs.
|
||||
|
||||
### 6.2 [x] Add manifest meta-data (Sequential — depends on 6.1)
|
||||
- Satisfies: Spec "Android Auto Discovery Declaration".
|
||||
- File: `android/app/src/main/AndroidManifest.xml`.
|
||||
- Inside `<application>`, add `<meta-data android:name="com.google.android.gms.car.application" android:resource="@xml/automotive_app_desc"/>`.
|
||||
- Static review: placed inside `<application>`, does not duplicate an existing meta-data entry, does not disturb existing `<service>`/`<activity>` declarations.
|
||||
|
||||
### 6.3 [x] Add default station artwork drawable (Parallel with 6.1/6.2 — different file)
|
||||
- Satisfies: Spec "Playable Item Metadata" / Scenario "Station has no logo"; Design "default artwork delivery".
|
||||
- File: `android/app/src/main/res/drawable/default_station_art.png` (new binary asset).
|
||||
- Static review: filename matches `default_station_art` referenced by Phase 1's `itemEmisora()` fallback URI exactly (case-sensitive, no extension in the `android.resource://` URI).
|
||||
|
||||
---
|
||||
|
||||
## Phase 7 — Full regression sweep (Sequential — depends on all prior phases green)
|
||||
|
||||
### 7.1 [x] Run full `flutter test` suite
|
||||
- Satisfies: Design "Testing Strategy" unit rows; Proposal-level success criteria.
|
||||
- Run the entire suite (not just `navegacion_auto_test.dart`/`estado_radio_test.dart`) to catch cross-file regressions (e.g. `EstadoRadio` callers relying on the old constructor signature, existing `estado_radio_test.dart` groups touching `_escucharErroresReproduccion`).
|
||||
- Fix any incidental breakage; do not weaken unrelated tests to force green.
|
||||
- **DEVIATION (execution constraint)**: orchestrator instructions forbid running the full suite in this environment. Ran targeted files instead: `navegacion_auto_test.dart` (10/10), `estado_radio_test.dart` (21/21), `servicio_audio_reconnect_test.dart`, `servicio_audio_source_switch_test.dart`, `servicio_audio_eq_reapply_test.dart`, `servicio_audio_session_test.dart` (all green, 52/52 combined) — these cover every file touched by this change. A genuine full-suite run is recommended before merge/CI.
|
||||
|
||||
### 7.2 [x] `flutter analyze` clean pass
|
||||
- Run `flutter analyze`; zero issues across all new/modified files.
|
||||
- **DEVIATION (execution constraint)**: `flutter analyze` is disallowed in this environment (hangs). Performed manual static review instead: verified override signatures against the installed `audio_service-0.18.18` package source, verified all imports resolve, verified no unused/undeclared symbols by successfully compiling+running every touched file through `flutter test`. A real `flutter analyze` pass is still recommended before merge/CI.
|
||||
|
||||
### 7.3 [x] Manual DHU verification note (optional, non-blocking, not coded)
|
||||
- Satisfies: Design "Testing Strategy" manual row (discovery, art render, playback, play/pause sync, grid/list).
|
||||
- Record in the commit/PR description that Android Auto Desktop Head Unit (DHU) verification is recommended before shipping but not required to land this change, since native surfaces (Phase 6) have no automated coverage here.
|
||||
|
||||
---
|
||||
|
||||
## Review Workload Forecast
|
||||
|
||||
- Files touched: `lib/servicios/navegacion_auto.dart` (new, ~150-180 lines incl. `FuenteEmisorasAutoLocal`), `lib/servicios/servicio_audio.dart` (~40-60 changed lines, 3 overrides + registration), `lib/estado/estado_radio.dart` (~25-35 changed lines, ctor param + 2 call sites), `lib/main.dart` (~5-10 lines), `android/app/src/main/res/xml/automotive_app_desc.xml` (new, ~4 lines), `android/app/src/main/AndroidManifest.xml` (~2 lines), `android/app/src/main/res/drawable/default_station_art.png` (new binary), `test/servicios/navegacion_auto_test.dart` (new, ~150-200 lines), `test/estado/estado_radio_test.dart` (~60-80 new lines).
|
||||
- Estimated total changed/added lines: roughly 450-560 lines (exceeds the 400-line single-PR budget, driven mostly by the new pure-builder file plus its dedicated test file).
|
||||
- Chained PRs recommended: Yes — natural split is (a) Phase 1+2 pure builder & local source + tests (self-contained, no handler/app wiring, ~300-350 lines), (b) Phase 3+4+5 wiring into handler/EstadoRadio/main.dart (~90-115 lines), (c) Phase 6 native declaration + art asset (~10 lines + binary, zero Dart risk, static-review-only).
|
||||
- 400-line budget risk: High if delivered as one PR; Low per slice if chained as above.
|
||||
- Decision needed before apply: Yes — confirm chained delivery (and, since this repo pushes directly to `main` with no PR workflow observed in its history, confirm whether "chained" here means sequential trunk commits per phase-group rather than separate PRs) before `sdd-apply` starts Phase 3.
|
||||
- Ownership/dependency note: Phase 1 is a hard sequential blocker for Phase 2, 3, and part of 4 (`reproducirPorMediaId`, `resolver`). Phase 3 and Phase 5 both depend on Phase 2's `FuenteEmisorasAutoLocal` and Phase 1's registration constants. Phase 4 depends only on Phase 2's `actualizarSnapshot` signature, not on Phase 3 — Phase 4 and Phase 3 can proceed in parallel (different files) once Phase 2 lands. Phase 6 has zero Dart dependency and may be done anytime in parallel with all Dart phases.
|
||||
@@ -0,0 +1,115 @@
|
||||
# Verification Report
|
||||
|
||||
**Change**: android-auto-media
|
||||
**Version**: N/A
|
||||
**Mode**: Strict TDD (Dart layers) + static-review-only (Kotlin/manifest/XML/PNG, per tasks.md)
|
||||
**Reviewer**: fresh adversarial sdd-verify pass (uncommitted working tree, size:exception pending single commit)
|
||||
|
||||
---
|
||||
|
||||
### Completeness
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Tasks total | 31 |
|
||||
| Tasks complete | 31 |
|
||||
| Tasks incomplete | 0 |
|
||||
|
||||
No incomplete tasks.
|
||||
|
||||
---
|
||||
|
||||
### Build & Tests Execution
|
||||
|
||||
**Build**: Not run (forbidden in this environment per instructions -- flutter build/analyze/gen-l10n hang here)
|
||||
|
||||
**Tests**: 31 passed / 0 failed / 0 skipped (targeted suites)
|
||||
```
|
||||
flutter test test/servicios/navegacion_auto_test.dart test/estado/estado_radio_test.dart --concurrency=1 --timeout=60s
|
||||
-> 00:01 +31: All tests passed!
|
||||
(10 in navegacion_auto_test.dart, 21 in estado_radio_test.dart)
|
||||
```
|
||||
|
||||
Additional safety-net re-run (files touched by the handler-adjacent change):
|
||||
```
|
||||
flutter test test/servicios/servicio_audio_reconnect_test.dart test/servicios/servicio_audio_source_switch_test.dart test/servicios/servicio_audio_eq_reapply_test.dart test/servicios/servicio_audio_session_test.dart --concurrency=1 --timeout=60s
|
||||
-> 00:02 +21: All tests passed!
|
||||
```
|
||||
Combined: 52/52 green -- matches the count claimed in apply-progress.md.
|
||||
|
||||
**Side effect found and reverted**: running flutter test triggered an implicit pub get that downgraded meta (1.18.0 to 1.17.0) and test_api (0.7.11 to 0.7.10) in pubspec.lock, due to this machine local pub cache. This was NOT part of the change under review -- reverted with a git checkout of pubspec.lock before this report was written. Working tree is clean of verifier-induced drift.
|
||||
|
||||
**Coverage**: Not available (no coverage tool run, per instructions)
|
||||
|
||||
---
|
||||
|
||||
### Spec Compliance Matrix
|
||||
|
||||
| Requirement | Scenario | Test | Result |
|
||||
|-------------|----------|------|--------|
|
||||
| Android Auto Discovery Declaration | Android Auto scans installed apps | static review -- automotive_app_desc.xml + manifest meta-data | COMPLIANT (static-only, no Android build env; documented exemption) |
|
||||
| Browsable Media Tree | Car requests the root | navegacion_auto_test.dart, raiz test | COMPLIANT |
|
||||
| Browsable Media Tree | Car requests a folder with no stations | navegacion_auto_test.dart, hijos empty-list test | COMPLIANT |
|
||||
| Browsable Media Tree | Browse requested before app state is loaded | none at handler level -- no test instantiates PluriWaveAudioHandler | PARTIAL -- pure-logic (hijos/raiz never throw on empty input) is tested; the handler getChildren try/catch and null-fuente fallback is only statically reviewed, never executed |
|
||||
| Playable Item Metadata | Station has a remote logo | navegacion_auto_test.dart, itemEmisora remote favicon test | COMPLIANT |
|
||||
| Playable Item Metadata | Station has no logo | navegacion_auto_test.dart, itemEmisora default-art fallback test | COMPLIANT |
|
||||
| Media Item Resolution by ID | Known station id | navegacion_auto_test.dart, resolver known-uuid test | COMPLIANT (pure resolver only; handler getMediaItem itself is static-review-only) |
|
||||
| Media Item Resolution by ID | Unknown station id | navegacion_auto_test.dart, resolver null-cases test | COMPLIANT |
|
||||
| Play by Media ID Reuses Existing Playback Path | User selects a station in the car | navegacion_auto_test.dart, reproducirPorMediaId delegates test | COMPLIANT |
|
||||
| Play by Media ID Reuses Existing Playback Path | Unknown or stale media id | navegacion_auto_test.dart, reproducirPorMediaId no-op test | COMPLIANT |
|
||||
| Playback State Synchronization | User pauses from the car | estado_radio_test.dart, reconciliation test | COMPLIANT (covers the net-new reconcile behavior; the underlying shared PlaybackState sync is pre-existing/unchanged) |
|
||||
| Playback State Synchronization | User pauses from the phone | pre-existing, unmodified code path | COMPLIANT (no new behavior -- audio pipeline untouched, not in scope for new tests) |
|
||||
|
||||
**Compliance summary**: 11/12 scenarios fully compliant, 1/12 PARTIAL (handler-level cold-start browse path is untested by execution, only by static review -- pre-declared limitation).
|
||||
|
||||
---
|
||||
|
||||
### Correctness (Static -- Structural Evidence)
|
||||
|
||||
| Requirement | Status | Notes |
|
||||
|------------|--------|-------|
|
||||
| Android Auto Discovery Declaration | Implemented | automotive_app_desc.xml declares uses name=media; manifest meta-data added inside application tag, correctly pointing to xml/automotive_app_desc |
|
||||
| Browsable Media Tree | Implemented | ConstructorArbolAuto raiz/hijos; handler dispatches by parentMediaId, wrapped in try/catch |
|
||||
| Playable Item Metadata | Implemented | itemEmisora art fallback verified against actual PNG at android/app/src/main/res/drawable/default_station_art.png; android.resource URI authority matches applicationId/namespace in build.gradle |
|
||||
| Media Item Resolution by ID | Implemented | resolver plus getMediaItem override, both null-safe |
|
||||
| Play by Media ID Reuses Existing Playback Path | Implemented | playFromMediaId delegates to reproducirPorMediaId then playMediaItem (unchanged internal path); servicio_audio.dart diff is 91 insertions and 0 deletions -- confirmed purely additive |
|
||||
| Playback State Synchronization | Implemented | Reconcile branch added in the estado listener; traced cambiarFuente in servicio_audio.dart -- emisoraActual is set synchronously before any playbackState/estadoStream emission for a given play attempt, so the reconcile branch is a provable no-op on phone-initiated plays (uuid already matches) and only fires for genuinely car-initiated changes -- confirms it cannot fight the phone UI |
|
||||
|
||||
---
|
||||
|
||||
### Coherence (Design)
|
||||
|
||||
| Decision | Followed? | Notes |
|
||||
|----------|-----------|-------|
|
||||
| getChildren data source (cold-start safe) | Yes | FuenteEmisorasAuto and registrarFuenteNavegacion mirrors registrarHandler; local reads never throw (favorites/custom wrapped in try/catch) |
|
||||
| media-id scheme | Yes | emisora colon uuid folders vs bare constants, exactly as designed |
|
||||
| default artwork delivery | Yes | android.resource URI, matches applicationId; PNG confirmed present at exact path |
|
||||
| which stations surface and ordering | Yes | ordenarEmisoras by calidad plus cap at 50, verified by the 60-to-50 test |
|
||||
| playback coherence with EstadoRadio | Yes | Reconcile branch added exactly as designed, traced safe (see above) |
|
||||
| content style (optional) | Yes | CONTENT_STYLE extras present, standard Android Auto keys |
|
||||
| Interface: actualizarSnapshot on FuenteEmisorasAuto abstract class | Deviated | Design code block only showed it on the local impl; promoted to the interface with a default no-op body -- self-documented in apply-progress as required for the fuenteAuto optional-call to type-check through the abstract-typed field. Judged: acceptable improvement, additive and behavior-neutral. |
|
||||
| Default artwork asset content | Deviated | PNG is a copy of ic_launcher.png (the launcher icon), not a bespoke no-logo placeholder. Self-documented as a known gap in apply-progress and tasks.md 6.3. Judged: acceptable to land, should fix before shipping -- functionally correct (loadable, right path and dimensions) but will show the full app icon as station art for any station without a logo, which is a UX smell. |
|
||||
|
||||
---
|
||||
|
||||
### Issues Found
|
||||
|
||||
**CRITICAL** (must fix before archive):
|
||||
None.
|
||||
|
||||
**WARNING** (should fix):
|
||||
1. Handler-level browse overrides have zero execution coverage. No test in the repo instantiates PluriWaveAudioHandler (confirmed via search), so getChildren, getMediaItem, and playFromMediaId -- including their try/catch cold-start-safety wrapper -- are only verified by static and manual review, never by a running test. This was pre-declared in tasks.md Phase 3 header as a scope limitation (no existing pattern instantiates PluriWaveAudioHandler in flutter test), and the pure logic underneath (ConstructorArbolAuto, reproducirPorMediaId) is thoroughly unit-tested -- but the actual dispatch and error-swallowing at the handler boundary is unverified by execution. Recommend a lightweight handler-instantiation test in a follow-up if the pattern is ever established, or explicit acceptance of this gap.
|
||||
2. default_station_art.png is a placeholder copy of the launcher icon, not a genuine no-logo graphic. Self-flagged by the implementer. Functionally correct and safe to land, but should be swapped before shipping to end users (tracked as an open design question already).
|
||||
3. docs/android-auto.md (new, roughly 280 lines, Spanish-language planning/how-to guide) is not listed in design.md File Changes table nor tasks.md, and is not referenced by any code or spec requirement. It reads like leftover exploration/onboarding content rather than a deliverable of this change. Harmless (documentation only, no code/behavior impact), but it is a stray artifact relative to the documented scope -- recommend either folding it into the SDD artifact trail (referencing it from design.md) or dropping it from this commit.
|
||||
|
||||
**SUGGESTION** (nice to have):
|
||||
1. The spy class in estado_radio_test.dart (FuenteEmisorasAutoEspia) implements FuenteEmisorasAuto but its actualizarSnapshot override is missing the override annotation (the other 3 methods have it). Cosmetic only -- would surface on a real flutter analyze pass as a lint, not an error.
|
||||
2. flutter analyze and a genuine full flutter test run are still pending (both explicitly deviated in tasks 7.1/7.2 due to this environment hanging on those commands) -- recommend running both in CI or a working Android toolchain before merge, as already noted by the implementer.
|
||||
3. An open question remains from design.md (confirm the system art loader accepts android.resource URIs) -- no automated way to verify without a DHU session; prioritize this manual check before shipping since a rejected URI would silently show broken art for every logo-less station.
|
||||
|
||||
---
|
||||
|
||||
### Verdict
|
||||
PASS WITH WARNINGS
|
||||
|
||||
0 CRITICAL, 3 WARNING, 3 SUGGESTION. All spec requirements are implemented and 52 of 52 targeted-plus-safety-net tests pass (independently re-run and confirmed by this reviewer). The audio pipeline (playMediaItem, just_audio wiring, reconnection controller) is verifiably untouched -- servicio_audio.dart diff is purely additive (91 insertions, 0 deletions). Manifest change is a clean, isolated addition with no regression to existing services or receivers. Handler override signatures were checked byte-for-byte against the installed audio_service 0.18.18 package source and match exactly. No stray debug prints or AI attribution were found anywhere in the diff. The one accidental side effect found during verification (pubspec.lock drift caused by this verification own flutter test invocation) was identified and reverted before this report was written, so it will not be part of the upcoming commit. Remaining warnings are pre-declared, environment-driven, or cosmetic -- none block the single-commit size-exception delivery, but the launcher-icon placeholder art and the untested handler boundary should be tracked for a fast follow-up.
|
||||
Reference in New Issue
Block a user