# 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`.