Files
pluriwave/openspec/specs/android-auto-media/spec.md
T
FreeTLab 9bfa9ac408 docs(openspec): archive android-auto-eq-presets
Merges its delta requirements into the android-auto-media base spec.
2026-07-19 14:19:11 +02:00

16 KiB

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 MediaItems 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) 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"). (Previously: root contained exactly 3 folders — Favoritos, Todas las emisoras, Mis emisoras — with no EQ 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 four folder MediaItems (Favoritos, Todas las emisoras, Mis emisoras, Ecualizador), 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 MediaItems, 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: 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.)

  • 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