Files
pluriwave/openspec/changes/archive/2026-07-20-android-auto-local-music-phase3/tasks.md
T
FreeTLab 49def4b276
Build & Deploy PluriWave / Análisis de código (push) Successful in 28s
Build & Deploy PluriWave / Build APK + AAB release (push) Failing after 1m18s
docs(openspec): archive android-auto-local-music-phase3
Merges its delta requirements into the android-auto-media base spec.
Completes the 3-phase local-music-in-Android-Auto feature. Standing
pre-release gate: on-device/DHU validation of every native surface
built across all 4 phases (SAF picking, metadata extraction, art
cache, queue/shuffle handler wiring) is still outstanding.
2026-07-20 01:15:46 +02:00

11 KiB

Tasks: Android Auto Local Music — Phase 3 (Folder Queue + Shuffle)

CRITICAL — Testability Gap vs Design (must-read before Phase 2/3)

PluriWaveAudioHandler cannot be instantiated in unit tests (its constructor builds a real just_audio.AudioPlayer needing platform MethodChannels — confirmed by servicio_audio_eq_reapply_test.dart:12-15 and servicio_audio_source_switch_test.dart:13; no test in the repo constructs it directly). Design's "Handler (integration)" test row ("mocked player/fuente") is therefore not achievable as written. Fix: extract the queue-advance decisions into a NEW pure module (lib/servicios/cola_local.dart), mirroring how ControladorReconexion was extracted from the same file — pure logic gets real unit tests; the actual field mutation on PluriWaveAudioHandler is static-review-only, verified against explicit acceptance criteria below (same precedent as the EQ re-apply listener wiring).

Review Workload Forecast

Field Value
Estimated changed lines ~850-950 (prod ~440: cola_local.dart ~90, navegacion_auto.dart ~150, servicio_audio.dart ~150; tests ~470)
400-line budget risk High
Chained PRs recommended Yes
Suggested split PR 1 (queue foundation + sequential play + transport) → PR 2 (shuffle variant)
Delivery strategy ask-on-risk
Chain strategy pending (ask user)

Decision needed before apply: Yes Chained PRs recommended: Yes Chain strategy: pending 400-line budget risk: High

Split recommendation overrides line-count alone: even if the line count came in lower, split PR 1/PR 2 anyway — PR 1 is the only unit that touches PluriWaveAudioHandler's shared error/completion listener; isolating it lets a reviewer verify the radio-regression guarantees without shuffle-logic noise.

Suggested Work Units

Unit Goal Likely PR Notes
1 ColaLocal + pure advance-decision + handler isolation/auto-advance/transport wiring + "Reproducir carpeta" + full regression re-run PR 1 Gate: all 4 existing suites byte-identical pass count before merge
2 Fisher-Yates shuffle + "Reproducir aleatorio" + collision test PR 2, base = PR 1 branch No handler changes beyond routing the new prefix through the existing seam

Phase 0: Regression Baseline (run BEFORE any code change)

  • 0.1 Run test/servicios/servicio_audio_reconnect_test.dart, servicio_audio_session_test.dart, servicio_audio_source_switch_test.dart, servicio_audio_eq_reapply_test.dart; record exact pass counts per file. Baseline: 8 / 5 / 3 / 5 = 21 total, 0 failures.
  • 0.2 Saved the baseline pass counts as the comparison target for Phase 5.4 (see apply-progress.md).

Phase 1: Pure Foundations (lib/servicios/cola_local.dart, new)

  • 1.1 RED: write ColaLocal tests — conSiguiente/conAnterior on empty, single-item, mid-list, and boundary (first/last index) cases. GREEN: implement immutable ColaLocal per design's contract. REFACTOR. lib/servicios/cola_local.dart, test/servicios/cola_local_test.dart.
  • 1.2 RED: write decidirAvanceCola tests — pure function (colaLocal, avanzandoCola, trackCompletado) -> DecisionAvanceCola {ninguna|avanzar|desactivar}. Cover: colaLocal==nullninguna (radio-isolation proxy, since radio never sets _colaLocal); avanzandoCola==trueninguna (double-advance latch, simulate two rapid trackCompletado calls); conSiguiente()==nulldesactivar. GREEN: implement in cola_local.dart (no just_audio import — caller maps ProcessingState.completed to the bool). REFACTOR.
  • 1.3 RED: write avanceEsValido(colaLocalActual, siguienteEsperado) tests — identical() on two structurally-equal-but-distinct ColaLocal instances must return false (guards against an accidental == swap in the mid-await race guard). GREEN: implement as thin identical() wrapper. REFACTOR. Gotcha found: const instances canonicalize in Dart, must use non-const ColaLocal(...) for the distinct-instance test case (documented in the test and saved to engram).

Phase 2: Pure Foundations — Shuffle & Collision (do NOT block Phase 3-5)

  • 2.1 RED: mezclarFisherYates(List<NodoLocal>, Random) tests — determinism under fixed seed (Random(42) twice → identical output order) and distribution sanity (1000 runs of a 5-item list, assert every item appears in every position at least once). GREEN: implement Fisher-Yates over the name-sorted list. REFACTOR.
  • 2.2 RED: pistasEnOrdenAleatorio/pistasEnOrdenNombre tests. GREEN.
  • 2.3 RED: collision-freedom test — for each of carpeta_local_reproducir: and carpeta_local_aleatorio:, assert startsWith is false against ALL 8 existing prefixes (emisora:, grupo:, eq_preset:, carpeta_local:, carpeta_local_pag:, carpeta_local_ord:, carpeta_local_bucket:, pista:) and vice-versa; assert first diverging char at index 14 (r/a vs p/o/b). GREEN: add the two const prefixes + predicates + strip-by-length codec in navegacion_auto.dart.

Phase 3: Handler Isolation Wiring — servicio_audio.dart (HIGHEST RISK)

Static-review-only (no handler instantiation possible). Verify each item by reading the diff against its acceptance criterion; do not mark done without the read.

  • 3.1 Split playMediaItem: public method clears _colaLocal = null; _avanzandoCola = false; FIRST, then delegates to new private _encolarCambioFuente(item) (current body of playMediaItem minus the two new clear lines). Acceptance: every existing external call site (ServicioAudio.reproducir, reproducirPorMediaId, reproducirPistaLocal, eq_preset non-path) still routes through the public method unchanged.
  • 3.2 Add _reproducirEntradaCola(NodoLocal): sets/keeps _colaLocal, calls _encolarCambioFuente WITHOUT clearing. Acceptance: this is the only other caller of _encolarCambioFuente besides public playMediaItem.
  • 3.3 Wire _manejarFinPista(ProcessingState proc) as the first line of the existing playerStateStream listener (servicio_audio.dart:253), calling decidirAvanceCola (1.2) with trackCompletado: proc == ProcessingState.completed. Acceptance: for every decision, avanzar sets _avanzandoCola = true synchronously before any await; desactivar calls _desactivarCola() (clears _colaLocal) + stop(); ninguna is a no-op — radio's playing && ready reset branch (line 256-260) is untouched.
  • 3.4 Wire the advance flow per ADR-3: compute siguiente = cola.conSiguiente(); capture it; await uriContenidoDePista; guard with avanceEsValido(_colaLocal, siguiente) (1.3) before calling _reproducirEntradaCola; reset _avanzandoCola when the next track reaches playing && ready.
  • 3.5 stop() (servicio_audio.dart:702) clears _colaLocal = null; _avanzandoCola = false; alongside existing reconnect-cancel logic. Acceptance: onTaskRemoved (calls stop()) inherits this for free.
  • 3.6 skipToNext/skipToPrevious overrides: no-op when _colaLocal == null; otherwise call conSiguiente/conAnterior (1.1 already proves clamping at boundaries) and _reproducirEntradaCola.
  • 3.7 Gate controls/systemActions in the playerStateStream listener: add MediaControl.skipToPrevious/skipToNext only when _colaLocal != null. Acceptance: read the diff and confirm the _colaLocal == null branch is byte-identical to the pre-change control/action list (radio regression guard).
  • 3.8 eq_preset: branch in playFromMediaId (servicio_audio.dart:812-824) stays FIRST, unconditional return, untouched — confirm no _colaLocal reference was added to it.

Phase 4: Browsable/Playable Action Items — navegacion_auto.dart

  • 4.1 RED: itemsLocales page-0 prepend tests — folder with ≥1 direct audio child prepends "Reproducir carpeta" + "Reproducir aleatorio" (both playable: true, prefixed ids); folder with 0 direct audio children (only subfolders) prepends neither (empty-folder edge case). GREEN: extend the page-0 prepend block (navegacion_auto.dart:448-457) mirroring ofreceOrdenCalidad's guard shape.
  • 4.2 RED/GREEN: reproducirCarpetaLocal(id, {aleatorio, fuente, rng, iniciarCola}) — fetches fuente.hijos(docId), filters non-directory, sorts/shuffles, calls iniciarCola; no-op on empty/unresolvable folder (fake FuenteMusicaLocalAuto, no handler needed).
  • 4.3 Wire the two new prefixes into playFromMediaId (servicio_audio.dart:801) as a THIRD branch (after eq_preset: and pista:, before the station fallthrough), calling reproducirCarpetaLocal with iniciarCola = a private helper that sets _colaLocal and calls _reproducirEntradaCola (3.2). Static-review-only for this wiring line.

Phase 5: Open Question + Final Regression

  • 5.1 Confirm (static read, no code change): ControladorReconexion (controlador_reconexion.dart) has no source-type parameter in registrarFallo — it structurally cannot special-case a local-track error vs a radio error, so leaving it untouched is provably safe. Confirmed by direct read; untouched.
  • 5.2 Grep servicio_audio_reconnect_test.dart for existing maxReintentos/agotado coverage (confirmed present: lines 115-137, using an abbreviated maxReintentos: 2). The default-maxReintentos: 5 "no 6th retry, no hang/crash" case was missing, so it was ADDED — but in a NEW file (test/servicios/controlador_reconexion_local_test.dart), not inside the protected servicio_audio_reconnect_test.dart, so that suite's exact pass count (8) stays byte-identical for Phase 5.4's regression gate. 3 new tests there prove: 5 failures schedule 5 backoff retries, the 6th returns agotado with no new timer created, and all timers end cancelled (no hang).
  • 5.3 Updated design's Open Questions checklist (see design.md edit in this same apply pass): both items confirmed — ControladorReconexion left untouched; deactivate + stop on terminal error/unresolvable URI.
  • 5.4 Re-ran all 4 suites from Phase 0.1; EXACT pass counts match the Phase 0.2 baseline byte-for-byte: reconnect 8/8, session 5/5, source_switch 3/3, eq_reapply 5/5 — 21/21 total, 0 failures, same test names/order. No mismatch.
  • 5.5 Ran the literal-encoding corruption scan over all touched/new .dart files — no Ã.|â€| matches found.

Phase 6: Deviations / Manual Follow-up (not executable here)

  • 6.1 flutter analyze, flutter test --coverage, flutter gen-l10n — manual follow-up, not run by this agent (per strict-TDD scope: only the target test files were run, per instruction).
  • 6.2 Native Kotlin: none touched by this change (confirmed — design's File Changes table lists no native/manifest changes; no native file was touched during apply) — static-review only, no code change needed.