Files
pluriwave/openspec/changes/android-auto-local-music/specs/android-auto-media/spec.md
T
FreeTLab 6ae7e378c4 feat(auto): browse and play local music folders in Android Auto [size:exception]
Phase 1: pick a device folder via SAF (persisted grant, no new
permission), browse its nested subfolders/tracks as a 5th Android
Auto root folder (hidden until configured), and play tracks through
the existing pipeline (EQ, art rotation, cold-start-safe source).
No metadata/sort/filter/shuffle yet -- filename is the title, generic
rotating art is the placeholder; deferred to a follow-up phase.

Adds a new pluriwave/file_actions native method (listAudioChildren)
and an onActivityResult override in MainActivity for the SAF folder
picker -- both static-review-only, no Android build available here.
2026-07-19 20:30:50 +02:00

7.4 KiB

Delta for Android Auto Media

Scope: Phase 1 (foundational plumbing) of android-auto-local-music only. See "Not in this delta" for explicit exclusions.

ADDED Requirements

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, using the raw filename as the displayed title.

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 is 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

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

A local-music folder (root or nested) whose immediate children exceed _maxItemsPorCarpeta (50) MUST be capped to that limit rather than returning unbounded results; pagination is out of scope for this delta.

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
  • THEN at most 50 items are returned, consistent with the existing _maxItemsPorCarpeta cap applied to station folders
  • AND no error or crash occurs as a result of the truncation

MODIFIED Requirements

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 new 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; Todas las emisoras and Mis emisoras remain flat. The Ecualizador folder is flat, non-playable, and contains only the 6 fixed EQ preset items. The local-music root folder is non-playable and may itself be nested (see "Local Music Browsable Tree"). (Previously: root contained exactly four folders — Favoritos, Todas las emisoras, Mis emisoras, Ecualizador — with no local-music root.)

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 MediaItems (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

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, and MUST NOT render literal placeholder text such as "null kbps"

Scenario: Ungrouped station appears exactly as before (regression guard)

  • GIVEN a station's Emisora.grupoFavoritosId equals GrupoFavoritos.sinAsignarId
  • WHEN the Favoritos, Todas las emisoras, or Mis emisoras folders are browsed
  • THEN that station appears as a playable emisora:<uuid> item exactly as before, unaffected by the local-music root's existence or content

Not in this delta

Phase 1 does NOT specify: local-track metadata display beyond the raw filename (no title/artist tagging), album art beyond the existing generic on-brand fallback, sort/filter of local tracks or folders, or shuffle. These are deferred to Phase 2/3 per the proposal and MUST NOT be implemented against this spec.