Files
pluriwave/openspec/specs/android-auto-media/spec.md
T
FreeTLab 066fedb7bc docs(openspec): archive android-auto-favorite-groups
Merges its delta requirements into the android-auto-media base spec.
2026-07-19 13:47:39 +02:00

193 lines
11 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) containing playable station 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, unchanged by this capability.
(Previously: `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 three folder `MediaItem`s (Favoritos, Todas las emisoras, Mis emisoras), 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: 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