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

560 lines
33 KiB
Markdown

# Android Auto Media Specification
## Purpose
PluriWave MUST expose a browsable station tree and play-by-media-id interface to Android Auto (projected) via the existing `PluriWaveAudioHandler` (`audio_service` MediaBrowserService/MediaSession), so a driver can browse and play stations from the car head unit without rebuilding the audio pipeline.
## Requirements
### Requirement: Android Auto Discovery Declaration
The app MUST declare itself as a media app to Android Auto so the car head unit discovers it.
#### Scenario: Android Auto scans installed apps
- GIVEN PluriWave is installed on the phone
- WHEN Android Auto scans installed apps for car-app support
- THEN it finds `res/xml/automotive_app_desc.xml` declaring `<uses name="media"/>`
- AND the `AndroidManifest.xml` `<application>` block declares `<meta-data android:name="com.google.android.gms.car.application" android:resource="@xml/automotive_app_desc"/>`
- AND PluriWave appears in the car's media app list
### Requirement: Favorite Group Sub-Folders
The Android Auto browse tree MUST expose favorite groups (`GrupoFavoritos`, as already modeled by `Emisora.grupoFavoritosId` and surfaced by `EstadoRadio.gruposFavoritos`) as browsable, non-playable sub-folders reachable from the existing `Favoritos` folder, without altering the 3 root folders (`Favoritos`, `Todas las emisoras`, `Mis emisoras`).
#### Scenario: Car requests the Favoritos folder and groups exist
- GIVEN the user has one or more favorite groups with at least one member station each
- WHEN `getChildren` is called with the `Favoritos` folder id
- THEN it returns one non-playable folder `MediaItem` per surfaced group, in addition to (or instead of, per the design's structural decision) any ungrouped favorite stations
- AND each group folder's id follows a browsable `grupo:<id>` scheme distinct from the `emisora:<uuid>` playable-item scheme
#### Scenario: Car requests a group folder's stations
- GIVEN a favorite group folder with id `grupo:<id>` was returned under `Favoritos`
- WHEN `getChildren` is called with that `grupo:<id>` folder id
- THEN it returns the playable `MediaItem`s for exactly the stations whose `Emisora.grupoFavoritosId` matches `<id>`
- AND those items are sorted and capped using the same ordering and item-count rules already applied to the other folders (`ordenarEmisoras`, 50-item cap)
- AND selecting one of those items plays the corresponding station via the existing `emisora:<uuid>` playback path, unchanged
#### Scenario: Car requests an unknown or stale group id
- GIVEN a `grupo:<id>` id that does not match any group known to the current snapshot
- WHEN `getChildren` is called with that id
- THEN it returns an empty list, not an error
### Requirement: Empty Favorite Group Handling
The system MUST produce a browsable tree that never presents a user-selectable folder promising content it cannot deliver: for any favorite group with zero member stations, the tree MUST either omit that group's folder from `Favoritos`'s children, or include it and return an empty (not erroring) child list when browsed. The specific choice between omitting empty-group folders and showing-but-empty, and any related folder-count/flat-vs-nested structural decision for `Favoritos`, is deferred to `sdd-design`; whichever mechanism design selects MUST satisfy both scenarios below.
#### Scenario: Empty group folder is browsed (if shown)
- GIVEN a favorite group has zero member stations and the design's chosen mechanism surfaces it as a folder under `Favoritos`
- WHEN `getChildren` is called with that group's `grupo:<id>`
- THEN it returns an empty list, not an error
#### Scenario: No user-facing dead end
- GIVEN the full set of favorite groups, including any empty ones
- WHEN the `Favoritos` folder is browsed and then each of its returned children is browsed
- THEN no returned folder child ever throws, hangs, or surfaces a driver-facing error state
- AND the car head unit's total folder/item count presented under `Favoritos` remains within the driver-distraction-safe bounds design establishes
### Requirement: Browsable Media Tree
`getChildren` MUST return a browsable tree rooted at `AudioService.browsableRootId`, organized into non-playable folders (Favoritos, Todas las emisoras, Mis emisoras, Ecualizador, and the local-music root) containing playable items. Playable station items SHOULD carry an audio-quality subtitle when known. The `Favoritos` folder additionally MAY contain non-playable favorite-group sub-folders (see "Favorite Group Sub-Folders"); `Todas las emisoras` and `Mis emisoras` remain flat. The `Ecualizador` folder is flat, non-playable, and contains only the 6 fixed EQ preset items (see "EQ Preset Browsable Folder"). The local-music root folder is non-playable and may itself be nested (see "Local Music Browsable Tree").
(Previously: root contained exactly 3 folders — Favoritos, Todas las emisoras, Mis emisoras — with no EQ or local-music folder; Favoritos was a flat folder of playable station items only, with no sub-folder nesting.)
#### Scenario: Car requests the root
- GIVEN the car head unit connects and requests the root (`AudioService.browsableRootId`)
- WHEN `getChildren` is called with the root id
- THEN it returns five folder `MediaItem`s (Favoritos, Todas las emisoras, Mis emisoras, Ecualizador, and the local-music root), each with `playable: false`
#### Scenario: Car requests a folder with no stations
- GIVEN the user has zero favorite stations
- WHEN `getChildren` is called with the Favoritos folder id
- THEN it returns an empty list, not an error
#### Scenario: Browse requested before app state is loaded
- GIVEN the audio handler starts cold and station/favorites Provider state has not finished loading
- WHEN `getChildren` is called (root or any folder)
- THEN it returns a valid, possibly empty, list without throwing and without blocking or crashing the service
#### Scenario: Station has known codec and bitrate
- GIVEN a station's `Emisora.codec` and `Emisora.bitrate` are both known (non-null)
- WHEN it is mapped to a playable `MediaItem`
- THEN `displaySubtitle` SHALL contain a human-readable quality hint combining bitrate and codec (e.g. "128 kbps · MP3")
#### Scenario: Station has unknown codec or bitrate
- GIVEN a station's `Emisora.codec` or `Emisora.bitrate` (or both) is null/unknown
- WHEN it is mapped to a playable `MediaItem`
- THEN `displaySubtitle` SHALL omit the quality hint gracefully (no subtitle, or a subtitle with no quality fragment)
- AND the subtitle MUST NOT render literal placeholder text such as "null kbps" or "null · null"
#### Scenario: Ungrouped station appears exactly as before (regression guard)
- GIVEN a station's `Emisora.grupoFavoritosId` equals `GrupoFavoritos.sinAsignarId` (`'sin_asignar'`, the default when no group is assigned)
- WHEN the `Favoritos`, `Todas las emisoras`, or `Mis emisoras` folders are browsed
- THEN that station appears as a playable `emisora:<uuid>` item in exactly the same folder(s), position (subject to existing sort rules), title, art, and subtitle as it did before favorite-group folders were introduced
- AND its presence and shape are unaffected by the existence, emptiness, or content of any favorite group
### Requirement: EQ Preset Browsable Folder
The Android Auto browse tree MUST expose the 6 fixed EQ presets (`PresetEcualizador.presets`) as playable items inside the `Ecualizador` root folder, each using a distinct `eq_preset:<nombre>` media-id scheme, separate from `emisora:<uuid>` and `grupo:<id>`.
#### Scenario: Car requests the Ecualizador folder
- GIVEN the `Ecualizador` folder id was returned under the root
- WHEN `getChildren` is called with the `Ecualizador` folder id
- THEN it returns exactly 6 playable `MediaItem`s, one per `PresetEcualizador.presets` entry, titled with the preset's name
- AND each item's id is `eq_preset:<nombre>`, where `<nombre>` is that preset's unique name
### Requirement: EQ Preset Selection Applies Without Disturbing Playback
Selecting an `eq_preset:<nombre>` item MUST apply that preset immediately through the headless-safe EQ seam (e.g. `PluriWaveAudioHandler.aplicarPreset`), via a branch in `playFromMediaId` intercepted BEFORE the `emisora:`/`grupo:` routing. It MUST NOT call `playMediaItem`, MUST NOT mutate the now-playing `mediaItem`, and MUST NOT start, stop, restart, or otherwise alter current playback or playback position.
#### Scenario: User selects a preset while a station is playing
- GIVEN a station is currently playing and projected to the car
- WHEN the user taps an `eq_preset:<nombre>` item, resolved via `playFromMediaId`
- THEN the matching preset is applied via the headless-safe apply seam
- AND the currently playing station continues uninterrupted, with no change to playback position or now-playing metadata
#### Scenario: User selects a preset while nothing is playing
- GIVEN no station is currently playing
- WHEN the user taps an `eq_preset:<nombre>` item
- THEN the matching preset is applied via the headless-safe apply seam
- AND no playback starts as a result of the selection
#### Scenario: Unknown or stale preset id
- GIVEN `playFromMediaId` receives an `eq_preset:<nombre>` id whose `<nombre>` matches no entry in the current `PresetEcualizador.presets`
- WHEN resolution fails to find a matching preset
- THEN the selection is a no-op: no preset is applied, no playback state changes, and no unhandled exception propagates from the handler
### Requirement: EQ Preset Persistence and Phone/Car Parity
An EQ preset applied from the car MUST persist as the principal preset, with the same observable outcome as the phone's existing `ServicioEcualizador.guardarPrincipal`/`cambiarPresetPrincipal` path, and MUST be reflected on the phone. A principal preset changed on the phone MUST be observable from the car. The exact code path used to reach the headless-safe persistence seam from an Android Auto bind is deferred to `sdd-design`.
#### Scenario: Preset applied from the car persists for the phone
- GIVEN the user selects `eq_preset:<nombre>` from the car head unit
- WHEN the selection is processed
- THEN the preset is persisted as the principal preset
- AND opening the phone's EQ UI afterward shows that preset as the active/principal preset
#### Scenario: Preset applied from the phone is reflected for the car
- GIVEN the user changes the principal preset from the phone UI
- WHEN the car head unit subsequently observes EQ-related state through the browse/selection surface
- THEN the car-observable state reflects the phone's most recently applied principal preset
### Requirement: Active Preset Indication (Optional)
The system MAY indicate the active/principal preset within the `Ecualizador` folder's rows. Because the legacy `MediaBrowserService` browsable-item model has no native "selected item" affordance, any indication, if implemented, MUST be conveyed via a row `title` text convention (e.g. a marker prefix/suffix) rather than assuming a checkmark or selection icon exists. If a reliable title-text convention is too costly to maintain, this requirement MAY instead be satisfied by explicitly not implementing active-preset indication.
#### Scenario: Active preset is indicated via title convention (if implemented)
- GIVEN the currently applied principal preset is known when `Ecualizador`'s children are built
- WHEN the design's chosen title-text convention is applied
- THEN exactly one of the 6 preset rows' `title` carries the active-preset marker, uniquely identifying that preset among the 6
#### Scenario: Active preset indication is out of scope (if not implemented)
- GIVEN the design opts not to implement an active-preset marker
- WHEN the `Ecualizador` folder's children are built
- THEN all 6 preset rows are returned with their plain preset names, with no partial or inconsistent marking applied
### Requirement: Local Music Root Access and Permission Persistence
The system MUST let the user select a local-music root folder via SAF (`ACTION_OPEN_DOCUMENT_TREE`), persist the granted URI permission, and MUST NOT crash or leave the local-music root folder in a broken state if that permission is later lost or was never granted.
#### Scenario: User picks a local music root folder
- GIVEN the user opens the local-music setup flow
- WHEN they complete the SAF folder picker and grant access
- THEN the selected folder's URI permission is persisted
- AND the local-music root becomes browsable in Android Auto without re-prompting the picker
#### Scenario: Persisted permission survives app restart
- GIVEN a local music root was previously picked and its permission persisted
- WHEN the app is restarted (cold start)
- THEN the local-music root remains browsable in Android Auto without re-prompting the SAF picker
#### Scenario: Permission revoked or never granted
- GIVEN the SAF permission for the local-music root was revoked outside the app, or no root was ever picked
- WHEN the local-music root folder is browsed in Android Auto
- THEN `getChildren` returns an empty or explanatory list, not an error
- AND the audio handler does not throw or crash
### Requirement: Local Music Browsable Tree
The Android Auto browse tree MUST expose a new non-playable local-music root folder alongside the existing station folders. Browsing that root and any nested subfolder MUST recursively mirror the picked SAF folder's structure: subfolders as non-playable `carpeta_local:<id>` items and audio files as playable `pista:<id>` items resolving to `PistaLocal` instances. When embedded metadata (title and/or album art) can be resolved for a track on the requested page, the displayed title and art MUST reflect that metadata instead of the raw filename and generic placeholder art. When metadata is unavailable, unparseable, or resolution otherwise fails, the item MUST fall back to the raw filename as title and the existing generic placeholder art, exactly as in Phase 1.
(Previously: always used the raw filename as the displayed title, with no metadata resolution.)
#### Scenario: Car browses the local-music root
- GIVEN a local music root folder was picked and its permission is valid
- WHEN `getChildren` is called with the local-music root folder id
- THEN it returns the root's immediate subfolders as `carpeta_local:<id>` items and audio files as `pista:<id>` playable items
- AND each playable item's title reflects resolved metadata when available, else the file's raw filename
#### Scenario: Car browses a nested subfolder
- GIVEN a `carpeta_local:<id>` folder returned from a prior browse call
- WHEN `getChildren` is called with that folder id
- THEN it returns that subfolder's own contents (nested folders and/or tracks), recursively mirroring the on-device structure
- AND browsing an empty subfolder returns an empty list, not an error
#### Scenario: Track has embedded metadata
- GIVEN an audio file on the requested page has parseable embedded title and/or album art
- WHEN it is mapped to a playable `pista:<id>` `MediaItem`
- THEN its `title` and `artUri` reflect the resolved metadata, not the raw filename or generic placeholder
#### Scenario: Metadata unavailable or resolution fails (graceful fallback)
- GIVEN an audio file's embedded metadata is absent, corrupt, in an unsupported format, or blocked by a permission edge case
- WHEN it is mapped to a playable `pista:<id>` `MediaItem`
- THEN its title falls back to the raw filename and its art falls back to the generic placeholder, exactly as in Phase 1
- AND no exception propagates from metadata resolution, and the rest of that page's items are still returned
### Requirement: Local Music Sort Mode Navigation
The local-music browse tree MUST expose sort mode as navigable, non-playable browse-tree entries (at least "by name" and "by audio quality") for a folder's tracks, since the underlying legacy browse surface cannot re-sort an already-rendered folder in place. Selecting a sort-mode entry MUST return that folder's tracks ordered accordingly: name sort orders by filename; quality sort orders by bitrate descending, reusing the existing `ordenarEmisoras(..., OrdenEmisoras.calidad)` comparator shape.
#### Scenario: Sort-mode entries are available for a folder
- GIVEN the user is browsing a local-music folder containing tracks
- WHEN that folder's sort-mode navigation is requested
- THEN at least a "by name" and a "by audio quality" navigable entry are returned
#### Scenario: User selects sort by quality
- GIVEN the user selects the "by audio quality" entry for a folder
- WHEN its children are resolved
- THEN the folder's tracks are returned ordered by bitrate descending
- AND tracks with unknown bitrate are ordered consistently, without throwing
#### Scenario: User selects sort by name
- GIVEN the user selects the "by name" entry for a folder
- WHEN its children are resolved
- THEN the folder's tracks are returned ordered by filename
### Requirement: Local Music Alphabetical Name Buckets
The local-music browse tree MUST expose alphabetical name-bucket entries (e.g. A-F, G-M, ...) as navigable, non-playable folders for a folder's tracks, as the Phase 2 realization of name-based filtering — the underlying legacy `MediaBrowserService` surface does not support a live text-search box.
#### Scenario: User browses name buckets for a folder
- GIVEN a local-music folder contains tracks spanning multiple starting letters
- WHEN the user browses that folder's name-bucket navigation level
- THEN each returned bucket is a non-playable folder covering a contiguous letter range
- AND selecting a bucket returns only the tracks whose name falls within that range
#### Scenario: Bucket with no matching tracks
- GIVEN a name-bucket range that currently matches zero tracks in a folder
- WHEN that bucket is browsed
- THEN it returns an empty list, not an error
### Requirement: Local Track Embedded Album Art Display
A playable `pista:<id>` `MediaItem` MUST display embedded album art via `artUri` when the track's embedded picture can be resolved and served as a static URI. When embedded art is absent or cannot be resolved (per the browse tree's metadata-resolution fallback), the item MUST fall back to Phase 1's existing generic rotating placeholder art (`station_art_*` rotation). The car head unit MUST NOT display an empty or broken art tile for any local track.
#### Scenario: Track has embedded album art
- GIVEN an audio file on the requested page has a resolvable embedded picture
- WHEN it is mapped to a playable `MediaItem`
- THEN `artUri` points at the resolved embedded art, served as a static URI
#### Scenario: Track has no or unresolvable embedded art
- GIVEN an audio file has no embedded picture, or it cannot be parsed or served
- WHEN it is mapped to a playable `MediaItem`
- THEN `artUri` falls back to the same rotating placeholder used in Phase 1
- AND no broken, empty, or indefinitely-loading art tile is shown
### Requirement: Local Track Playback Reuses Existing Pipeline
Selecting a `pista:<id>` item MUST resolve to the corresponding `PistaLocal` and play it through the existing `playMediaItem`/`PluriWaveAudioHandler` pipeline, unchanged, including the shared EQ signal chain.
#### Scenario: User selects a local track
- GIVEN the user taps a `pista:<id>` playable item on the car head unit
- WHEN `playFromMediaId(id)` is called
- THEN the id resolves to a `PistaLocal` and playback starts through the existing `playMediaItem` internal path
- AND standard transport (play/pause/stop) and fallback album art work as they do for stations
#### Scenario: EQ still applies to local track playback (regression guard)
- GIVEN a non-flat EQ preset is currently the active/principal preset
- WHEN a `pista:<id>` item is played
- THEN the audible output passes through the same shared EQ signal chain used for station playback, with no separate or bypassed path for local tracks
#### Scenario: Unknown or stale track id
- GIVEN `playFromMediaId` is called with a `pista:<id>` that no longer resolves to a known `PistaLocal`
- WHEN resolution fails
- THEN playback does not start and no unhandled exception propagates from the handler
### Requirement: Local Music Folder Item Cap and Paging
A local-music folder (root or nested) whose immediate children exceed `_maxItemsCarpetaLocal` (50) MUST NOT silently truncate the excess items. `getChildren` MUST return at most 50 items for the requested page, followed by a trailing non-playable, browsable "more" item whenever additional items remain beyond that page. Selecting the "more" item MUST invoke `getChildren` again and reveal the next page of up to 50 items, continuing until every item in the folder has been reached; no item may be permanently unreachable. A page that exhausts the folder's remaining items MUST NOT include a "more" item.
(Previously: folders exceeding 50 immediate children were capped to 50 with the remainder silently dropped; pagination was explicitly out of scope.)
#### Scenario: Folder exceeds the item cap
- GIVEN a picked or nested local-music folder contains more than 50 immediate children (subfolders and/or tracks combined)
- WHEN `getChildren` is called with that folder id for the first page
- THEN exactly 50 items are returned for that page, followed by a trailing "more" item
- AND no error or crash occurs, and no item is permanently unreachable
#### Scenario: Folder has exactly 50 items (boundary)
- GIVEN a local-music folder contains exactly 50 immediate children
- WHEN `getChildren` is called with that folder id
- THEN all 50 items are returned
- AND no "more" item is appended, since nothing remains to reveal
#### Scenario: Folder with 51-100 items pages in two steps
- GIVEN a local-music folder contains between 51 and 100 immediate children
- WHEN `getChildren` is called with that folder id
- THEN the first page returns 50 items plus a trailing "more" item
- AND WHEN the user selects that "more" item, `getChildren` is invoked again
- THEN it returns the remaining items with no further "more" item, since that is the last page
#### Scenario: Folder with 100+ items chains across 3+ pages
- GIVEN a local-music folder contains more than 100 immediate children
- WHEN the user repeatedly selects each returned page's "more" item
- THEN each intermediate page returns 50 items plus a trailing "more" item
- AND the final page returns only the remaining items with no "more" item
- AND every item in the folder is reachable through this chain, none dropped
#### Scenario: Only the requested page's items are resolved into MediaItems
- GIVEN a local-music folder contains more than 50 immediate children
- WHEN `getChildren` is called for a given page
- THEN only that page's items (at most 50, plus the "more" item) are constructed into full `MediaItem`s (art, resolved title)
- AND items belonging to pages not yet requested are NOT constructed into `MediaItem`s as part of that call
### Requirement: Local Music Folder Friendly Name Display
The settings UI MUST display a human-readable label for the picked local-music root folder instead of the raw SAF `content://` tree URI. If a friendly name cannot be derived from the persisted URI, the settings UI MUST fall back to a clear, non-empty label rather than showing a broken, blank, or raw-URI value.
#### Scenario: Folder picked and friendly name derivable
- GIVEN a local-music root folder has been picked and its URI persisted
- WHEN the settings screen displays the current folder
- THEN it shows a human-readable name derived from the folder, not the raw content URI
#### Scenario: Friendly name cannot be derived
- GIVEN a persisted local-music root URI whose friendly name cannot be derived (e.g. a malformed or unexpected SAF documentId shape)
- WHEN the settings screen displays the current folder
- THEN it falls back to a non-empty, non-broken label instead of an empty or corrupted value
### Requirement: Playable Item Metadata
Every playable `MediaItem` (station) MUST include a non-empty `title` and a loadable `artUri`. Stations without a logo MUST fall back to on-brand artwork, and stations whose logo URL cannot actually be loaded MUST degrade to the same on-brand fallback instead of rendering broken or blank art. The fallback MUST be visually consistent with the phone UI's per-station rotation rather than a generic launcher-icon copy.
(Previously: fallback only covered a null/empty favicon and pointed at a bundled default-artwork asset that read as a copy of the launcher icon.)
#### Scenario: Station has a valid, reachable remote logo
- GIVEN a station has a remote logo URL that resolves to a loadable image
- WHEN it is mapped to a `MediaItem`
- THEN `title` is the station name and `artUri` is the station's logo URL
#### Scenario: Station has no logo (Case A)
- GIVEN a station has no logo (`favicon` is null or empty)
- WHEN it is mapped to a `MediaItem`
- THEN `artUri` is set to one of the on-brand `station_art_*` fallback assets instead of being empty or null
#### Scenario: Station's logo URL is present but unreachable (Case B)
- GIVEN a station has a non-empty `favicon` URL that cannot be loaded (dead link, unreachable host, or non-image response)
- WHEN it is mapped to (or resolved as) a `MediaItem` in the Android Auto browse tree
- THEN the system SHALL show the same on-brand fallback art used for Case A instead of broken or blank art
- AND the car head unit MUST NOT display an empty, broken-image, or indefinitely-loading art tile for that station
#### Scenario: Fallback art matches phone-UI per-station selection
- GIVEN a station falls back to on-brand art (Case A or Case B)
- WHEN the fallback asset is chosen for that station
- THEN the selected asset MUST be one of the same 4 rotating assets used by the phone UI (`station_art_aurora`, `station_art_cosmic`, `station_art_pulse`, `station_art_nova`)
- AND the same station MUST deterministically resolve to the same asset on both the phone UI and Android Auto (per-station selection parity), not a random or session-varying choice
#### Scenario: Fallback art is on-brand, not the launcher icon
- GIVEN a station requires fallback art (Case A or Case B)
- WHEN its `artUri` is resolved
- THEN it MUST NOT point at a byte-for-byte copy of the app launcher icon
- AND it MUST point at one of the 4 on-brand `station_art_*` assets
### Requirement: Media Item Resolution by ID
`getMediaItem` MUST resolve a single station id to its full `MediaItem`, returning `null` for unknown ids instead of throwing.
#### Scenario: Known station id
- GIVEN a valid station id that exists in current state
- WHEN `getMediaItem(id)` is called
- THEN it returns the corresponding `MediaItem` with `title` and `artUri` populated
#### Scenario: Unknown station id
- GIVEN an id that does not match any known station or folder
- WHEN `getMediaItem(id)` is called
- THEN it returns `null` without throwing
### Requirement: Play by Media ID Reuses Existing Playback Path
`playFromMediaId` MUST resolve the id to an `Emisora` and invoke the existing internal `playMediaItem` path; it MUST NOT duplicate playback or reconnection logic.
#### Scenario: User selects a station in the car
- GIVEN the user taps a playable station item on the car head unit
- WHEN `playFromMediaId(id)` is called
- THEN the id is resolved to an `Emisora` via the existing resolution logic (`_emisoraDesdeMediaItem`)
- AND playback starts through the existing `playMediaItem` internal path
- AND no separate or duplicated playback logic is executed
#### Scenario: Unknown or stale media id
- GIVEN `playFromMediaId` is called with an id that no longer resolves to a known station
- WHEN resolution fails
- THEN playback does not start and no unhandled exception propagates from the handler
### Requirement: Playback State Synchronization
Play/pause/stop state changes MUST remain synchronized between the car head unit and the phone UI, regardless of which side initiated the change.
#### Scenario: User pauses from the car
- GIVEN a station is playing and projected to the car
- WHEN the user pauses from the car head unit
- THEN the phone UI reflects the paused state via the shared `MediaSession`/`PlaybackState`
#### Scenario: User pauses from the phone
- GIVEN a station is playing and projected to the car
- WHEN the user pauses from the phone UI
- THEN the car head unit reflects the paused state via the same shared session
### Requirement: Folder-Scoped Sequential Play Action
The local-music browse tree MUST prepend a "Reproducir carpeta" playable item on page 0 of any local folder that has at least one direct audio-file child (`NodoLocal` non-directory). Selecting it MUST build a queue of that folder's direct audio children in filename order and start sequential playback.
#### Scenario: Folder has tracks
- GIVEN a local folder has one or more direct audio-file children
- WHEN page 0 is browsed
- THEN "Reproducir carpeta" is prepended, and selecting it plays those tracks in name order
#### Scenario: Folder has no tracks
- GIVEN a local folder has zero direct audio-file children
- WHEN page 0 is browsed
- THEN no "Reproducir carpeta" item is returned
### Requirement: Folder-Scoped Shuffled Play Action
The local-music browse tree MUST prepend a "Reproducir aleatorio" playable item alongside "Reproducir carpeta", under the same folder-has-tracks condition. Selecting it MUST build a queue of the same direct-audio-children set in shuffled order and start playback.
#### Scenario: Folder has tracks
- GIVEN a local folder has one or more direct audio-file children
- WHEN page 0 is browsed
- THEN "Reproducir aleatorio" is prepended, and selecting it plays the same set shuffled
#### Scenario: Deterministic under seed
- GIVEN a fixed shuffle seed is injected for testing
- WHEN the queue is built for a given track set
- THEN the resulting order is reproducible across runs with that seed
### Requirement: Local Queue Auto-Advance and Transport Skip
While a local-music queue (from either action) is active, `PluriWaveAudioHandler` MUST auto-advance to the next queued track when the current track finishes, and MUST move within the queue when `skipToNext`/`skipToPrevious` is invoked.
#### Scenario: Track completes mid-queue
- GIVEN the current track is not the last one in the queue
- WHEN it finishes
- THEN the handler advances to and plays the next track automatically
#### Scenario: Skip within queue
- GIVEN a local-music queue is active
- WHEN `skipToNext`/`skipToPrevious` is invoked, including at the first or last track
- THEN the queue index moves accordingly and the corresponding track plays without throwing
### Requirement: Local Queue Mode Isolation From Radio Playback (Regression Guard — CRITICAL)
The local-music queue mechanism (auto-advance, skip-within-queue, queue state) MUST be scoped by an explicit mode boundary and MUST NEVER activate for, interfere with, or alter radio playback, reconnection, or any other non-local-music playback path. Switching between an active local queue and radio playback, in either direction, MUST cleanly stop/replace the previously active mode. This is the highest-priority regression requirement of this change.
#### Scenario: Radio starts while a local queue is active
- GIVEN a local-music queue is actively auto-advancing
- WHEN the user selects a station (`emisora:<uuid>`) or a radio-group entry
- THEN the local queue mode stops cleanly (no further auto-advance or skip behavior leaks)
- AND radio playback starts and behaves exactly as before this feature, including reconnection
#### Scenario: Local queue starts while radio is active
- GIVEN a station is currently playing
- WHEN the user selects "Reproducir carpeta" or "Reproducir aleatorio"
- THEN radio playback and its reconnect state exit cleanly
- AND the local-music queue starts playing with auto-advance/skip active
#### Scenario: Radio reconnect logic is provably untouched
- GIVEN no local-music queue has been started in the current session
- WHEN a live stream stalls and reconnects, or errors
- THEN reconnect behavior is byte-identical to pre-feature behavior, since `ProcessingState.completed` (the queue-advance trigger) never fires for radio streams
#### Scenario: Single-track local playback (`pista:<id>`) is unaffected
- GIVEN the user taps a single track directly, not via a folder-play action
- WHEN that track finishes
- THEN no queue auto-advance occurs, matching existing Phase 1/2 single-track behavior
### Requirement: Local Queue End-of-Queue Behavior
When the last track in an active local-music queue finishes, playback MUST stop cleanly rather than loop back to the first track or leave the handler in an inconsistent state, consistent with repeat modes being out of scope for this phase.
#### Scenario: Last track finishes
- GIVEN the current track is the last one in the queue
- WHEN it finishes
- THEN playback stops with no wraparound to the first track
- AND handler state remains consistent for a subsequent, independent play action
### Requirement: New Action Media-IDs Are Collision-Free
The media-ids for "Reproducir carpeta" and "Reproducir aleatorio" MUST use prefixes distinct from every existing media-id family: `emisora:`, `grupo:`, `eq_preset:`, `carpeta_local:`, `carpeta_local_pag:`, `carpeta_local_ord:`, `carpeta_local_bucket:`, `pista:`.
#### Scenario: Prefix uniqueness
- GIVEN the full set of existing prefixes plus the two new action prefixes
- WHEN each prefix is compared against every other
- THEN none duplicates or substring-collides with another
#### Scenario: Raw documentIds with `:`/`/` survive round-trip
- GIVEN a folder's SAF documentId contains `:` or `/` characters
- WHEN an action media-id is encoded and later decoded via `playFromMediaId`
- THEN the original documentId is recovered verbatim, reusing the existing strip-by-length + split-on-first-colon codec