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.
13 KiB
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),CacheMetadatosSesionLRU, 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 ONEMediaItemand hands it toplayMediaItem.PluriWaveAudioHandler._cambiarFuente(servicio_audio.dart) does_recrearPlayer()+ a single_player.setUrl(mediaItem.id). There is NOjust_audioConcatenatingAudioSource, NOaudio_servicequeue, NOskipToNext, 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, andskipToNext/skipToPreviouswired 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
NodoLocals), sorted by name for sequential and randomized (seeded, deterministically testable) for shuffle. - Content-URI resolution per advance via the existing
resolvePlayableUrichannel 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. ConcatenatingAudioSourcerebuild 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
.arbkeys, 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,playFromMediaIdbranch 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) fromNodoLocals, 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 existinghijos+resolvePlayableUri.android/.../MainActivity.kt: no change anticipated (lazy per-advance resolution reusesresolvePlayableUri).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.completedhandling 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
.arbkeys.
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.