diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaLibrarySessionImpl.java b/libraries/session/src/main/java/androidx/media3/session/MediaLibrarySessionImpl.java index deb6094cba..03efd6db8e 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaLibrarySessionImpl.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaLibrarySessionImpl.java @@ -166,10 +166,10 @@ public ListenableFuture>> onGetChildrenOn if (!canResumePlaybackOnStart()) { return Futures.immediateFuture(LibraryResult.ofError(ERROR_NOT_SUPPORTED)); } - // Advertise support for playback resumption. If STATE_IDLE, the request arrives at boot time - // to get the full item data to build a notification. If not STATE_IDLE we don't need to - // deliver the full media item, so we do the minimal viable effort. - return getPlayerWrapper().getPlaybackState() == Player.STATE_IDLE + // Advertise support for playback resumption. If we're not playing, the request probably + // arrived at boot time to get the full item data to build a notification. If we're playing, + // we don't need to deliver the full media item, so we do the minimal viable effort. + return !getPlayerWrapper().getPlayWhenReady() ? getRecentMediaItemAtDeviceBootTime(browser, params) : Futures.immediateFuture( LibraryResult.ofItemList( @@ -470,7 +470,7 @@ private void postOrRunOnApplicationHandler(Runnable runnable) { ? checkNotNull(getMediaNotificationControllerInfo()) : controller; ListenableFuture future = - callback.onPlaybackResumption(instance, controller); + callback.onPlaybackResumption(instance, controller, false); Futures.addCallback( future, new FutureCallback() { diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java index 6fc261fbb7..1ca633058d 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java @@ -1729,6 +1729,17 @@ default ListenableFuture onSetMediaItems( new MediaItemsWithStartPosition(mediaItemList, startIndex, startPositionMs))); } + /** + * @deprecated Override {@link MediaSession.Callback#onPlaybackResumption(MediaSession, + * ControllerInfo, boolean)} instead. + */ + @Deprecated + @UnstableApi + default ListenableFuture onPlaybackResumption( + MediaSession mediaSession, ControllerInfo controller) { + return Futures.immediateFailedFuture(new UnsupportedOperationException()); + } + /** * Returns the playlist with which the player should be prepared when a controller requests to * play without a current {@link MediaItem}. @@ -1750,16 +1761,24 @@ default ListenableFuture onSetMediaItems( * Player#COMMAND_GET_CURRENT_MEDIA_ITEM} and either {@link Player#COMMAND_SET_MEDIA_ITEM} or * {@link Player#COMMAND_CHANGE_MEDIA_ITEMS} available. * + *

If {@code isForPlayback} is true, {@link ManuallyHandlePlaybackResumption} may be set on + * the future to manually handle playback resumption. Refer to the {@link + * ManuallyHandlePlaybackResumption} javadoc for more details. + * * @param mediaSession The media session for which playback resumption is requested. * @param controller The {@linkplain ControllerInfo controller} that requests the playback * resumption. This may be a short living controller created only for issuing a play command * for resuming playback. + * @param isForPlayback Whether playback is going to be started as a result of the future being + * completed. If false, SystemUI is querying for media item data in order to build and + * display the resumption notification at boot time. * @return The {@linkplain MediaItemsWithStartPosition playlist} to resume playback with. */ @UnstableApi + @SuppressWarnings("deprecation") // calling deprecated API for backwards compatibility default ListenableFuture onPlaybackResumption( - MediaSession mediaSession, ControllerInfo controller) { - return Futures.immediateFailedFuture(new UnsupportedOperationException()); + MediaSession mediaSession, ControllerInfo controller, boolean isForPlayback) { + return onPlaybackResumption(mediaSession, controller); } /** @@ -1875,6 +1894,25 @@ public int hashCode() { } } + /** + * Exception that may be set to the future in {@link + * MediaSession.Callback#onPlaybackResumption(MediaSession, ControllerInfo, boolean)} if the + * parameter {@code isForPlayback} is true, in order to signal that the app wishes to handle this + * resumption itself. This means the app is responsible for restoring a playlist, setting it to + * the player, and starting playback. + * + *

Do note this has various pitfalls and needs to be used carefully:
+ * - {@link MediaSession.Callback#onPlayerInteractionFinished(MediaSession, ControllerInfo, + * Player.Commands)} will NOT be called for the resumption command.
+ * - If playback is not actually resumed in this method, the resumption notification will end up + * non-functional, but will keep being displayed. This is not a suitable way to disable playback + * resumption, do not attempt to disable it this way.
+ * - If the app takes too long to go into foreground, the grant to go into foreground may have + * expired. + */ + @UnstableApi + public static class ManuallyHandlePlaybackResumption extends Exception {} + /** * A result for {@link Callback#onConnect(MediaSession, ControllerInfo)} to denote the set of * available commands and the media button preferences for a {@link ControllerInfo controller}. diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java index 602d314e6c..5a7e194399 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java @@ -1121,7 +1121,7 @@ protected MediaSessionServiceLegacyStub getLegacyBrowserService() { @Nullable ListenableFuture future = checkNotNull( - callback.onPlaybackResumption(instance, controllerForRequest), + callback.onPlaybackResumption(instance, controllerForRequest, true), "Callback.onPlaybackResumption must return a non-null future"); Futures.addCallback( future, @@ -1151,6 +1151,9 @@ public void onFailure(Throwable t) { + " media button receiver to your manifest or if you implement the recent" + " media item contract with your MediaLibraryService.", t); + } else if (t instanceof MediaSession.ManuallyHandlePlaybackResumption) { + // See ManuallyHandlePlaybackResumption javadoc for details. + return; } else { Log.e( TAG,