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:
parent
eb8ec87a5c
commit
9bf6b7ea20
@ -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(
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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()) {
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user