Skip to content

Add native Android media controls for HA media_player entities#6626

Open
FletcherD wants to merge 118 commits into
home-assistant:mainfrom
FletcherD:feature/native-media-controls
Open

Add native Android media controls for HA media_player entities#6626
FletcherD wants to merge 118 commits into
home-assistant:mainfrom
FletcherD:feature/native-media-controls

Conversation

@FletcherD
Copy link
Copy Markdown

@FletcherD FletcherD commented Mar 25, 2026

Summary

I wanted to be able to control a media player entity natively on Android without having to open the app or navigate to a widget. So this feature exposes one or more Home Assistant media_player entities as native Android Media Controls (described here) in the notification shade, the same UI used by other media players on Android.
The media controls show the currently playing track info and play position with album art. Prev/next track, play/pause and seek controls work and are forwarded to the media_player entity (if the entity supports them).
A new "Media controls" setting is added under "Companion app" to choose which media_player entities to expose in the media controls, if any. You can choose more than one entity, in which case a notification will be created for each one.
Unit tests are added to test playback state mapping, state flow, settings and everything else I could think of.

Checklist

  • New or updated tests have been added to cover the changes following the testing guidelines.
  • The code follows the project's code style and best_practices.
  • The changes have been thoroughly tested, and edge cases have been considered.
  • Changes are backward compatible whenever feasible. Any breaking changes are documented in the changelog for users and/or in the code for developers depending on the relevance.

Screenshots

notification_shade_media_controls settings_media_controls_entry_dark media_controls_settings_dark

Link to pull request in documentation repositories

User Documentation PR: home-assistant/companion.home-assistant#1304

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a native Android MediaSession-backed surface for controlling a selected Home Assistant media_player entity from the notification shade, plus companion settings and supporting data plumbing.

Changes:

  • Introduces MediaControlRepository + state model to observe a configured media_player and map HA state/attributes into media metadata and supported commands
  • Adds HaMediaSessionService and HaRemoteMediaPlayer to expose the entity via Android’s media controls and forward transport/seek actions back to HA
  • Adds a new “Media controls” settings screen and preference entry to select/clear the exposed entity, plus unit tests and changelog entry

Reviewed changes

Copilot reviewed 25 out of 25 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
common/src/test/kotlin/io/homeassistant/companion/android/common/data/mediacontrol/MediaControlRepositoryImplTest.kt Unit tests for repository configuration and HA→media state mapping
common/src/main/res/values/strings.xml New UI strings for the media controls settings
common/src/main/kotlin/io/homeassistant/companion/android/common/data/prefs/PrefsRepositoryImpl.kt Persists configured media control server/entity IDs; clears on server removal
common/src/main/kotlin/io/homeassistant/companion/android/common/data/prefs/PrefsRepository.kt Adds prefs API for media controls configuration
common/src/main/kotlin/io/homeassistant/companion/android/common/data/mediacontrol/MediaControlState.kt New state model + playback state types for media controls
common/src/main/kotlin/io/homeassistant/companion/android/common/data/mediacontrol/MediaControlRepositoryImpl.kt Observes websocket entity updates and emits MediaControlState
common/src/main/kotlin/io/homeassistant/companion/android/common/data/mediacontrol/MediaControlRepository.kt Repository interface for configuration + observation
common/src/main/kotlin/io/homeassistant/companion/android/common/data/mediacontrol/MediaControlModule.kt Hilt binding for the new repository
common/src/main/kotlin/io/homeassistant/companion/android/common/data/integration/Entity.kt Adds media_player supported-features constants and helper accessors
automotive/src/main/AndroidManifest.xml Declares MediaSessionService and media playback FGS permission
automotive/lint-baseline.xml Updates lint baseline (new ComposeUnstableCollections entries)
app/src/test/kotlin/io/homeassistant/companion/android/settings/mediacontrol/MediaControlSettingsViewModelTest.kt Unit tests for settings ViewModel selection/save/clear behaviors
app/src/test/kotlin/io/homeassistant/companion/android/mediacontrol/HaRemoteMediaPlayerTest.kt Robolectric tests for player state mapping, commands, and callbacks
app/src/main/res/xml/preferences.xml Adds preference category/entry for “Media controls”
app/src/main/res/xml/changelog_master.xml Adds user-facing changelog entry for the feature
app/src/main/res/drawable/ic_play_circle_outline.xml New icon for the settings entry
app/src/main/kotlin/io/homeassistant/companion/android/webview/WebViewActivity.kt Starts the media session service on app start when configured
app/src/main/kotlin/io/homeassistant/companion/android/settings/mediacontrol/views/MediaControlSettingsView.kt Compose UI for selecting server/entity and saving/clearing
app/src/main/kotlin/io/homeassistant/companion/android/settings/mediacontrol/MediaControlSettingsViewModel.kt Loads servers/entities/registries, manages selection, and starts/stops service
app/src/main/kotlin/io/homeassistant/companion/android/settings/mediacontrol/MediaControlSettingsFragment.kt Fragment host for the Compose settings screen (+ help link)
app/src/main/kotlin/io/homeassistant/companion/android/settings/SettingsFragment.kt Wires the new preference click to open the media controls settings
app/src/main/kotlin/io/homeassistant/companion/android/mediacontrol/HaRemoteMediaPlayer.kt Media3 SimpleBasePlayer proxy translating commands to HA callbacks
app/src/main/kotlin/io/homeassistant/companion/android/mediacontrol/HaMediaSessionService.kt MediaSessionService that observes HA state, loads artwork, and calls HA actions
app/src/main/AndroidManifest.xml Declares MediaSessionService and media playback FGS permission
app/lint-baseline.xml Updates lint baseline (new ComposeUnstableCollections entries)

Comment thread app/lint-baseline.xml Outdated
Comment thread automotive/lint-baseline.xml Outdated
@FletcherD FletcherD force-pushed the feature/native-media-controls branch from c9a2d04 to 785d6a5 Compare March 25, 2026 07:04
Copy link
Copy Markdown
Member

@TimoPtr TimoPtr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't check the code yet, I'm curious could it also work on the watch? (In another PR)

@TimoPtr
Copy link
Copy Markdown
Member

TimoPtr commented Mar 25, 2026

Screen_recording_20260325_145042.mp4

When you open the setting screen, it has a weird animation that shouldn't be there.
On a wider screen it looks off (margin on the right side).
The screen feels empty I wonder if we can do something about it, like showing how it looks like?

@TimoPtr
Copy link
Copy Markdown
Member

TimoPtr commented Mar 25, 2026

Why limiting to only one entity ? The service does support multiple sessions it would be nice to be able to track multiple entities as per the documentaion

However, if you're capable of handling multiple playbacks and want to keep their sessions while the app is in the background, create multiple sessions and add them to this service with addSession(MediaSession).

An extension to your PR (in another one) would be that we should be able to send a media player from HA to the phone through a command so that from an automation you can dynamically add/remove a session. (if you think it is feasible let's create an issue)

image "This phone" is wrong it should be probably the entity name, when clicking on it I can change the volume of my phone which is wrong. --> You probably need to set `.setDeviceInfo(deviceInfo)` on the State object and also use the device type `PLAYBACK_TYPE_REMOTE`. --> For the volume something similar like `setDeviceVolume` Or try to disable it in the scope of this PR.

@TimoPtr TimoPtr marked this pull request as draft March 25, 2026 14:54
@TimoPtr
Copy link
Copy Markdown
Member

TimoPtr commented Mar 25, 2026

@FletcherD it does look promising, I didn't go in details for now but I gave you some comments that are important to look at before going any further.

@FletcherD
Copy link
Copy Markdown
Author

FletcherD commented Mar 26, 2026

Thanks for the comments, they're good ideas.
I refactored to support multiple entities, now you can add and remove them in the setting.

"This phone" is wrong it should be probably the entity name, when clicking on it I can change the volume of my phone which is wrong. --> You probably need to set .setDeviceInfo(deviceInfo) on the State object and also use the device type PLAYBACK_TYPE_REMOTE. --> For the volume something similar like setDeviceVolume Or try to disable it in the scope of this PR.

I exposed volume set/adjust which also allows the volume to be adjusted with the hardware buttons.
Also the device info so the badge shows "Other device". As far as I can tell there's no way to set the name here, it is determined by the OS

@TimoPtr TimoPtr mentioned this pull request Mar 26, 2026
4 tasks
Comment thread automotive/src/main/AndroidManifest.xml
Comment thread automotive/src/main/AndroidManifest.xml Outdated
</intent-filter>
</service>


Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually it would be nice to test if it works on automotive

Copy link
Copy Markdown
Author

@FletcherD FletcherD May 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apparently there is no way to access settings on automotive at all, due to Google's restrictions, lol. So there is definitely no way to support automotive without a lot of work.
https://community.home-assistant.io/t/ha-with-android-auto-dashboard/593994/16

Copy link
Copy Markdown
Member

@TimoPtr TimoPtr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went over the PR thanks for your huge work. After this set of comments I think it is ready to be reviewed by @jpelgrom that might have other comments since it's huge I probably missed some stuff.

@TimoPtr TimoPtr requested a review from jpelgrom May 13, 2026 15:45
FletcherD added 9 commits May 15, 2026 04:41
- Thread 33: Replace implementation-detail KDoc on MediaControlSettingsScreen
  with caller-facing description
- Thread 35: Change subtitle parameter in ConfiguredEntityRow from String? to
  String; remove null-check
- Thread 48: Add serverId to loadRegistry error log message
- Thread 51: Replace all getSessions() calls with sessions Kotlin accessor
  in HaMediaSessionServiceTest
- Thread 29: Use ServiceController.of(HaMediaSessionService(observationScope), null)
  in tests instead of Robolectric.buildService; remove reflection-based scope injection
- Thread 30: Make startObservingEntities() @VisibleForTesting internal; replace
  reflection-based invocation in test with direct call
- Thread 32: Add notificationEntityName field (set from entityFriendlyName on Main);
  use as fallback in buildNotification title instead of raw id string
- Thread 36: Remove Surface wrapper from ConfiguredEntityRow; apply background
  color directly to Row via Modifier.background
- Thread 52: Consolidate entity config key — move session id format into
  MediaControlEntityConfig as a property
…tion

On API 36+, Notification.Builder.build() calls Icon.scaleDownIfNecessary()
on the Main thread, triggering a StrictMode CustomViolation that crashes
debug builds via FailFast.

Remove the hardcoded 256px Coil size constraint so artwork loads at native
server resolution (better quality on lock screen / expanded controls). After
compressing the full-resolution PNG bytes for MediaMetadata, explicitly call
Bitmap.createScaledBitmap() to android.R.dimen.notification_large_icon_width
on IO before passing the bitmap to setLargeIcon() — ensuring
scaleDownIfNecessary has nothing to do on Main.
- Split MediaControlSettingsContent into private composables (DescriptionSection, ServerDropdownSection, EntityPickerSection)
- Always render icon slot (Image or Spacer) in ConfiguredEntityRow so rows align when mixing entities with and without icons
- Use Arrangement.spacedBy(SPACE3) for horizontal rhythm in ConfiguredEntityRow; remove per-child start padding
…ingsViewModel

- Merge configuredEntities + entityNamesByConfig + entityIconsByConfig into
  List<ConfiguredEntityItem> updated atomically (thread 42, 44)
- Move IIcon resolution into Compose layer via LocalContext (thread 44)
- Make availableEntities a computed val on UiState instead of a stored
  field (thread 43), removing updateAvailableEntities() entirely (thread 46)
- Invert data flow: addEntity/removeEntity write to the repository only;
  observeConfiguredEntities() drives configuredEntityItems reactively,
  eliminating the read-after-write race in persistAndNotifyService()
  (thread 47) and the copy() duplication in add/remove (thread 45)
- Add multi-server screenshot test variant (thread 38)
Ordering of media control notifications is not important, so there is
no need to persist list order in the DB.
…mandFuture in updateState()

Previously handleCommand() completed the SettableFuture via invokeOnCompletion when the HTTP
call returned. SimpleBasePlayer then called getState() with the stale pre-command mediaState
(e.g. Playing@178s after a pause), briefly flashing the old position before the WebSocket
state confirmation arrived ~25ms later.

The future is now stored in pendingCommandFuture and completed by updateState() after the
server-confirmed state has been written to mediaState, so getState() always sees fresh data.
invokeOnCompletion is kept only as an error fallback for unrecoverable job failures.
Adds multi-server variant references and refreshes existing ones.
Resolve changelog conflict: keep 2026.5.3 version and include both the
native media controls entry and the upstream bug fixes entry.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants