# 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 `` - AND the `AndroidManifest.xml` `` block declares `` - AND PluriWave appears in the car's media app list ### 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. (Previously: no subtitle requirement; folders and playable items were otherwise unchanged.) #### 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" ### 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