Skip to content

android: let other apps browse the library and see the queue, behind a setting - #9

Merged
ingoau merged 2 commits into
mainfrom
claude/android-media-browser-api-a49m2o
Sep 25, 2026
Merged

ingoau merged 2 commits into
mainfrom
claude/android-media-browser-api-a49m2o

Conversation

@ingoau

@ingoau ingoau commented Sep 25, 2026

Copy link
Copy Markdown
Owner

Summary

Other apps can now browse the library, see the queue and control playback, through Android's media browser API. Examples are Android Auto, Wear, Assistant and media browser apps. All of this is off until you turn on the new Allow control by other apps setting.

Library browsing (MediaLibraryService)

  • PlaybackService is now a Media3 MediaLibraryService. The app declares automotive_app_desc.xml, so it shows up in Android Auto.
  • LibraryBrowser answers browse requests with core queries against the first server:
    • The root has Albums, Artists, Playlists and Genres, with grid/list display hints for Auto.
    • An album or playlist lists its tracks, and an artist or genre lists its albums.
    • Results are paged.
    • Search is local only (no server request).
  • Media ids (MediaIds.kt) carry everything needed to play the item and are URL-encoded, so an id another app saved from an earlier connection still works.
  • Playing an item sends the same command the UI would send:
    • An album, artist, playlist or genre plays as that context, starting at the tapped track.
    • A lone track plays through PlayTracks.
    • Adding items uses PlayNext or PlayLater.
    • Voice "play …" requests resolve to the best match and follow the focus hint in EXTRA_MEDIA_FOCUS. An empty request resumes playback.
  • Subscribed browsers are told when the library changes (debounced 2 s). The "recent" root is refused, in line with the existing rule that playback never auto-resumes.

Queue

  • The session player's playlist is now the core's QueueView: the last 25 history entries, the current track, playing next and upcoming. Auto's queue view, Wear and the legacy setQueue show it.
  • Picking an entry sends JumpToQueueItem.
  • Moving an upcoming entry sends MoveQueueItem, and removing one sends RemoveQueueItems. The current track is never removed.
  • Next and previous still go through the core.

Artwork

  • ArtworkProvider serves cover art as content://<package>.artwork/<size>/<coverArt>, because other apps cannot read the core's cache files.
  • It is exported, but it only serves this app and the controllers the session accepted. Access is read-only, and only the core's fixed cache sizes are served.

"Allow control by other apps" (media.externalControl)

  • A new core registry setting. It is device-local, off by default, and shown in Settings > Playback.
  • While it is off, only the system's own controls can connect: this app, Media3's notification controller, and callers holding MEDIA_CONTENT_CONTROL (SystemUI, Bluetooth, checked by uid).
  • On Android 8.0–8.1 the platform does not identify legacy controller callers, so those anonymous callers are let through. Otherwise the lock screen and Bluetooth controls would break there.
  • Every browse, search, add and custom command is checked again. Turning the setting off strips already-connected apps of all their commands and revokes their artwork access.
  • The service keeps a copy of the value in SharedPreferences, excluded from backups. A controller that connects before the core's snapshot arrives is judged by the last choice.

Testing

  • New unit tests: MediaIdsTest, LibraryBrowserTest (runs against FakeCore), CoreSessionQueueTest, ExternalControlTest, plus new cases in ManifestRulesTest and SettingsCategoriesTest.
  • ./gradlew testDebugUnitTest passes: app 125, core 77, playback 62 tests.
  • cargo test -p hocket-core --lib settings passes.
  • :app:assembleDebug builds, and the merged manifest has the right provider authority.
  • Not yet tried on a device or in Android Auto.

🤖 Generated with Claude Code

https://claude.ai/code/session_01BFy2RUWjfmXxF8wCXTm7HM


Generated by Claude Code

…yService)

PlaybackService is now a Media3 MediaLibraryService. Android Auto (declared via
automotive_app_desc.xml), Wear, Assistant and any MediaBrowser can browse the first
server's library: Albums, Artists, Playlists and Genres, each album or playlist with its
tracks, and each artist or genre with its albums, plus local search. Playing an item sends
the command the UI would send. An album, artist, playlist or genre plays as that context
(starting at the tapped track). Search results play through PlayTracks. Added items become
PlayNext or PlayLater. Voice "play ..." requests resolve to the best match and honour
EXTRA_MEDIA_FOCUS.

The session player's playlist is now the core's queue: recent history, the current entry,
playing next and upcoming. Controllers show it as the session queue. Picking an entry sends
JumpToQueueItem, and moving or removing upcoming entries sends MoveQueueItem or
RemoveQueueItems.

Cover art reaches controllers through ArtworkProvider (content URIs). It serves only this
app and the controllers the session accepted, and only read-only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFy2RUWjfmXxF8wCXTm7HM
A new device-local core setting, media.externalControl, decides whether other apps
(Android Auto, Wear, media browsers, automation apps) may connect to the media
session to browse the library, see the queue and control playback. It is shown in
Settings > Playback.

While it is off, the session accepts only the system's own controls. These are this
app, Media3's notification controller, and callers holding MEDIA_CONTENT_CONTROL
(SystemUI, Bluetooth). Before API 28 the platform's anonymous legacy controller is
also accepted, because it cannot be identified. Every browse, search, add and custom
command is checked again, so turning the setting off also strips already-connected
apps of their commands and revokes their artwork access. PlaybackService keeps a copy
of the value (excluded from backups), so a controller that connects before the core's
snapshot is judged by the last choice.

Also fixes library browsing: onConnect granted DEFAULT_SESSION_COMMANDS, which leaves
out the library commands, so browsers would have been refused. It now grants
DEFAULT_SESSION_AND_LIBRARY_COMMANDS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFy2RUWjfmXxF8wCXTm7HM
@ingoau
ingoau merged commit 68f55e8 into main Sep 25, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants