223 lines
20 KiB
Markdown
223 lines
20 KiB
Markdown
# 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.
|