Files
pluriwave/openspec/changes/archive/2026-07-20-android-auto-local-music-phase3/apply-progress.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

238 lines
13 KiB
Markdown

# Apply Progress: Android Auto Local Music — Phase 3 (Folder Queue + Shuffle)
## Status: DONE — all executable tasks complete in a single pass (single PR, `size:exception` per user's explicit choice)
Batch: FIRST and ONLY batch. All phases (0-5) implemented in one pass per
the user's explicit delivery-strategy choice (single PR with `size:exception`,
acknowledged as the highest-risk change of the session). Phase 6 items are
manual follow-up (not executable by this agent) or confirmed not applicable.
## Tasks completed: 24 / 26 executable checklist items (Phase 6.1 is
explicitly manual follow-up per tasks.md; Phase 6.2 confirmed not applicable,
no code change needed)
- Phase 0 (Regression Baseline): 0.1, 0.2 — DONE
- Phase 1 (Pure Foundations — ColaLocal/decidirAvanceCola/avanceEsValido): 1.1, 1.2, 1.3 — DONE
- Phase 2 (Shuffle & Collision): 2.1, 2.2, 2.3 — DONE
- Phase 3 (Handler Isolation Wiring, static-review-only): 3.1-3.8 — DONE
- Phase 4 (Browsable/Playable Actions): 4.1, 4.2, 4.3 — DONE
- Phase 5 (Open Question + Final Regression): 5.1-5.5 — DONE
- Phase 6 (Deviations/Manual Follow-up): 6.1 deferred (manual, `flutter analyze`/`--coverage`/`gen-l10n` not run per strict-TDD scope); 6.2 confirmed N/A (no native/manifest change)
## Files changed
### New
- `lib/servicios/cola_local.dart``ColaLocal` (immutable queue holder),
`DecisionAvanceCola` enum, `decidirAvanceCola`, `avanceEsValido`. Pure
Dart, no `just_audio` import, mirrors the `ControladorReconexion`
extraction pattern so it is fully unit-testable without instantiating
`PluriWaveAudioHandler`.
- `test/servicios/cola_local_test.dart` — 18 tests covering `ColaLocal`
nav/boundaries, `decidirAvanceCola`'s 3-way gate, `avanceEsValido`'s
`identical()` guard.
- `test/servicios/controlador_reconexion_local_test.dart` — 3 tests proving
the design's open-question resolution: a dead local track retries up to
the DEFAULT `maxReintentos: 5` via the existing `ControladorReconexion`
(source-agnostic, no special-casing), fails cleanly on the 6th failure
(no 6th retry, no hang, no crash). Deliberately kept as a SEPARATE file
from `servicio_audio_reconnect_test.dart` so that protected suite's exact
pass count stays byte-identical (see "Regression discipline" below).
### Modified
- `lib/servicios/navegacion_auto.dart`
- `pistasEnOrdenNombre`, `mezclarFisherYates`, `pistasEnOrdenAleatorio`
(Fisher-Yates over the canonical name-sorted order, injected `Random`).
- Two new `ConstructorArbolAuto` prefixes:
`carpeta_local_reproducir:`/`carpeta_local_aleatorio:`, both
PLAYABLE (unlike every other `carpeta_local_*` prefix), with
predicates + strip-by-length codec, collision-proven at index 14
against the 3 sibling `carpeta_local_*` prefixes and index 13 against
`carpeta_local:`.
- `itemsLocales`' page-0 prepend block extended: "Reproducir
carpeta"/"Reproducir aleatorio" prepended BEFORE the sort/bucket nav
entries (per Design ADR-5's explicit ordering), guarded by
`totalPistas > 0` — absent for a folder with only subfolders.
- `reproducirCarpetaLocal` (fetches children, filters/orders, calls
`iniciarCola`; no-op on empty/unresolvable folder) and
`construirMediaItemColaLocal` (resolves a queue track's content URI +
MediaItem, reusing `reproducirPistaLocal`'s title derivation).
- Test file `test/servicios/navegacion_auto_test.dart`: 139 tests total
(was ~120 before this change) — added new groups for the above, PLUS
updated 4 pre-existing tests whose exact item counts/positions shifted
because the new page-0 prepend items are real production behavior
changes (not a regression — this file is not one of the 4 protected
suites):
- "exactamente 50 items": `hasLength(51)``hasLength(53)`.
- "51 a 100 items" pagina0: `hasLength(56)``hasLength(58)`.
- "page-0 mode/bucket prepend" (>150-pistas case): first-5-non-playable
loop replaced with explicit checks (index 0/1 = the two new playable
action items, index 2-6 = calidad/buckets).
- `lib/servicios/servicio_audio.dart` (HIGHEST RISK, static-review-only —
no handler instantiation possible in unit tests) —
- New fields: `ColaLocal? _colaLocal`, `bool _avanzandoCola`.
- `playMediaItem` split: public method now ALWAYS clears
`_colaLocal`/`_avanzandoCola` FIRST, then delegates to new private
`_encolarCambioFuente` (body is the previous `playMediaItem`
implementation, byte-identical).
- New `_reproducirEntradaCola(NodoLocal, {ColaLocal? colaEsperada})`
the ONLY other caller of `_encolarCambioFuente` besides public
`playMediaItem`. `colaEsperada`, when provided, applies the
`avanceEsValido` mid-await race guard AFTER resolving the content URI
and BEFORE playing — functionally identical to the design's advance-flow
pseudocode, refactored so the guard lives inside the sole resolve+play
entry point (avoids a second, redundant URI resolve).
- `_manejarFinPista(ProcessingState)` wired as the FIRST line of the
`playerStateStream` listener, calling `decidirAvanceCola`; sets the
`_avanzandoCola` latch synchronously before any await for the
`avanzar` case; the radio `playing && ready` reset branch gained one
additional line (`_avanzandoCola = false`) alongside its two existing
untouched statements.
- `_desactivarCola()`, `_iniciarColaLocal(List<NodoLocal>)` (the
`iniciarCola` seam for `reproducirCarpetaLocal`).
- `stop()` now also calls `_desactivarCola()``onTaskRemoved` inherits
this for free (it calls `stop()`).
- `skipToNext`/`skipToPrevious` overrides added: no-op when
`_colaLocal == null`; `skipToNext` past the last track deactivates +
stops (Design ADR-4, no wraparound); `skipToPrevious` at index 0
clamps to 0 (restarts the track).
- `playerStateStream`'s `playbackState.add(...)` gated: `controls`/
`systemActions`/`androidCompactActionIndices` only add
skip-previous/skip-next when `_colaLocal != null`; the `null` branch is
byte-identical to the pre-change list (verified: same elements, same
order, same compact index).
- `playFromMediaId` gained a THIRD branch (after `eq_preset:`/`pista:`,
before the station fallthrough) for the two new action prefixes,
calling `reproducirCarpetaLocal` with `iniciarCola: _iniciarColaLocal`.
- Confirmed UNTOUCHED by this change (verified via diff read): `_cambiarFuente`,
`ControladorReconexion` usage/`_reconexion` field, `_esErrorDeRed`,
`_gestionarErrorReproduccion`, `_intentarReconexion`, `_reintentarFuente`.
- `openspec/changes/android-auto-local-music-phase3/tasks.md` — all 24
executable checklist items marked `[x]` with brief inline notes; 6.1 left
unchecked (manual follow-up, explicitly out of this agent's scope).
- `openspec/changes/android-auto-local-music-phase3/design.md` — both Open
Questions marked `[x]` confirmed, with the apply-time resolution recorded
inline.
## Test results (independently re-confirmed via a single combined run)
Combined run of every touched/new test file
(`cola_local_test.dart` + `navegacion_auto_test.dart` +
`controlador_reconexion_local_test.dart` + the 4 protected regression
suites), `flutter test ... --concurrency=1 --timeout=60s`:
```
+181: All tests passed!
```
Per-file breakdown (from the same run, cumulative counters):
| File | Tests | Pass | Fail |
|---|---|---|---|
| `cola_local_test.dart` (new) | 18 | 18 | 0 |
| `navegacion_auto_test.dart` (modified) | 139 | 139 | 0 |
| `controlador_reconexion_local_test.dart` (new) | 3 | 3 | 0 |
| `servicio_audio_reconnect_test.dart` (protected) | 8 | 8 | 0 |
| `servicio_audio_session_test.dart` (protected) | 5 | 5 | 0 |
| `servicio_audio_source_switch_test.dart` (protected) | 3 | 3 | 0 |
| `servicio_audio_eq_reapply_test.dart` (protected) | 5 | 5 | 0 |
| **Total** | **181** | **181** | **0** |
## Regression discipline (Phase 0 baseline vs Phase 5.4 final — the
non-negotiable gate)
Baseline (Phase 0.1, captured BEFORE any code change):
`servicio_audio_reconnect_test.dart` = 8, `servicio_audio_session_test.dart`
= 5, `servicio_audio_source_switch_test.dart` = 3,
`servicio_audio_eq_reapply_test.dart` = 5. Total 21, 0 failures.
Final (Phase 5.4, re-run after ALL code changes, same command, same order):
8 / 5 / 3 / 5 = 21, 0 failures. **EXACT MATCH — same test names, same
order, same counts, byte-identical.** No regression in any of the 4
protected suites. Verified twice independently (once right after the
`servicio_audio.dart` wiring, once as the final Phase 5.4 gate) with
identical results both times.
One deliberate, disclosed exception to "nothing about the 4 protected files
changes": Phase 5.2 required a "5 failures → agotado, no 6th retry, no
hang/crash" test using the DEFAULT `maxReintentos: 5` (the existing test at
`servicio_audio_reconnect_test.dart:115-137` uses an abbreviated
`maxReintentos: 2`). Rather than add a 9th test into the protected file
(which would have changed its pass count from 8 to 9 and technically
violated the "exact pass count" gate), this was added to a NEW,
purpose-built file (`controlador_reconexion_local_test.dart`) instead,
testing the same `ControladorReconexion` class (already fully
unit-testable, source-agnostic by construction). This satisfies task 5.2's
substance without touching the protected file's contents or count at all.
## Diff size
Estimated: prod ~470 lines (`cola_local.dart` ~100 new,
`navegacion_auto.dart` ~180 added, `servicio_audio.dart` ~190 added/changed
net of the split), tests ~950 lines across 3 new/modified test files. Total
~1400+ lines — over the original ~850-950 estimate in tasks.md's Review
Workload Forecast, primarily because of the number of existing
`navegacion_auto_test.dart` assertions that needed explicit updates once
the page-0 prepend behavior changed, plus the dedicated open-question
regression file. Delivered as a single PR with `size:exception` per the
user's explicit, informed choice this session (told this was the
highest-risk change of the day, chose to proceed as one PR anyway).
## Deviations from design (disclosed)
1. **Mid-await race guard placement**: design's ADR-3 pseudocode describes
the `avanceEsValido` guard as gating whether `_reproducirEntradaCola`
gets called at all (guard, THEN call). Implemented instead as an
optional `colaEsperada` parameter checked INSIDE
`_reproducirEntradaCola`, immediately after its own URI resolve and
before calling `_encolarCambioFuente`. This is functionally identical
(same causal order: capture → await → guard → play) and avoids a
redundant second `uriContenidoDePista` channel call, while preserving
task 3.2's stricter acceptance criterion ("`_reproducirEntradaCola` is
the ONLY other caller of `_encolarCambioFuente`").
2. **Resolve-failure handling not explicit in design's happy-path
pseudocode**: when an auto-advance's `construirMediaItemColaLocal`
resolves to `null` (dead/moved/permission-revoked local file, distinct
from a `PlayerException` during actual playback), the queue is
deactivated and stopped rather than left with a stuck `_avanzandoCola`
latch. This is a defensive addition consistent with ADR-4's "no
inconsistent state" principle, not explicitly scripted in the design's
pseudocode.
3. **`androidCompactActionIndices` dynamic index**: not explicitly called
out in tasks.md's acceptance criteria for 3.7, but necessary for
correctness — when `_colaLocal != null`, `skipToPrevious` is prepended
at index 0, so the compact-view highlighted action (play/pause) index
shifts to `1` (`colaActiva ? 1 : 0`) to keep pointing at play/pause
instead of skip-previous. The `_colaLocal == null` branch remains `[0]`,
unchanged.
4. **Phase 5.2 test placement** — see "Regression discipline" above: added
to a new file, not the protected `servicio_audio_reconnect_test.dart`,
to preserve that suite's exact pass count.
## Risks / follow-ups for verify phase
- Phase 3's handler wiring (the highest-risk part of this change) is
static-review-only by design — `PluriWaveAudioHandler` cannot be
instantiated in unit tests. Correctness there rests on (a) the pure
`cola_local.dart` functions being fully unit-tested and (b) careful
manual diff review of the wiring, both completed in this pass, but a
human/verify-phase re-read of `servicio_audio.dart`'s diff is still
warranted given the stakes.
- Phase 6.1 (`flutter analyze`, `flutter test --coverage`, `flutter
gen-l10n`) was explicitly out of scope for this apply pass (strict-TDD
mode: only target test files were run) and remains a manual follow-up
before merge.
- On-device / DHU manual QA for real car transport buttons and a genuine
`ProcessingState.completed` emission is out of scope here (session
precedent, per design's Testing Strategy table) and should be scheduled
separately.
## Skill compliance
- `literal-encoding`: scan run over all touched/new `.dart` files
(`cola_local.dart`, `navegacion_auto.dart`, `servicio_audio.dart`,
`cola_local_test.dart`, `navegacion_auto_test.dart`,
`controlador_reconexion_local_test.dart`) for `Ã.|â€|` — 0 matches,
passed.
- `work-unit-commits`: working tree left UNSTAGED per instruction — the
orchestrator handles commit/push with a `[size:exception]` tag. No
`git commit` was run by this agent.