Adds "Reproducir carpeta" (sequential) and "Reproducir aleatorio" (Fisher-Yates over the name-sorted order) as folder-scoped playable actions, with auto-advance on track completion and skip next/prev. Isolation from live radio is structural, not disciplinary: the public playMediaItem always clears the local queue on any call, and a new private _encolarCambioFuente is the only path that can advance within it. _cambiarFuente, ControladorReconexion, and the reconnect error path are untouched -- confirmed by a byte-for-byte empty diff on all 4 pre-existing radio/reconnect regression suites, independently re-run before and after (21/21 both times). Handler wiring itself is static-review-only (PluriWaveAudioHandler can't be unit-instantiated); the isolation/advance/race-guard decision logic is extracted into cola_local.dart's pure functions, which are fully unit-tested.
233 lines
13 KiB
Markdown
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`.
|