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
This commit is contained in:
bachinger 2023-05-12 16:41:29 +01:00 committed by Ian Baker
parent eb8ec87a5c
commit 9bf6b7ea20
5 changed files with 141 additions and 3 deletions

View File

@ -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.checkState;
import static androidx.media3.common.util.Assertions.checkStateNotNull; import static androidx.media3.common.util.Assertions.checkStateNotNull;
import static androidx.media3.common.util.Util.postOrRun; 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_ERROR_SESSION_AUTHENTICATION_EXPIRED;
import static androidx.media3.session.LibraryResult.RESULT_SUCCESS; import static androidx.media3.session.LibraryResult.RESULT_SUCCESS;
import static androidx.media3.session.MediaConstants.ERROR_CODE_AUTHENTICATION_EXPIRED_COMPAT; 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.annotation.Nullable;
import androidx.collection.ArrayMap; import androidx.collection.ArrayMap;
import androidx.media3.common.MediaItem; import androidx.media3.common.MediaItem;
import androidx.media3.common.MediaMetadata;
import androidx.media3.common.Player; import androidx.media3.common.Player;
import androidx.media3.common.util.BitmapLoader; import androidx.media3.common.util.BitmapLoader;
import androidx.media3.common.util.Log; 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.ControllerCb;
import androidx.media3.session.MediaSession.ControllerInfo; import androidx.media3.session.MediaSession.ControllerInfo;
import com.google.common.collect.ImmutableList; 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.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.MoreExecutors;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.concurrent.CancellationException; import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
@ -52,12 +56,16 @@ import java.util.concurrent.Future;
/* package */ class MediaLibrarySessionImpl extends MediaSessionImpl { /* 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 instance;
private final MediaLibrarySession.Callback callback; private final MediaLibrarySession.Callback callback;
@GuardedBy("lock") @GuardedBy("lock")
private final ArrayMap<ControllerCb, Set<String>> subscriptions; private final ArrayMap<ControllerCb, Set<String>> subscriptions;
/** Creates an instance. */
public MediaLibrarySessionImpl( public MediaLibrarySessionImpl(
MediaLibrarySession instance, MediaLibrarySession instance,
Context context, Context context,
@ -123,6 +131,24 @@ import java.util.concurrent.Future;
public ListenableFuture<LibraryResult<MediaItem>> onGetLibraryRootOnHandler( public ListenableFuture<LibraryResult<MediaItem>> onGetLibraryRootOnHandler(
ControllerInfo browser, @Nullable LibraryParams params) { 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<LibraryResult<MediaItem>> future = ListenableFuture<LibraryResult<MediaItem>> future =
callback.onGetLibraryRoot(instance, browser, params); callback.onGetLibraryRoot(instance, browser, params);
future.addListener( future.addListener(
@ -142,6 +168,23 @@ import java.util.concurrent.Future;
int page, int page,
int pageSize, int pageSize,
@Nullable LibraryParams params) { @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<LibraryResult<ImmutableList<MediaItem>>> future = ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> future =
callback.onGetChildren(instance, browser, parentId, page, pageSize, params); callback.onGetChildren(instance, browser, parentId, page, pageSize, params);
future.addListener( future.addListener(

View File

@ -560,6 +560,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
} }
} }
/* package */ boolean canResumePlaybackOnStart() {
return sessionLegacyStub.canResumePlaybackOnStart();
}
/* package */ void setMediaSessionListener(MediaSession.Listener listener) { /* package */ void setMediaSessionListener(MediaSession.Listener listener) {
this.mediaSessionListener = listener; this.mediaSessionListener = listener;
} }

View File

@ -630,6 +630,10 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
return connectedControllersManager; return connectedControllersManager;
} }
/* package */ boolean canResumePlaybackOnStart() {
return canResumePlaybackOnStart;
}
private void dispatchSessionTaskWithPlayerCommand( private void dispatchSessionTaskWithPlayerCommand(
@Player.Command int command, SessionTask task, @Nullable RemoteUserInfo remoteUserInfo) { @Player.Command int command, SessionTask task, @Nullable RemoteUserInfo remoteUserInfo) {
if (sessionImpl.isReleased()) { if (sessionImpl.isReleased()) {

View File

@ -21,6 +21,8 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS;
import android.content.Context; import android.content.Context;
import androidx.annotation.Nullable; 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.LibraryParams;
import androidx.media3.session.MediaLibraryService.MediaLibrarySession; import androidx.media3.session.MediaLibraryService.MediaLibrarySession;
import androidx.media3.session.MediaSession.ControllerInfo; 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.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest; 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.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import java.util.ArrayList;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import org.junit.Before; import org.junit.Before;
import org.junit.ClassRule; import org.junit.ClassRule;
@ -132,4 +137,86 @@ public class MediaLibrarySessionCallbackTest {
browser.unsubscribe(testParentId); browser.unsubscribe(testParentId);
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); 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<LibraryResult<MediaItem>> 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<MediaItem> 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<MediaItem> 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<LibraryResult<ImmutableList<MediaItem>>> 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<ImmutableList<MediaItem>> 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<ImmutableList<MediaItem>> 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);
}
} }

View File

@ -27,7 +27,7 @@
<activity android:name="androidx.media3.test.session.common.SurfaceActivity" <activity android:name="androidx.media3.test.session.common.SurfaceActivity"
android:exported="false" /> android:exported="false" />
<receiver android:name="androidx.media.session.MediaButtonReceiver" <receiver android:name="androidx.media3.session.MediaButtonReceiver"
android:exported="true" android:exported="true"
android:process=":remote"> android:process=":remote">
<intent-filter> <intent-filter>