Files
Android-247-Radio/docs/plans/2026-03-18-media3-android-auto-design.md
cottongin cfc845479b Add design doc for Media3 migration + Android Auto support
Covers architecture for replacing MediaSessionCompat with Media3
MediaLibraryService, RadioPlayerAdapter facade, browse tree for
Android Auto, and notification improvements.

Made-with: Cursor
2026-03-18 05:50:40 -04:00

6.1 KiB

Media3 Migration + Android Auto Support

Goals

  1. Proper system media notification with metadata, album art, and transport controls (Play/Stop + Seek to Live)
  2. Android Auto compatibility — app registers as a media source with a browsable station catalog

Approach

Migrate from legacy MediaSessionCompat + manual NotificationHelper to Media3 MediaLibraryService with a thin Player adapter wrapping the existing custom audio engine. The custom audio pipeline (OkHttp → IcyParser → MediaCodec → AudioTrack) remains completely untouched.

Architecture

RadioController ──intent──▶ RadioPlaybackService (MediaLibraryService)
                                    │
                                    ├── RadioPlayerAdapter (implements Player)
                                    │        └── delegates to AudioEngine (untouched)
                                    │
                                    ├── MediaLibrarySession
                                    │        ├── auto-publishes metadata/state/art to OS
                                    │        ├── auto-manages notification (Play/Stop/SeekLive)
                                    │        └── serves browse tree for Android Auto
                                    │
                                    └── AudioEngine (unchanged custom pipeline)

RadioPlayerAdapter

A thin facade implementing androidx.media3.common.Player that maps the small radio-relevant command surface to existing logic. It does NOT produce or route audio.

Supported commands

  • COMMAND_PLAY_PAUSE
  • COMMAND_STOP
  • COMMAND_GET_CURRENT_MEDIA_ITEM
  • COMMAND_GET_MEDIA_ITEMS_METADATA
  • Custom SessionCommand("SEEK_TO_LIVE") — exposed as a skip-forward button

State mapping

PlaybackState (existing) Media3 Player.STATE_* isPlaying
Idle STATE_IDLE false
Connecting STATE_BUFFERING false
Playing STATE_READY true
Paused STATE_READY false
Reconnecting STATE_BUFFERING false

Command mapping

  • play()RadioController.play(currentStation)
  • stop()RadioController.stop()
  • pause()RadioController.stop() (radio streams don't pause)
  • Metadata changes from ICY events update MediaMetadata on the adapter, which auto-propagates to notification/lockscreen/Auto/Bluetooth

RadioPlaybackService Changes

Converts from LifecycleService to MediaLibraryService.

Stays the same

  • Intent-based ACTION_PLAY/STOP/PAUSE/SEEK_LIVE control flow
  • AudioEngine lifecycle (create, start, stop, event collection)
  • Wake lock + WiFi lock management
  • Reconnect loop with backoff
  • Connection span / listening session DB tracking
  • StreamResolver URL resolution

Changes

  • LifecycleServiceMediaLibraryService
  • MediaSessionCompatMediaLibrarySession (built with RadioPlayerAdapter)
  • onGetSession() returns the MediaLibrarySession
  • NotificationHelper deleted — Media3 auto-generates notifications from session state
  • startForeground() calls removed — MediaLibraryService handles foreground lifecycle
  • On ICY metadata change, adapter's MediaMetadata updates → auto-propagates everywhere
  • Album art: AlbumArtResolver resolves artwork → set on MediaMetadata.artworkUri

Notification layout (auto-managed)

  • Compact: Play/Stop toggle + Seek to Live (custom command button)
  • Expanded: same + album art + station name + track info
  • Channel importance: LOW

Android Auto Browse Tree

[ROOT]
  ├── Playlist: "SomaFM"
  │     ├── Groove Salad
  │     ├── Drone Zone
  │     └── ...
  ├── Playlist: "My Favorites"
  │     ├── Station A
  │     └── Station B
  └── Unsorted (if any stations have no playlist)
        ├── Station X
        └── Station Y

Callbacks

  • onGetLibraryRoot() → root MediaItem (browsable)
  • onGetChildren(rootId) → playlists + unsorted folder (browsable MediaItems)
  • onGetChildren(playlistId) → stations in playlist (playable MediaItems)
  • onGetChildren("unsorted") → stations with no playlist
  • onSetMediaItem() / onAddMediaItems() → parse mediaId, look up station, call RadioController.play()

Media IDs

  • Root: "root"
  • Playlist folder: "playlist:{id}"
  • Unsorted folder: "unsorted"
  • Station item: "station:{id}"

Data freshness

DAO Flow methods use .first() for snapshots. Updates visible on next navigation (acceptable for a radio catalog).

Dependencies

Add

media3 = "1.6.0"
media3-session = { group = "androidx.media3", name = "media3-session", version.ref = "media3" }
media3-common = { group = "androidx.media3", name = "media3-common", version.ref = "media3" }

Remove

  • media-session (androidx.media:media:1.7.1)

No media3-exoplayer needed — custom audio engine stays.

Manifest Changes

<service
    android:name=".service.RadioPlaybackService"
    android:exported="true"
    android:foregroundServiceType="mediaPlayback">
    <intent-filter>
        <action android:name="androidx.media3.session.MediaLibraryService"/>
        <action android:name="android.media.browse.MediaBrowserService"/>
    </intent-filter>
</service>

<meta-data
    android:name="com.google.android.gms.car.application"
    android:resource="@xml/automotive_app" />

New File: res/xml/automotive_app.xml

<?xml version="1.0" encoding="utf-8"?>
<automotiveApp>
    <uses name="media"/>
</automotiveApp>

Files Affected

File Action
libs.versions.toml Add media3 version + libraries, remove old media
app/build.gradle.kts Swap dependencies
AndroidManifest.xml Update service declaration, add Auto meta-data
res/xml/automotive_app.xml New file
RadioPlayerAdapter.kt New file — Player facade
RadioPlaybackService.kt Rewrite to extend MediaLibraryService
NotificationHelper.kt Delete
RadioController.kt Minor — may need adapter reference for metadata updates
RadioApplication.kt Minor — remove NotificationHelper if referenced