Files
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

233 lines
13 KiB
Markdown

# Proposal: Android Auto Local Music — Phase 3 (Folder-Scoped Queue + Shuffle)
## Status of the local-music feature (context)
This is the FINAL planned phase of local music in Android Auto. Shipped and
archived already:
- **Phase 1**: SAF folder pick, persisted URI permission, lazy per-level
DocumentFile traversal, `PistaLocal`/`NodoLocal`, filename titles, placeholder
art, browse + play of a single track.
- **Paging fast-follow**: on-demand `carpeta_local_pag:` "Más…" pagination
replacing silent 50-item truncation.
- **Phase 2**: real embedded metadata (native `readAudioMetadataBatch` /
`MediaMetadataRetriever`), `CacheMetadatosSesion` LRU, quality-sort
(`carpeta_local_ord:`), alphabetical name-buckets (`carpeta_local_bucket:`),
embedded album art.
The original Phase-1 proposal sketched Phase 3 as: **"subfolder scoping
refinements + shuffle + transport-control polish."** This proposal traces that
label against the live code and defines precisely what actually remains.
## Honest scope reconciliation (the key finding — read this first)
The original label reads like "two small UX toggles." It is not. Reading the
live playback pipeline shows the real gap:
- **Browsing nested subfolders is already done** (Phase 1's SAF tree traversal).
So "subfolder scoping refinements" is NOT about navigation — that works.
- **There is no multi-track playback of any kind.** `reproducirPistaLocal`
(`navegacion_auto.dart`) builds exactly ONE `MediaItem` and hands it to
`playMediaItem`. `PluriWaveAudioHandler._cambiarFuente`
(`servicio_audio.dart`) does `_recrearPlayer()` + a single
`_player.setUrl(mediaItem.id)`. There is NO `just_audio`
`ConcatenatingAudioSource`, NO `audio_service` `queue`, NO `skipToNext`, NO
auto-advance on track completion. The handler was built for RADIO — one live
stream at a time. **Tapping a local track plays that ONE track and then
stops.** There is no "play this folder", no "next track", nothing.
Therefore both original Phase-3 items collapse onto a single missing
foundation:
- **"subfolder scoping"** honestly resolves to: *play the audio tracks of a
chosen (sub)folder as a scoped queue* — a sequence bounded to that folder.
- **"shuffle"** is the randomized ordering of that same folder-scoped queue.
- **"transport-control polish"** is wiring next/previous so Android Auto's
transport controls advance within that local queue.
**Honest verdict: Phase 3 is ONE foundational feature — a local multi-track
queue inside the audio handler — with two browsable entry points (play folder /
shuffle folder) and transport wiring on top. Shuffle without a queue is
meaningless, so the queue is the real, unavoidable work. It is larger than the
label implies and it touches the app's most sensitive, most heavily-tested
component (the audio handler with its reconnect state machine and EQ chain).**
## Intent
**Problem**: Local music in Android Auto is browse-and-tap-one-track only. A
driver cannot start a folder playing and let it run; every track requires a new
tap, there is no continuous listening, and "shuffle my music" — the single most
common car-music expectation — is impossible because there is no queue to
shuffle.
**Why now**: This is the last planned phase; the browse/metadata/sort surface is
complete and the only remaining promised capability (shuffle, committed in the
very first proposal) requires the queue foundation this phase introduces.
**Success looks like**: From any local (sub)folder in Android Auto, the driver
can tap "Reproducir carpeta" to play that folder's tracks in order, or
"Reproducir aleatorio" to play them shuffled, with playback auto-advancing
track-to-track and the car's next/previous controls moving within the queue —
all reusing the existing EQ signal chain and buffering, with no regression to
radio playback.
## Scope — In (Phase 3)
- **Local multi-track queue foundation** in `PluriWaveAudioHandler`: an ordered
list of pending local tracks + current index, auto-advance to the next track
on completion, and `skipToNext`/`skipToPrevious` wired to move within it. A
mode boundary keeps this active ONLY for local-queue playback, never for
radio (live streams never "complete").
- **Two browsable, folder-scoped play actions**, prepended on page 0 of a local
folder (same prepend precedent as Phase 2's sort/bucket entries):
- **"Reproducir carpeta"** — the folder's direct audio children, name-ordered.
- **"Reproducir aleatorio"** — the same set, shuffled.
- **New playable media-id families** for those actions (design settles exact
strings; must be collision-free against the existing `pista:` / `emisora:` /
`grupo:` / `eq_preset:` / `carpeta_local[_pag|_ord|_bucket]:` prefixes,
reusing the proven strip-by-length + split-on-first-colon codec so raw SAF
documentIds with `:` / `/` survive verbatim).
- **Queue building** scoped to the tapped folder's DIRECT audio children
(non-directory `NodoLocal`s), sorted by name for sequential and randomized
(seeded, deterministically testable) for shuffle.
- **Content-URI resolution per advance** via the existing
`resolvePlayableUri` channel method — lazy, one track at a time as the queue
advances (no eager whole-folder resolution, no new native method).
- Pure-Dart unit tests for shuffle ordering (seeded), queue construction, action
media-id encode/decode + collision guards, and page-0 action prepend; handler
tests for completion-advance and skip within the local-queue mode boundary.
## Scope — Out (explicitly deferred / rejected)
- **Recursive whole-subtree queue.** The queue is scoped to the tapped folder's
direct audio children only — consistent with the lazy per-level enumeration
model and every prior phase's rejection of eager subtree scans. Enqueuing a
whole nested subtree would require recursive native traversal; out of scope.
- **OS-level shuffle toggle** (`AudioService.setShuffleMode` /
`AudioServiceShuffleMode`). The package supports it, but it presupposes a
persistent OS play-queue with a shuffle affordance in the transport UI — a
model this app's browse-tree, tap-to-select integration does not use (the
notification exposes only play/pause/stop). Shuffle is therefore exposed as a
browsable ACTION (the EQ-preset-folder precedent), not an OS toggle. Design
may revisit, but the browsable action is the recommended fit.
- **`ConcatenatingAudioSource` rebuild of the player pipeline** — see Approach;
recommended AGAINST in favor of the lighter app-managed queue.
- **Cross-folder / persisted / resumable queues**, queue reordering UI, repeat
modes. Not promised, not in scope.
- **New phone-UI strings / l10n work.** Car-tree labels are hardcoded Spanish
by established precedent ("Favoritos", "Música Local", "Más…", "Ordenar por
calidad"). "Reproducir carpeta" / "Reproducir aleatorio" are hardcoded Spanish
car labels — **NO new `.arb` keys, NO 13-locale change.** If design surfaces a
genuinely new PHONE-UI string, it must be scoped into all 13 locales
explicitly; none is anticipated.
## Approach (with rationale)
**Recommended: an app-managed local queue that reuses the existing single-source
pipeline — do NOT rebuild the player around `ConcatenatingAudioSource`.**
The handler recreates the player + EQ on every source change
(`_recrearPlayer()` inside `_cambiarFuente`) and wraps source switches in a
revision-guarded queue built for live-stream reconnect. That machinery is the
app's most delicate, most-tested code. Bolting a `just_audio`
`ConcatenatingAudioSource` + `audio_service` `queue` + `setShuffleMode` onto it
fights the recreate-per-source model and the reconnect state machine head-on —
high blast radius on the one component we least want to destabilize.
Instead, keep the single-source player and add a thin queue LAYER in the
handler: hold the ordered local-track list + index, listen for
`ProcessingState.completed` (which only fires for finite local files, never for
radio) to advance by re-driving the existing `_cambiarFuente`, and override
`skipToNext`/`skipToPrevious` to move the index and re-drive the same path. A
mode flag scopes all of this to local-queue playback so radio behavior is
byte-identical. This mirrors Phase-2/EQ philosophy: expose the capability
through the existing seams and a browsable action, rather than adopting a
platform model the app's integration doesn't actually use.
All ordering, queue-building, media-id codec, and action-item logic stays pure
Dart in `navegacion_auto.dart` (fully unit-testable, seeded shuffle). The
handler change is the only stateful/coupled part and gets its own focused tests
behind the mode boundary. No new native method (lazy `resolvePlayableUri` per
advance), no new dependency, no manifest/pubspec change anticipated.
The design phase must settle: (1) app-managed queue vs `ConcatenatingAudioSource`
(recommendation: app-managed); (2) exact completion/advance and skip semantics
against the reconnect machine; (3) the two new media-id prefixes and their
collision proof; (4) shuffle seed strategy for deterministic tests.
## Affected areas (anticipated — design confirms)
- `lib/servicios/servicio_audio.dart` (Modify): local-queue state, completion
auto-advance, `skipToNext`/`skipToPrevious`, mode boundary, `playFromMediaId`
branch for the new action ids. **The sensitive change — main risk surface.**
- `lib/servicios/navegacion_auto.dart` (Modify): action media-id prefixes +
predicates + codec, queue-building (name-order / seeded-shuffle) from
`NodoLocal`s, page-0 action-item prepend, play orchestration.
- `lib/servicios/musica_local_auto.dart` (Possibly): a folder-tracks helper if
the orchestration needs one; likely reuses existing `hijos` + `resolvePlayableUri`.
- `android/.../MainActivity.kt`: **no change anticipated** (lazy per-advance
resolution reuses `resolvePlayableUri`).
- `AndroidManifest.xml` / `pubspec.yaml` / `lib/l10n/*.arb`: **no change
anticipated.**
## Risks
- **Audio-handler blast radius (HIGH)**: the queue layer lives in the most
sensitive, most-tested component. Mitigation: strict mode boundary so radio is
provably untouched; app-managed layer over pipeline rebuild; focused
regression tests. This is the dominant risk.
- **Completion-advance vs reconnect machine (MED-HIGH)**: `ProcessingState.completed`
handling must not collide with the live-stream stall/retry logic. Design must
prove the two paths are disjoint (mode flag).
- **Not runtime-verifiable here (HIGH, session precedent)**: no DHU/on-device
Android Auto in this environment; queue/skip/completion behavior is
static-review + unit-test only, same as all prior native/handler work this
session. Real transport-control behavior in the car cannot be exercised here.
- **Scope/size (MED)**: honestly larger than the "subfolder + shuffle" label.
See estimate below.
- **Shuffle determinism (LOW)**: seed the shuffle so tests are stable; a
fixed/injectable seed avoids flaky ordering assertions.
## Size estimate & delivery (flag early, per session pattern)
Comparable to or larger than Phase 2 (~2000 lines) because it modifies the audio
handler core and adds handler-level tests on top of the pure-Dart surface.
**Realistically a single `size:exception` PR, consistent with every prior local-music
change this session (Phase 1 ~1300, paging ~800, Phase 2 ~2000, all
single size:exception PRs).**
Natural work-unit split if chaining is preferred: (1) **queue foundation +
"Reproducir carpeta" sequential playback + transport wiring** (the bulk / the
real foundation), then (2) **"Reproducir aleatorio" shuffle** (a thin
ordering variant once the queue exists). Shuffle is small once the foundation
lands. Surface this to the delivery-strategy guard before apply.
## Rollback plan
Additive. Rollback = remove the new action media-id prefixes/predicates/codec,
the queue-building and page-0 action prepend in `navegacion_auto.dart`, and the
queue layer (state, completion-advance, skip overrides, mode flag) in
`servicio_audio.dart`. Single-track `pista:` playback and all radio playback
revert untouched.
## Success criteria
- [ ] "Reproducir carpeta" and "Reproducir aleatorio" appear on page 0 of a
local folder that has audio tracks; absent when it has none.
- [ ] Tapping "Reproducir carpeta" plays the folder's direct audio children in
name order, auto-advancing track to track.
- [ ] Tapping "Reproducir aleatorio" plays the same set shuffled; ordering is
deterministic under a fixed seed in tests.
- [ ] Car next/previous transport controls move within the local queue.
- [ ] Radio playback, reconnect, and EQ behavior are provably unchanged (mode
boundary; regression tests green).
- [ ] Pure-Dart logic fully unit-tested; handler queue behavior tested behind
the mode boundary; native (if any) static-reviewed. No new `.arb` keys.
## Next recommended
`sdd-spec` and `sdd-design` can run in parallel from this proposal. Design must
settle the app-managed-queue vs `ConcatenatingAudioSource` decision, the
completion/skip semantics against the reconnect machine, and the two new
media-id prefixes with a collision proof before `sdd-tasks`.