Covers architecture for replacing MediaSessionCompat with Media3 MediaLibraryService, RadioPlayerAdapter facade, browse tree for Android Auto, and notification improvements. Made-with: Cursor
6.1 KiB
6.1 KiB
Media3 Migration + Android Auto Support
Goals
- Proper system media notification with metadata, album art, and transport controls (Play/Stop + Seek to Live)
- 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_PAUSECOMMAND_STOPCOMMAND_GET_CURRENT_MEDIA_ITEMCOMMAND_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
MediaMetadataon 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
LifecycleService→MediaLibraryServiceMediaSessionCompat→MediaLibrarySession(built withRadioPlayerAdapter)onGetSession()returns theMediaLibrarySessionNotificationHelperdeleted — Media3 auto-generates notifications from session statestartForeground()calls removed —MediaLibraryServicehandles foreground lifecycle- On ICY metadata change, adapter's
MediaMetadataupdates → auto-propagates everywhere - Album art:
AlbumArtResolverresolves artwork → set onMediaMetadata.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 playlistonSetMediaItem()/onAddMediaItems()→ parse mediaId, look up station, callRadioController.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 |