From 06d61ffaaae784c0ced8dad6bc1b95272bafe439 Mon Sep 17 00:00:00 2001 From: bachinger Date: Mon, 8 Jul 2024 09:41:55 -0700 Subject: [PATCH] Allow an app to decide to not start the service Once a service is started as a foreground service, it must be started into the foreground. This means an app can not suppress a play command arriving from the `MediaButtonReceiver` once the receiver has started the service. This change adds a method to the `MediaButtonReceiver` that allows app to suppress starting the service to not get into this situation of wanting to suppress the play command after the service is already started. Issue: androidx/media#1528 PiperOrigin-RevId: 650280025 --- RELEASENOTES.md | 6 +++ .../media3/session/MediaButtonReceiver.java | 43 +++++++++++++++++-- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index b61b9e995d..2c1dfd63be 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -45,6 +45,12 @@ * Muxers: * IMA extension: * Session: + * Add `MediaButtonReceiver.shouldStartForegroundService(Intent)` to allow + apps to suppress a play command coming in for playback resumption by + overriding this method. By default, the service is always started and + playback can't be suppressed without the system crashing the service + with a `ForegroundServiceDidNotStartInTimeException` + ([#1528](https://github.com/google/ExoPlayer/issues/1528)). * UI: * Downloads: * OkHttp Extension: diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaButtonReceiver.java b/libraries/session/src/main/java/androidx/media3/session/MediaButtonReceiver.java index 815e9c2898..1141a175f8 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaButtonReceiver.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaButtonReceiver.java @@ -118,11 +118,16 @@ public class MediaButtonReceiver extends BroadcastReceiver { return; } + @Nullable + KeyEvent keyEvent = checkNotNull(intent.getExtras()).getParcelable(Intent.EXTRA_KEY_EVENT); + if (keyEvent == null + || keyEvent.getAction() != KeyEvent.ACTION_DOWN + || keyEvent.getRepeatCount() != 0) { + // Only handle the intent once with the earliest key event that arrives. + return; + } if (Util.SDK_INT >= 26) { - @Nullable - KeyEvent keyEvent = checkNotNull(intent.getExtras()).getParcelable(Intent.EXTRA_KEY_EVENT); - if (keyEvent != null - && keyEvent.getKeyCode() != KeyEvent.KEYCODE_MEDIA_PLAY + if (keyEvent.getKeyCode() != KeyEvent.KEYCODE_MEDIA_PLAY && keyEvent.getKeyCode() != KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) { // Starting with Android 8 (API 26), the service must be started immediately in the // foreground when being started. Also starting with Android 8, the system sends media @@ -143,6 +148,14 @@ public class MediaButtonReceiver extends BroadcastReceiver { ComponentName mediaButtonServiceComponentName = getServiceComponentByAction(context, action); if (mediaButtonServiceComponentName != null) { intent.setComponent(mediaButtonServiceComponentName); + if (!shouldStartForegroundService(intent)) { + Log.i( + TAG, + "onReceive(Intent) does not start the media button event target service into the" + + " foreground on app request: " + + mediaButtonServiceComponentName.getClassName()); + return; + } try { ContextCompat.startForegroundService(context, intent); } catch (/* ForegroundServiceStartNotAllowedException */ IllegalStateException e) { @@ -161,6 +174,28 @@ public class MediaButtonReceiver extends BroadcastReceiver { "Could not find any Service that handles any of the actions " + Arrays.toString(ACTIONS)); } + /** + * Returns whether to start the {@linkplain Intent#getComponent() media button event target + * service} into the foreground. + * + *

Returns true by default. Apps can override this method to decide to not start a service when + * receiving an event with {@link KeyEvent#KEYCODE_MEDIA_PLAY} or {@link + * KeyEvent#KEYCODE_MEDIA_PLAY_PAUSE} that should be suppressed. + * + *

Note: Once the service is started into the foreground by the receiver, the app must start + * playback to get into the foreground or the system will crash the service with a {@code + * ForegroundServiceDidNotStartInTimeException} or an {@link IllegalStateException}. + * + * @param intent The intent that {@linkplain #onReceive(Context, Intent) was received by the media + * button event receiver}. + * @return true if the service should be {@linkplain ContextCompat#startForegroundService(Context, + * Intent) started as a foreground service}. If false is returned the service is not started + * and the receiver call is a no-op. + */ + protected boolean shouldStartForegroundService(Intent intent) { + return true; + } + /** * This method is called when an exception is thrown when calling {@link * Context#startForegroundService(Intent)} as a result of receiving a media button event.