Wire mobile app for Media3 service toggle#5150
Wire mobile app for Media3 service toggle#5150geekygecko merged 3 commits intomedia3/06b-session-wiringfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Wires the mobile app to support switching between Media3 PlaybackService and LegacyPlaybackService via a feature flag, and updates video PiP controls to use app-local broadcast actions instead of MediaButtonReceiver.
Changes:
- Call
PlaybackServiceToggle.ensureCorrectServiceEnabled()duringPocketCastsApplication.onCreate(). - Update
VideoActivityPiP remote actions to fire app-scoped broadcast intents handled by a dynamically registered receiver. - Update manifest service declarations (both services disabled by default) and remove the
androidx.media.session.MediaButtonReceivermanifest entry; delete an obsolete instrumentation test.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| modules/features/player/src/main/java/au/com/shiftyjelly/pocketcasts/player/view/video/VideoActivity.kt | Reworks PiP remote actions to use custom broadcast actions handled by an in-activity receiver. |
| app/src/main/java/au/com/shiftyjelly/pocketcasts/PocketCastsApplication.kt | Invokes playback service component toggle during app startup. |
| app/src/main/AndroidManifest.xml | Declares both playback services disabled by default and removes the legacy MediaButtonReceiver manifest receiver. |
| app/src/androidTest/java/au/com/shiftyjelly/pocketcasts/ui/PlaybackServiceTest.kt | Removes an instrumentation test tied to old service internals. |
Comments suppressed due to low confidence (1)
app/src/main/AndroidManifest.xml:626
- The manifest removes the androidx.media.session.MediaButtonReceiver declaration, but the app still builds playback notification actions using MediaButtonReceiver.buildMediaButtonPendingIntent() (e.g., NotificationDrawerImpl). Without a manifest-declared receiver, those PendingIntents may not resolve and notification transport controls can stop working (especially for LegacyPlaybackService). Either keep the receiver declaration, or update notification action PendingIntents to target an existing receiver/service (e.g., PlayerBroadcastReceiver / explicit service intents).
<!-- Both services start disabled; PlaybackServiceToggle.ensureCorrectServiceEnabled()
enables the correct one in Application.onCreate() based on the MEDIA3_SESSION feature flag.
This avoids the system resolving the wrong service before the toggle runs. -->
<service
android:name="au.com.shiftyjelly.pocketcasts.repositories.playback.PlaybackService"
android:exported="true"
android:enabled="false"
android:label="@string/app_name"
android:foregroundServiceType="mediaPlayback">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService"/>
<action android:name="androidx.media3.session.MediaLibraryService"/>
</intent-filter>
</service>
<service
android:name="au.com.shiftyjelly.pocketcasts.repositories.playback.LegacyPlaybackService"
android:exported="true"
android:enabled="false"
android:label="@string/app_name"
android:foregroundServiceType="mediaPlayback">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService"/>
</intent-filter>
</service>
You can also share your feedback on Copilot code review. Take the survey.
| PlaybackServiceToggle.ensureCorrectServiceEnabled(this) | ||
|
|
There was a problem hiding this comment.
Fixed — moved PlaybackServiceToggle.ensureCorrectServiceEnabled() after setupCrashLogging() and setupAnalytics(), so Firebase Remote Config and feature flag providers are initialized before the toggle reads the flag value.
Project manifest changes for appThe following changes in the --- ./build/reports/diff_manifest/app/release/base_manifest.txt 2026-04-02 11:38:10.370287756 +0000
+++ ./build/reports/diff_manifest/app/release/head_manifest.txt 2026-04-02 11:38:13.110275459 +0000
@@ -717,9 +717,25 @@
android:name="androidx.mediarouter.media.MediaTransferReceiver"
android:exported="true" >
</receiver>
-
+ <!--
+ Both services start disabled; PlaybackServiceToggle.ensureCorrectServiceEnabled()
+ enables the correct one in Application.onCreate() based on the MEDIA3_SESSION feature flag.
+ This avoids the system resolving the wrong service before the toggle runs.
+ -->
<service
android:name="au.com.shiftyjelly.pocketcasts.repositories.playback.PlaybackService"
+ android:enabled="false"
+ android:exported="true"
+ android:foregroundServiceType="mediaPlayback"
+ android:label="@string/app_name" >
+ <intent-filter>
+ <action android:name="android.media.browse.MediaBrowserService" />
+ <action android:name="androidx.media3.session.MediaLibraryService" />
+ </intent-filter>
+ </service>
+ <service
+ android:name="au.com.shiftyjelly.pocketcasts.repositories.playback.LegacyPlaybackService"
+ android:enabled="false"
android:exported="true"
android:foregroundServiceType="mediaPlayback"
android:label="@string/app_name" >
@@ -738,20 +754,7 @@
android:pathPrefix="/pocket_casts_wear_communication"
android:scheme="wear" />
</intent-filter>
- </service>
- <!--
- A receiver that will receive media buttons and send as
- intents to your MediaBrowserServiceCompat implementation.
- Required on pre-Lollipop. More information at
- http://developer.android.com/reference/android/support/v4/media/session/MediaButtonReceiver.html
- -->
- <receiver
- android:name="androidx.media.session.MediaButtonReceiver"
- android:exported="true" >
- <intent-filter>
- <action android:name="android.intent.action.MEDIA_BUTTON" />
- </intent-filter>
- </receiver> <!-- Google play services -->
+ </service> <!-- Google play services -->
<meta-data
android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version" /> <!-- Chromecast -->Go to https://buildkite.com/automattic/pocket-casts-android/builds/16230/canvas?sid=019d4df6-70cb-4277-b3bf-e18d4ed0d33f, click on the |
| </intent-filter> | ||
| </service> | ||
|
|
||
| <service |
Check warning
Code scanning / Android Lint
Exported service does not require permission Warning
There was a problem hiding this comment.
MediaLibraryService must be exported to allow external media controllers (Android Auto, Google Assistant, Wear OS) to connect. This is standard for media apps — the Media3 framework handles authentication via onConnect in the session callback.
aba1e9a to
e1fc3e9
Compare
42a46be to
f96355f
Compare
|
Version |
e1fc3e9 to
e250248
Compare
f96355f to
7f72738
Compare
There was a problem hiding this comment.
Pull request overview
Wires the mobile app to switch between the new Media3 playback service and the legacy playback service via a feature-flag-driven toggle, and updates video PiP controls to use app-scoped broadcast actions instead of MediaButtonReceiver.
Changes:
- Call
PlaybackServiceToggle.ensureCorrectServiceEnabled()duringPocketCastsApplication.onCreate(). - Update
VideoActivityPiPRemoteActions to usePendingIntent.getBroadcast()with custom actions handled by a dynamically registered receiver. - Update
AndroidManifest.xmlto declare both playback services disabled by default and remove the legacyMediaButtonReceiver; delete an obsolete instrumentation test.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| modules/features/player/src/main/java/au/com/shiftyjelly/pocketcasts/player/view/video/VideoActivity.kt | Replaces PiP media button pending intents with custom broadcast actions handled inside the Activity. |
| app/src/main/java/au/com/shiftyjelly/pocketcasts/PocketCastsApplication.kt | Invokes the playback service toggle during app startup. |
| app/src/main/AndroidManifest.xml | Declares both playback services disabled by default; removes the manifest MediaButtonReceiver entry. |
| app/src/androidTest/java/au/com/shiftyjelly/pocketcasts/ui/PlaybackServiceTest.kt | Removes an instrumentation test tied to legacy service internals. |
Comments suppressed due to low confidence (1)
app/src/main/AndroidManifest.xml:626
- Both PlaybackService and LegacyPlaybackService are declared with android:enabled="false". When both are disabled, the package will expose no component handling android.media.browse.MediaBrowserService / androidx.media3.session.MediaLibraryService, so external clients (e.g., Android Auto discovery, MediaBrowser clients, voice/Bluetooth integrations) cannot discover or bind to any playback service until the user manually launches the app and the toggle runs. Consider leaving a safe default service enabled in the manifest (matching the feature flag default) and only flipping when the flag state is known, or provide an always-enabled proxy entrypoint so discovery works before first app launch.
<!-- Both services start disabled; PlaybackServiceToggle.ensureCorrectServiceEnabled()
enables the correct one in Application.onCreate() based on the MEDIA3_SESSION feature flag.
This avoids the system resolving the wrong service before the toggle runs. -->
<service
android:name="au.com.shiftyjelly.pocketcasts.repositories.playback.PlaybackService"
android:exported="true"
android:enabled="false"
android:label="@string/app_name"
android:foregroundServiceType="mediaPlayback">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService"/>
<action android:name="androidx.media3.session.MediaLibraryService"/>
</intent-filter>
</service>
<service
android:name="au.com.shiftyjelly.pocketcasts.repositories.playback.LegacyPlaybackService"
android:exported="true"
android:enabled="false"
android:label="@string/app_name"
android:foregroundServiceType="mediaPlayback">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService"/>
</intent-filter>
</service>
| RxJavaUncaughtExceptionHandling.setUp() | ||
| setupCrashLogging() | ||
| setupLogging() | ||
| setupAnalytics() | ||
|
|
||
| PlaybackServiceToggle.ensureCorrectServiceEnabled(this) | ||
|
|
||
| setupApp() |
There was a problem hiding this comment.
PlaybackServiceToggle.ensureCorrectServiceEnabled() is called before FeatureFlag.initialize() runs (FeatureFlag.initialize is invoked from AppLifecycleObserver.setupFeatureFlags(), which is called later in setupApp()). As a result, this toggle will likely read the default value for MEDIA3_SESSION and may enable the wrong service for the entire process. Move the toggle call to a point after FeatureFlag.initialize(), or refactor PlaybackServiceToggle to accept a resolved boolean that’s computed after providers are initialized.
There was a problem hiding this comment.
Good catch — fixed. Moved PlaybackServiceToggle.ensureCorrectServiceEnabled() to after appLifecycleObserver.setup() which calls FeatureFlag.initialize().
| private val pipReceiver = object : BroadcastReceiver() { | ||
| override fun onReceive(context: Context, intent: Intent) { | ||
| when (intent.action) { | ||
| ACTION_PIP_SKIP_BACK -> playbackManager.skipBackward(sourceView = SourceView.MEDIA_BUTTON_BROADCAST_ACTION) | ||
| ACTION_PIP_PLAY -> playbackManager.playQueue(sourceView = SourceView.MEDIA_BUTTON_BROADCAST_ACTION) | ||
| ACTION_PIP_PAUSE -> playbackManager.pause(sourceView = SourceView.MEDIA_BUTTON_BROADCAST_ACTION) | ||
| ACTION_PIP_SKIP_FORWARD -> playbackManager.skipForward(sourceView = SourceView.MEDIA_BUTTON_BROADCAST_ACTION) | ||
| } |
There was a problem hiding this comment.
PR description says PiP remote actions now “send KeyEvent keycodes via explicit service intents” with dynamic service selection, but this implementation uses a dynamically-registered BroadcastReceiver in the Activity that directly calls PlaybackManager methods. Either update the implementation to match the intended design (service intent + keycodes) or adjust the PR description to reflect the actual approach so reviewers/testers know what to validate.
There was a problem hiding this comment.
Good catch on the description mismatch. The PR description will be updated to reflect the BroadcastReceiver approach.
e250248 to
bf2eed9
Compare
7f72738 to
713ec73
Compare
bf2eed9 to
225fa59
Compare
713ec73 to
a382432
Compare
a382432 to
f18a518
Compare
| <!-- Both services start disabled; PlaybackServiceToggle.ensureCorrectServiceEnabled() | ||
| enables the correct one in Application.onCreate() based on the MEDIA3_SESSION feature flag. | ||
| This avoids the system resolving the wrong service before the toggle runs. --> | ||
| <service | ||
| android:name="au.com.shiftyjelly.pocketcasts.repositories.playback.PlaybackService" |
There was a problem hiding this comment.
The manifest removes the androidx.media.session.MediaButtonReceiver declaration, but the app still builds notification media action PendingIntents via MediaButtonReceiver.buildMediaButtonPendingIntent(...) (e.g., NotificationDrawerImpl.kt). Without a registered receiver, those notification action buttons (play/pause/skip/stop) will no longer be delivered. Either re-add the receiver (and ensure it forwards to the correct enabled service), or switch notification actions to target an explicit in-app component (e.g., your existing PlayerBroadcastReceiver or the enabled playback service) instead of MediaButtonReceiver.
| private val pipReceiver = object : BroadcastReceiver() { | ||
| override fun onReceive(context: Context, intent: Intent) { | ||
| when (intent.action) { | ||
| ACTION_PIP_SKIP_BACK -> playbackManager.skipBackward(sourceView = SourceView.MEDIA_BUTTON_BROADCAST_ACTION) | ||
| ACTION_PIP_PLAY -> playbackManager.playQueue(sourceView = SourceView.MEDIA_BUTTON_BROADCAST_ACTION) | ||
| ACTION_PIP_PAUSE -> playbackManager.pause(sourceView = SourceView.MEDIA_BUTTON_BROADCAST_ACTION) | ||
| ACTION_PIP_SKIP_FORWARD -> playbackManager.skipForward(sourceView = SourceView.MEDIA_BUTTON_BROADCAST_ACTION) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| override fun onCreate(savedInstanceState: Bundle?) { | ||
| super.onCreate(savedInstanceState) | ||
|
|
||
| val filter = IntentFilter().apply { | ||
| addAction(ACTION_PIP_SKIP_BACK) | ||
| addAction(ACTION_PIP_PLAY) | ||
| addAction(ACTION_PIP_PAUSE) | ||
| addAction(ACTION_PIP_SKIP_FORWARD) | ||
| } | ||
| ContextCompat.registerReceiver(this, pipReceiver, filter, ContextCompat.RECEIVER_NOT_EXPORTED) |
There was a problem hiding this comment.
ContextCompat.registerReceiver(..., RECEIVER_NOT_EXPORTED) only enforces the "not exported" behavior on Android 13+. On API 24–32 (your minSdk is 24), this dynamically-registered receiver can still be triggered by any other app sending a broadcast with these actions, which would allow external apps to control playback while the activity is alive (PiP). Consider routing PiP actions through an explicit non-exported manifest receiver/service, or registering the receiver with a signature-level broadcast permission so only your app can send these intents on pre-33.
289dd2a to
3abc684
Compare
- Move PlaybackServiceToggle call after Firebase and feature flag initialization Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Ensures the toggle reads the correct feature flag value instead of the default, since FeatureFlag.initialize() runs in appLifecycleObserver.setup(). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
f18a518 to
8e47b50
Compare
Description
Wires the mobile app to support the Media3 service toggle. The AndroidManifest now declares both PlaybackService (Media3) and LegacyPlaybackService as disabled; PlaybackServiceToggle.ensureCorrectServiceEnabled() is called in PocketCastsApplication.onCreate() to enable the correct one based on the MEDIA3_SESSION feature flag. VideoActivity's PiP remote actions are updated to send KeyEvent keycodes via explicit service intents instead of using MediaButtonReceiver with PlaybackStateCompat actions, with dynamic service class selection. The legacy MediaButtonReceiver manifest entry is removed. The obsolete PlaybackServiceTest instrumentation test is deleted as it tested the old PlaybackService internals directly.
Testing Instructions
Test the mobile app with
media3_sessionFF on and off. Remember to kill app process after changing flag!Checklist
./gradlew spotlessApplyto automatically apply formatting/linting)modules/services/localization/src/main/res/values/strings.xml