Return full media item when SystemUI calls service on device boot time

#minor-release

PiperOrigin-RevId: 533465595
This commit is contained in:
bachinger 2023-05-19 16:49:30 +01:00 committed by Ian Baker
parent d5f9cf4f19
commit 3fa666c687
2 changed files with 219 additions and 5 deletions

View File

@ -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.MediaConstants.ERROR_CODE_AUTHENTICATION_EXPIRED_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.content.Context;
@ -43,9 +45,11 @@ 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.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.SettableFuture;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
@ -169,9 +173,14 @@ import java.util.concurrent.Future;
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))
if (!canResumePlaybackOnStart()) {
return 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(
LibraryResult.ofItemList(
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;
}
}

View File

@ -23,6 +23,7 @@ import android.content.Context;
import androidx.annotation.Nullable;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MediaMetadata;
import androidx.media3.common.Player;
import androidx.media3.session.MediaLibraryService.LibraryParams;
import androidx.media3.session.MediaLibraryService.MediaLibrarySession;
import androidx.media3.session.MediaSession.ControllerInfo;
@ -176,13 +177,23 @@ public class MediaLibrarySessionCallbackTest {
}
@Test
public void onGetChildren_systemUiCallForRecentItems_returnsRecentItems() throws Exception {
public void onGetChildren_systemUiCallForRecentItemsWhenIdle_callsOnPlaybackResumption()
throws Exception {
ArrayList<MediaItem> mediaItems = MediaTestUtils.createMediaItems(/* size= */ 3);
MockMediaLibraryService service = new MockMediaLibraryService();
service.attachBaseContext(context);
CountDownLatch latch = new CountDownLatch(1);
CountDownLatch latch = new CountDownLatch(2);
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= */ 1, /* startPositionMs= */ 1000L));
}
@Override
public ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> onGetChildren(
MediaLibrarySession session,
@ -203,6 +214,166 @@ public class MediaLibrarySessionCallbackTest {
.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("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 =
browser.getChildren(
"androidx.media3.session.recent.root",