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.
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
ColaLocaltests —conSiguiente/conAnterioron empty, single-item, mid-list, and boundary (first/last index) cases. GREEN: implement immutableColaLocalper design's contract. REFACTOR.lib/servicios/cola_local.dart,test/servicios/cola_local_test.dart. - 1.2 RED: write
decidirAvanceColatests — pure function(colaLocal, avanzandoCola, trackCompletado) -> DecisionAvanceCola {ninguna|avanzar|desactivar}. Cover:colaLocal==null→ninguna(radio-isolation proxy, since radio never sets_colaLocal);avanzandoCola==true→ninguna(double-advance latch, simulate two rapidtrackCompletadocalls);conSiguiente()==null→desactivar. GREEN: implement incola_local.dart(nojust_audioimport — caller mapsProcessingState.completedto the bool). REFACTOR. - 1.3 RED: write
avanceEsValido(colaLocalActual, siguienteEsperado)tests —identical()on two structurally-equal-but-distinctColaLocalinstances must returnfalse(guards against an accidental==swap in the mid-await race guard). GREEN: implement as thinidentical()wrapper. REFACTOR. Gotcha found:constinstances canonicalize in Dart, must use non-constColaLocal(...)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/pistasEnOrdenNombretests. GREEN. - 2.3 RED: collision-freedom test — for each of
carpeta_local_reproducir:andcarpeta_local_aleatorio:, assertstartsWithis 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/avsp/o/b). GREEN: add the twoconstprefixes + predicates + strip-by-length codec innavegacion_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 ofplayMediaItemminus the two new clear lines). Acceptance: every existing external call site (ServicioAudio.reproducir,reproducirPorMediaId,reproducirPistaLocal,eq_presetnon-path) still routes through the public method unchanged. - 3.2 Add
_reproducirEntradaCola(NodoLocal): sets/keeps_colaLocal, calls_encolarCambioFuenteWITHOUT clearing. Acceptance: this is the only other caller of_encolarCambioFuentebesides publicplayMediaItem. - 3.3 Wire
_manejarFinPista(ProcessingState proc)as the first line of the existingplayerStateStreamlistener (servicio_audio.dart:253), callingdecidirAvanceCola(1.2) withtrackCompletado: proc == ProcessingState.completed. Acceptance: for every decision,avanzarsets_avanzandoCola = truesynchronously before anyawait;desactivarcalls_desactivarCola()(clears_colaLocal) +stop();ningunais a no-op — radio'splaying && readyreset branch (line 256-260) is untouched. - 3.4 Wire the advance flow per ADR-3: compute
siguiente = cola.conSiguiente(); capture it;await uriContenidoDePista; guard withavanceEsValido(_colaLocal, siguiente)(1.3) before calling_reproducirEntradaCola; reset_avanzandoColawhen the next track reachesplaying && ready. - 3.5
stop()(servicio_audio.dart:702) clears_colaLocal = null; _avanzandoCola = false;alongside existing reconnect-cancel logic. Acceptance:onTaskRemoved(callsstop()) inherits this for free. - 3.6
skipToNext/skipToPreviousoverrides: no-op when_colaLocal == null; otherwise callconSiguiente/conAnterior(1.1 already proves clamping at boundaries) and_reproducirEntradaCola. - 3.7 Gate
controls/systemActionsin theplayerStateStreamlistener: addMediaControl.skipToPrevious/skipToNextonly when_colaLocal != null. Acceptance: read the diff and confirm the_colaLocal == nullbranch is byte-identical to the pre-change control/action list (radio regression guard). - 3.8
eq_preset:branch inplayFromMediaId(servicio_audio.dart:812-824) stays FIRST, unconditionalreturn, untouched — confirm no_colaLocalreference was added to it.
Phase 4: Browsable/Playable Action Items — navegacion_auto.dart
- 4.1 RED:
itemsLocalespage-0 prepend tests — folder with ≥1 direct audio child prepends "Reproducir carpeta" + "Reproducir aleatorio" (bothplayable: 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) mirroringofreceOrdenCalidad's guard shape. - 4.2 RED/GREEN:
reproducirCarpetaLocal(id, {aleatorio, fuente, rng, iniciarCola})— fetchesfuente.hijos(docId), filters non-directory, sorts/shuffles, callsiniciarCola; no-op on empty/unresolvable folder (fakeFuenteMusicaLocalAuto, no handler needed). - 4.3 Wire the two new prefixes into
playFromMediaId(servicio_audio.dart:801) as a THIRD branch (aftereq_preset:andpista:, before the station fallthrough), callingreproducirCarpetaLocalwithiniciarCola= a private helper that sets_colaLocaland 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 inregistrarFallo— 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.dartfor existingmaxReintentos/agotadocoverage (confirmed present: lines 115-137, using an abbreviatedmaxReintentos: 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 protectedservicio_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 returnsagotadowith 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 —
ControladorReconexionleft 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-encodingcorruption scan over all touched/new.dartfiles — 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.