Skip to content

Add native Android media controls for HA media_player entities#6626

Draft
FletcherD wants to merge 74 commits intohome-assistant:mainfrom
FletcherD:feature/native-media-controls
Draft

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

Conversation

@FletcherD
Copy link
Copy Markdown

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 a Home Assistant media_player entity 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 entity to expose in the media controls, if any. One entity at a time can be selected.
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
@TimoPtr
Copy link
Copy Markdown
Member

TimoPtr commented Apr 13, 2026

image

The loading animation is missing some padding.

For some reason I'm not able to reorder the items.

When adding a second item a grey line appear above the first entity just bellow the Add media player.

I've a strange behavior, If I go from the Media controls setting screen back to the settings and I open again the Media controls it show the loading animation for a long time.

Do you have a recording of the multi-session support? For me I always see only one session.

When I swipe the notification away and open the app again, it doesn't appear again. How do we expect to get the notification card again? The only way I found at the moment is to add back the entity after removing it from the settings.

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 review in depth the screen and tests. I focused on the session and service that we get right.

Please also take a look at my previous comment about the behavior. Test it manually with multi session and some edge cases.

It takes a lot of effort to review.

<string name="matter_commissioning_cancelled">Unable to add Matter device?</string>
<string name="media_controls">Media controls</string>
<string name="media_controls_summary">Control a media player from the notification shade</string>
<string name="media_control_description">Select one or more media player entities to show as native media controls in the notification shade. You can control playback without opening the app.</string>
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.

The description and screenshot of the PR needs to be updated.

Comment thread app/src/main/kotlin/io/homeassistant/companion/android/webview/WebViewActivity.kt Outdated
Comment thread app/src/main/kotlin/io/homeassistant/companion/android/webview/WebViewActivity.kt Outdated
@TimoPtr TimoPtr marked this pull request as draft April 13, 2026 16:24
@@ -1,6 +1,8 @@
package io.homeassistant.companion.android.settings.server

import android.content.Context
@@ -1,6 +1,8 @@
package io.homeassistant.companion.android.settings.server

import android.content.Context

import android.content.Context
import androidx.preference.PreferenceDataStore
import dagger.hilt.android.qualifiers.ApplicationContext

import android.content.Context
import androidx.preference.PreferenceDataStore
import dagger.hilt.android.qualifiers.ApplicationContext
@@ -0,0 +1,408 @@
package io.homeassistant.companion.android.mediacontrol

import android.os.Looper

org.junit.Assert.assertFalse(scope.isActive)
}

…bSocket subscription when we shouldn't

Remove unused HaMediaNotificationProvider.kt
Move commandCallback out of init block
Rename reconnect() to observe()
…stead convert observe() to a suspend fun that owns the full resource lifecycle and provides the scope. Add an ArtworkCache value class to store artwork url and bytes.
    Add missing SEEK commands
MediaControlSettingsScreen.kt:
    Use HADropdownMenu
    Extract Key function
    Use takeIf to get friendlyName
MediaControlSettingsViewModel.kt:
    Fix drag-to-reorder
    Launch the init coroutine on Dispatchers.Default
- Entity.kt: Fix log message typo "get getVolumeMuted" → "get volumeMuted"
- strings.xml: Pluralise media_controls_summary ("a media player" → "media players")
- HaMediaSessionTest.kt: Add test for callMediaAction exception-catch path (line 228)
- HaMediaSessionService.kt: Restructure reconcileSessions to precompute diff on Default
  then batch all Android/Media3 API calls into a single withContext(Main) hop
Rename startIfConfigured(context, repo) to start(context) — the service
already calls stopSelf() when reconcileSessions receives an empty entity
list, so the pre-check was redundant. Remove MediaControlRepository from
MediaControlStarterViewModel, MediaControlSettingsFragment, and
WebViewActivity accordingly.
'position' was ambiguous — it looked like a playback position. Rename to
'index' throughout (entity, DAO, repository).
…change 'Unable to get getVolumeMuted' to match other log messages; Add OptIn import
…rselves, Override onUpdateNotification() with Per-Session Notification IDs
…ing finishes, fade in animation, correct text color
…ivate

Addresses PR review comments 9, 10, and 11:
- activeSessions: private (tests now assert via MediaSessionService.getSessions())
- reconcileSessions: private (no longer takes sessionScope; uses serviceScope directly)
- startObservingEntities: @VisibleForTesting internal (no longer takes scope params)
- serviceScope: moved to @VisibleForTesting primary constructor; secondary @Inject
  no-arg constructor provides the production default, following the AudioUrlPlayer pattern
- Tests replace serviceScope via reflection after Robolectric.buildService().get() so
  the test-controlled UnconfinedTestDispatcher scope is used without calling onCreate()
- Default entity state flow changed to non-completing MutableSharedFlow so observe()
  stays suspended, keeping sessions alive in getSessions() for assertions
- Fixed pre-existing bug in onTaskRemoved: anyPlaying was computed but never used;
  stopSelf() now only called when nothing is playing
…the ViewModel and replace it with testDispatcher in tests
Fix lint errors (notification permissions)
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