From 9bf6b7ea2062435970cea132c295659b48508b4e Mon Sep 17 00:00:00 2001 From: bachinger Date: Fri, 12 May 2023 16:41:29 +0100 Subject: [PATCH] Implement SystemUI contract for media resumption When a `MediaButtonReceiver` is found in the manifest, the library can implement the contract of SystemUI to signal that the app wants a playback resumption notification to be displayed. And, vice versa, if no `MediaButtonReceiver` is in the manifest, the library will signal to not show the notification after the app has been terminated. #minor-release PiperOrigin-RevId: 531516023 --- .../session/MediaLibrarySessionImpl.java | 43 +++++++++ .../media3/session/MediaSessionImpl.java | 4 + .../session/MediaSessionLegacyStub.java | 4 + .../MediaLibrarySessionCallbackTest.java | 87 +++++++++++++++++++ .../src/main/AndroidManifest.xml | 6 +- 5 files changed, 141 insertions(+), 3 deletions(-) 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 e8287f65c0..5fda9c6e82 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaLibrarySessionImpl.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaLibrarySessionImpl.java @@ -19,6 +19,7 @@ import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkState; import static androidx.media3.common.util.Assertions.checkStateNotNull; import static androidx.media3.common.util.Util.postOrRun; +import static androidx.media3.session.LibraryResult.RESULT_ERROR_NOT_SUPPORTED; import static androidx.media3.session.LibraryResult.RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED; import static androidx.media3.session.LibraryResult.RESULT_SUCCESS; import static androidx.media3.session.MediaConstants.ERROR_CODE_AUTHENTICATION_EXPIRED_COMPAT; @@ -33,6 +34,7 @@ import androidx.annotation.GuardedBy; import androidx.annotation.Nullable; import androidx.collection.ArrayMap; import androidx.media3.common.MediaItem; +import androidx.media3.common.MediaMetadata; import androidx.media3.common.Player; import androidx.media3.common.util.BitmapLoader; import androidx.media3.common.util.Log; @@ -41,10 +43,12 @@ import androidx.media3.session.MediaLibraryService.MediaLibrarySession; import androidx.media3.session.MediaSession.ControllerCb; import androidx.media3.session.MediaSession.ControllerInfo; import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import java.util.HashSet; import java.util.List; +import java.util.Objects; import java.util.Set; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; @@ -52,12 +56,16 @@ import java.util.concurrent.Future; /* package */ class MediaLibrarySessionImpl extends MediaSessionImpl { + private static final String RECENT_LIBRARY_ROOT_MEDIA_ID = "androidx.media3.session.recent.root"; + private static final String SYSTEM_UI_PACKAGE_NAME = "com.android.systemui"; + private final MediaLibrarySession instance; private final MediaLibrarySession.Callback callback; @GuardedBy("lock") private final ArrayMap> subscriptions; + /** Creates an instance. */ public MediaLibrarySessionImpl( MediaLibrarySession instance, Context context, @@ -123,6 +131,24 @@ import java.util.concurrent.Future; public ListenableFuture> onGetLibraryRootOnHandler( ControllerInfo browser, @Nullable LibraryParams params) { + if (params != null + && params.isRecent + && Objects.equals(browser.getPackageName(), SYSTEM_UI_PACKAGE_NAME)) { + // Advertise support for playback resumption, if enabled. + return !canResumePlaybackOnStart() + ? Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_NOT_SUPPORTED)) + : Futures.immediateFuture( + LibraryResult.ofItem( + new MediaItem.Builder() + .setMediaId(RECENT_LIBRARY_ROOT_MEDIA_ID) + .setMediaMetadata( + new MediaMetadata.Builder() + .setIsBrowsable(true) + .setIsPlayable(false) + .build()) + .build(), + params)); + } ListenableFuture> future = callback.onGetLibraryRoot(instance, browser, params); future.addListener( @@ -142,6 +168,23 @@ import java.util.concurrent.Future; int page, int pageSize, @Nullable LibraryParams params) { + if (Objects.equals(parentId, RECENT_LIBRARY_ROOT_MEDIA_ID)) { + // Advertise support for playback resumption, if enabled. + return !canResumePlaybackOnStart() + ? Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_NOT_SUPPORTED)) + : Futures.immediateFuture( + LibraryResult.ofItemList( + ImmutableList.of( + new MediaItem.Builder() + .setMediaId("androidx.media3.session.recent.item") + .setMediaMetadata( + new MediaMetadata.Builder() + .setIsBrowsable(false) + .setIsPlayable(true) + .build()) + .build()), + params)); + } ListenableFuture>> future = callback.onGetChildren(instance, browser, parentId, page, pageSize, params); future.addListener( 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 e82f64fc47..db50614290 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java @@ -560,6 +560,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } } + /* package */ boolean canResumePlaybackOnStart() { + return sessionLegacyStub.canResumePlaybackOnStart(); + } + /* package */ void setMediaSessionListener(MediaSession.Listener listener) { this.mediaSessionListener = listener; } diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java index 4c9dbf4353..cc9075b3df 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java @@ -630,6 +630,10 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; return connectedControllersManager; } + /* package */ boolean canResumePlaybackOnStart() { + return canResumePlaybackOnStart; + } + private void dispatchSessionTaskWithPlayerCommand( @Player.Command int command, SessionTask task, @Nullable RemoteUserInfo remoteUserInfo) { if (sessionImpl.isReleased()) { diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaLibrarySessionCallbackTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaLibrarySessionCallbackTest.java index 7c61699150..3eedbacb6c 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaLibrarySessionCallbackTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaLibrarySessionCallbackTest.java @@ -21,6 +21,8 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS; import android.content.Context; import androidx.annotation.Nullable; +import androidx.media3.common.MediaItem; +import androidx.media3.common.MediaMetadata; import androidx.media3.session.MediaLibraryService.LibraryParams; import androidx.media3.session.MediaLibraryService.MediaLibrarySession; import androidx.media3.session.MediaSession.ControllerInfo; @@ -29,8 +31,11 @@ import androidx.media3.test.session.common.MainLooperTestRule; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.MediumTest; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import java.util.ArrayList; import java.util.concurrent.CountDownLatch; import org.junit.Before; import org.junit.ClassRule; @@ -132,4 +137,86 @@ public class MediaLibrarySessionCallbackTest { browser.unsubscribe(testParentId); assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); } + + @Test + public void onGetLibraryRoot_callForRecentRootNonSystemUiPackageName_notIntercepted() + throws Exception { + MediaItem mediaItem = + new MediaItem.Builder() + .setMediaId("rootMediaId") + .setMediaMetadata( + new MediaMetadata.Builder().setIsPlayable(false).setIsBrowsable(true).build()) + .build(); + MockMediaLibraryService service = new MockMediaLibraryService(); + service.attachBaseContext(context); + CountDownLatch latch = new CountDownLatch(1); + MediaLibrarySession.Callback callback = + new MediaLibrarySession.Callback() { + @Override + public ListenableFuture> onGetLibraryRoot( + MediaLibrarySession session, ControllerInfo browser, @Nullable LibraryParams params) { + if (params != null && params.isRecent) { + latch.countDown(); + } + return Futures.immediateFuture(LibraryResult.ofItem(mediaItem, params)); + } + }; + MediaLibrarySession session = + sessionTestRule.ensureReleaseAfterTest( + new MediaLibrarySession.Builder(service, player, callback) + .setId("onGetChildren_callForRecentRootNonSystemUiPackageName_notIntercepted") + .build()); + RemoteMediaBrowser browser = controllerTestRule.createRemoteBrowser(session.getToken()); + + LibraryResult libraryRoot = + browser.getLibraryRoot(new LibraryParams.Builder().setRecent(true).build()); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(libraryRoot.value).isEqualTo(mediaItem); + } + + @Test + public void onGetChildren_systemUiCallForRecentItems_returnsRecentItems() throws Exception { + ArrayList mediaItems = MediaTestUtils.createMediaItems(/* size= */ 3); + MockMediaLibraryService service = new MockMediaLibraryService(); + service.attachBaseContext(context); + CountDownLatch latch = new CountDownLatch(1); + MediaLibrarySession.Callback callback = + new MediaLibrarySession.Callback() { + @Override + public ListenableFuture>> onGetChildren( + MediaLibrarySession session, + ControllerInfo browser, + String parentId, + int page, + int pageSize, + @Nullable LibraryParams params) { + latch.countDown(); + return Futures.immediateFuture( + LibraryResult.ofItemList(mediaItems, /* params= */ null)); + } + }; + MediaLibrarySession session = + sessionTestRule.ensureReleaseAfterTest( + new MediaLibrarySession.Builder(service, player, callback) + .setId("onGetChildren_systemUiCallForRecentItems_returnsRecentItems") + .build()); + RemoteMediaBrowser browser = controllerTestRule.createRemoteBrowser(session.getToken()); + + LibraryResult> recentItem = + browser.getChildren( + "androidx.media3.session.recent.root", + /* page= */ 0, + /* pageSize= */ 100, + /* params= */ null); + // Load children of a non recent root that must not be intercepted. + LibraryResult> children = + browser.getChildren("children", /* page= */ 0, /* pageSize= */ 100, /* params= */ null); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(recentItem.resultCode).isEqualTo(LibraryResult.RESULT_SUCCESS); + assertThat(Lists.transform(recentItem.value, (item) -> item.mediaId)) + .containsExactly("androidx.media3.session.recent.item"); + assertThat(children.value).isEqualTo(mediaItems); + } } diff --git a/libraries/test_session_current/src/main/AndroidManifest.xml b/libraries/test_session_current/src/main/AndroidManifest.xml index 457b099fd8..b873a58120 100644 --- a/libraries/test_session_current/src/main/AndroidManifest.xml +++ b/libraries/test_session_current/src/main/AndroidManifest.xml @@ -27,9 +27,9 @@ - +