Skip to content

Wire mobile app for Media3 service toggle#5150

Merged
geekygecko merged 3 commits intomedia3/06b-session-wiringfrom
media3/06c-mobile-integration
Apr 8, 2026
Merged

Wire mobile app for Media3 service toggle#5150
geekygecko merged 3 commits intomedia3/06b-session-wiringfrom
media3/06c-mobile-integration

Conversation

@sztomek
Copy link
Copy Markdown
Contributor

@sztomek sztomek commented Mar 19, 2026

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_session FF on and off. Remember to kill app process after changing flag!

  1. Start playing an episode
  2. Check media notification -- action buttons should work and player state should be in sync with the fullscreen player.
  3. Start playing a video
  4. Do the same as you did in point 2
  5. Touch the video, enter PiP mode
  6. Check PiP controls work fine

Checklist

  • If this is a user-facing change, I have added an entry in CHANGELOG.md
  • Ensure the linter passes (./gradlew spotlessApply to automatically apply formatting/linting)
  • I have considered whether it makes sense to add tests for my changes
  • All strings that need to be localized are in modules/services/localization/src/main/res/values/strings.xml
  • Any jetpack compose components I added or changed are covered by compose previews
  • I have updated (or requested that someone edit) the spreadsheet to reflect any new or changed analytics.

@sztomek sztomek added this to the 8.10 milestone Mar 19, 2026
Copilot AI review requested due to automatic review settings March 19, 2026 15:09
@sztomek sztomek requested a review from a team as a code owner March 19, 2026 15:09
@sztomek sztomek requested review from geekygecko and removed request for a team March 19, 2026 15:09
@sztomek sztomek added do not merge [Type] Enhancement Improve an existing feature. [Area] Playback Episode playback issue labels Mar 19, 2026
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

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() during PocketCastsApplication.onCreate().
  • Update VideoActivity PiP 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.MediaButtonReceiver manifest 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.

Comment on lines +160 to +161
PlaybackServiceToggle.ensureCorrectServiceEnabled(this)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

@wpmobilebot
Copy link
Copy Markdown
Collaborator

wpmobilebot commented Mar 19, 2026

Project manifest changes for app

The following changes in the app's merged AndroidManifest.xml file were detected (build variant: release):

--- ./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 Artifacts tab and audit the files.

</intent-filter>
</service>

<service

Check warning

Code scanning / Android Lint

Exported service does not require permission Warning

Exported service does not require permission
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

@sztomek sztomek modified the milestones: 8.10, 8.9 Mar 20, 2026
@sztomek sztomek force-pushed the media3/06b-session-wiring branch from aba1e9a to e1fc3e9 Compare March 27, 2026 16:14
@sztomek sztomek force-pushed the media3/06c-mobile-integration branch from 42a46be to f96355f Compare March 27, 2026 16:17
@wpmobilebot wpmobilebot modified the milestones: 8.9, 8.10 Mar 30, 2026
@wpmobilebot
Copy link
Copy Markdown
Collaborator

Version 8.9 has now entered code-freeze, so the milestone of this PR has been updated to 8.10.

@sztomek sztomek force-pushed the media3/06b-session-wiring branch from e1fc3e9 to e250248 Compare April 1, 2026 18:10
Copilot AI review requested due to automatic review settings April 1, 2026 18:12
@sztomek sztomek force-pushed the media3/06c-mobile-integration branch from f96355f to 7f72738 Compare April 1, 2026 18:12
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

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() during PocketCastsApplication.onCreate().
  • Update VideoActivity PiP RemoteActions to use PendingIntent.getBroadcast() with custom actions handled by a dynamically registered receiver.
  • Update AndroidManifest.xml to declare both playback services disabled by default and remove the legacy MediaButtonReceiver; 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>

Comment on lines 161 to 168
RxJavaUncaughtExceptionHandling.setUp()
setupCrashLogging()
setupLogging()
setupAnalytics()

PlaybackServiceToggle.ensureCorrectServiceEnabled(this)

setupApp()
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch — fixed. Moved PlaybackServiceToggle.ensureCorrectServiceEnabled() to after appLifecycleObserver.setup() which calls FeatureFlag.initialize().

Comment on lines +56 to +63
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)
}
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch on the description mismatch. The PR description will be updated to reflect the BroadcastReceiver approach.

@sztomek sztomek force-pushed the media3/06b-session-wiring branch from e250248 to bf2eed9 Compare April 1, 2026 20:34
@sztomek sztomek force-pushed the media3/06c-mobile-integration branch from 7f72738 to 713ec73 Compare April 1, 2026 20:38
@sztomek sztomek force-pushed the media3/06b-session-wiring branch from bf2eed9 to 225fa59 Compare April 2, 2026 10:57
@sztomek sztomek force-pushed the media3/06c-mobile-integration branch from 713ec73 to a382432 Compare April 2, 2026 10:57
Copilot AI review requested due to automatic review settings April 2, 2026 11:18
@sztomek sztomek force-pushed the media3/06c-mobile-integration branch from a382432 to f18a518 Compare April 2, 2026 11:18
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

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

Comment on lines +602 to 606
<!-- 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"
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +56 to +76
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)
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
@sztomek sztomek force-pushed the media3/06b-session-wiring branch from 289dd2a to 3abc684 Compare April 2, 2026 11:30
sztomek and others added 3 commits April 2, 2026 13:30
- 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>
@sztomek sztomek force-pushed the media3/06c-mobile-integration branch from f18a518 to 8e47b50 Compare April 2, 2026 11:31
@geekygecko geekygecko merged commit d76ec99 into media3/06b-session-wiring Apr 8, 2026
19 checks passed
@geekygecko geekygecko deleted the media3/06c-mobile-integration branch April 8, 2026 07:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Area] Playback Episode playback issue [Type] Enhancement Improve an existing feature.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants