Return full media item when SystemUI calls service on device boot time
#minor-release PiperOrigin-RevId: 533465595
This commit is contained in:
parent
d5f9cf4f19
commit
3fa666c687
@ -24,6 +24,8 @@ import static androidx.media3.session.LibraryResult.RESULT_ERROR_SESSION_AUTHENT
|
|||||||
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;
|
||||||
import static androidx.media3.session.MediaConstants.EXTRAS_KEY_ERROR_RESOLUTION_ACTION_INTENT_COMPAT;
|
import static androidx.media3.session.MediaConstants.EXTRAS_KEY_ERROR_RESOLUTION_ACTION_INTENT_COMPAT;
|
||||||
|
import static java.lang.Math.max;
|
||||||
|
import static java.lang.Math.min;
|
||||||
|
|
||||||
import android.app.PendingIntent;
|
import android.app.PendingIntent;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
@ -43,9 +45,11 @@ 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.FutureCallback;
|
||||||
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 com.google.common.util.concurrent.MoreExecutors;
|
import com.google.common.util.concurrent.MoreExecutors;
|
||||||
|
import com.google.common.util.concurrent.SettableFuture;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
@ -169,9 +173,14 @@ import java.util.concurrent.Future;
|
|||||||
int pageSize,
|
int pageSize,
|
||||||
@Nullable LibraryParams params) {
|
@Nullable LibraryParams params) {
|
||||||
if (Objects.equals(parentId, RECENT_LIBRARY_ROOT_MEDIA_ID)) {
|
if (Objects.equals(parentId, RECENT_LIBRARY_ROOT_MEDIA_ID)) {
|
||||||
// Advertise support for playback resumption, if enabled.
|
if (!canResumePlaybackOnStart()) {
|
||||||
return !canResumePlaybackOnStart()
|
return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_NOT_SUPPORTED));
|
||||||
? Futures.immediateFuture(LibraryResult.ofError(RESULT_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
|
||||||
|
? getRecentMediaItemAtDeviceBootTime(browser, params)
|
||||||
: Futures.immediateFuture(
|
: Futures.immediateFuture(
|
||||||
LibraryResult.ofItemList(
|
LibraryResult.ofItemList(
|
||||||
ImmutableList.of(
|
ImmutableList.of(
|
||||||
@ -386,4 +395,38 @@ import java.util.concurrent.Future;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ListenableFuture<LibraryResult<ImmutableList<MediaItem>>>
|
||||||
|
getRecentMediaItemAtDeviceBootTime(
|
||||||
|
ControllerInfo controller, @Nullable LibraryParams params) {
|
||||||
|
SettableFuture<LibraryResult<ImmutableList<MediaItem>>> settableFuture =
|
||||||
|
SettableFuture.create();
|
||||||
|
ListenableFuture<MediaSession.MediaItemsWithStartPosition> future =
|
||||||
|
callback.onPlaybackResumption(instance, controller);
|
||||||
|
Futures.addCallback(
|
||||||
|
future,
|
||||||
|
new FutureCallback<MediaSession.MediaItemsWithStartPosition>() {
|
||||||
|
@Override
|
||||||
|
public void onSuccess(MediaSession.MediaItemsWithStartPosition playlist) {
|
||||||
|
if (playlist.mediaItems.isEmpty()) {
|
||||||
|
settableFuture.set(
|
||||||
|
LibraryResult.ofError(LibraryResult.RESULT_ERROR_INVALID_STATE, params));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int sanitizedStartIndex =
|
||||||
|
max(0, min(playlist.startIndex, playlist.mediaItems.size() - 1));
|
||||||
|
settableFuture.set(
|
||||||
|
LibraryResult.ofItemList(
|
||||||
|
ImmutableList.of(playlist.mediaItems.get(sanitizedStartIndex)), params));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(Throwable t) {
|
||||||
|
settableFuture.set(LibraryResult.ofError(LibraryResult.RESULT_ERROR_UNKNOWN, params));
|
||||||
|
Log.e(TAG, "Failed fetching recent media item at boot time: " + t.getMessage(), t);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
MoreExecutors.directExecutor());
|
||||||
|
return settableFuture;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,7 @@ import android.content.Context;
|
|||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.media3.common.MediaItem;
|
import androidx.media3.common.MediaItem;
|
||||||
import androidx.media3.common.MediaMetadata;
|
import androidx.media3.common.MediaMetadata;
|
||||||
|
import androidx.media3.common.Player;
|
||||||
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;
|
||||||
@ -176,13 +177,23 @@ public class MediaLibrarySessionCallbackTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void onGetChildren_systemUiCallForRecentItems_returnsRecentItems() throws Exception {
|
public void onGetChildren_systemUiCallForRecentItemsWhenIdle_callsOnPlaybackResumption()
|
||||||
|
throws Exception {
|
||||||
ArrayList<MediaItem> mediaItems = MediaTestUtils.createMediaItems(/* size= */ 3);
|
ArrayList<MediaItem> mediaItems = MediaTestUtils.createMediaItems(/* size= */ 3);
|
||||||
MockMediaLibraryService service = new MockMediaLibraryService();
|
MockMediaLibraryService service = new MockMediaLibraryService();
|
||||||
service.attachBaseContext(context);
|
service.attachBaseContext(context);
|
||||||
CountDownLatch latch = new CountDownLatch(1);
|
CountDownLatch latch = new CountDownLatch(2);
|
||||||
MediaLibrarySession.Callback callback =
|
MediaLibrarySession.Callback callback =
|
||||||
new MediaLibrarySession.Callback() {
|
new MediaLibrarySession.Callback() {
|
||||||
|
@Override
|
||||||
|
public ListenableFuture<MediaSession.MediaItemsWithStartPosition> onPlaybackResumption(
|
||||||
|
MediaSession mediaSession, ControllerInfo controller) {
|
||||||
|
latch.countDown();
|
||||||
|
return Futures.immediateFuture(
|
||||||
|
new MediaSession.MediaItemsWithStartPosition(
|
||||||
|
mediaItems, /* startIndex= */ 1, /* startPositionMs= */ 1000L));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> onGetChildren(
|
public ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> onGetChildren(
|
||||||
MediaLibrarySession session,
|
MediaLibrarySession session,
|
||||||
@ -203,6 +214,166 @@ public class MediaLibrarySessionCallbackTest {
|
|||||||
.build());
|
.build());
|
||||||
RemoteMediaBrowser browser = controllerTestRule.createRemoteBrowser(session.getToken());
|
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("mediaItem_2");
|
||||||
|
assertThat(children.value).isEqualTo(mediaItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void
|
||||||
|
onGetChildren_systemUiCallForRecentItemsWhenIdleWithEmptyResumptionPlaylist_resultInvalidState()
|
||||||
|
throws Exception {
|
||||||
|
MockMediaLibraryService service = new MockMediaLibraryService();
|
||||||
|
service.attachBaseContext(context);
|
||||||
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
MediaLibrarySession.Callback callback =
|
||||||
|
new MediaLibrarySession.Callback() {
|
||||||
|
@Override
|
||||||
|
public ListenableFuture<MediaSession.MediaItemsWithStartPosition> onPlaybackResumption(
|
||||||
|
MediaSession mediaSession, ControllerInfo controller) {
|
||||||
|
latch.countDown();
|
||||||
|
return Futures.immediateFuture(
|
||||||
|
new MediaSession.MediaItemsWithStartPosition(
|
||||||
|
ImmutableList.of(), /* startIndex= */ 11, /* startPositionMs= */ 1000L));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
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);
|
||||||
|
|
||||||
|
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
|
||||||
|
assertThat(recentItem.resultCode).isEqualTo(LibraryResult.RESULT_ERROR_INVALID_STATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void
|
||||||
|
onGetChildren_systemUiCallForRecentItemsWhenIdleStartIndexTooHigh_setToLastItemItemInList()
|
||||||
|
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<MediaSession.MediaItemsWithStartPosition> onPlaybackResumption(
|
||||||
|
MediaSession mediaSession, ControllerInfo controller) {
|
||||||
|
latch.countDown();
|
||||||
|
return Futures.immediateFuture(
|
||||||
|
new MediaSession.MediaItemsWithStartPosition(
|
||||||
|
mediaItems, /* startIndex= */ 11, /* startPositionMs= */ 1000L));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
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);
|
||||||
|
|
||||||
|
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
|
||||||
|
assertThat(recentItem.resultCode).isEqualTo(LibraryResult.RESULT_SUCCESS);
|
||||||
|
assertThat(Lists.transform(recentItem.value, (item) -> item.mediaId))
|
||||||
|
.containsExactly("mediaItem_3");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void onGetChildren_systemUiCallForRecentItemsWhenIdleStartIndexNegative_setToZero()
|
||||||
|
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<MediaSession.MediaItemsWithStartPosition> onPlaybackResumption(
|
||||||
|
MediaSession mediaSession, ControllerInfo controller) {
|
||||||
|
latch.countDown();
|
||||||
|
return Futures.immediateFuture(
|
||||||
|
new MediaSession.MediaItemsWithStartPosition(
|
||||||
|
mediaItems, /* startIndex= */ -11, /* startPositionMs= */ 1000L));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
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);
|
||||||
|
|
||||||
|
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
|
||||||
|
assertThat(recentItem.resultCode).isEqualTo(LibraryResult.RESULT_SUCCESS);
|
||||||
|
assertThat(Lists.transform(recentItem.value, (item) -> item.mediaId))
|
||||||
|
.containsExactly("mediaItem_1");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void onGetChildren_systemUiCallForRecentItemsWhenNotIdle_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));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
player.playbackState = Player.STATE_READY;
|
||||||
|
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 =
|
LibraryResult<ImmutableList<MediaItem>> recentItem =
|
||||||
browser.getChildren(
|
browser.getChildren(
|
||||||
"androidx.media3.session.recent.root",
|
"androidx.media3.session.recent.root",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user