diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 55c33006ca..3534ff283b 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -134,6 +134,9 @@ `MediaBrowserCompat` when connecting to a legacy `MediaBrowserCompat`. The service can receive the connection hints passed in as root hints with the first call to `onGetRoot()`. + * Fix bug where a `MediaBrowser` connected to a legacy browser service, + didn't receive an error sent by the service after the browser has + subscribed to a `parentid`. * UI: * Make the stretched/cropped video in `PlayerView`-in-Compose-`AndroidView` workaround opt-in, due to issues diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaBrowserImplLegacy.java b/libraries/session/src/main/java/androidx/media3/session/MediaBrowserImplLegacy.java index 3c9ad16da1..1a852d5216 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaBrowserImplLegacy.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaBrowserImplLegacy.java @@ -151,15 +151,16 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; if (browserCompat == null) { return Futures.immediateFuture(LibraryResult.ofError(ERROR_SESSION_DISCONNECTED)); } + Bundle options = createOptionsForSubscription(params); SettableFuture> future = SettableFuture.create(); - SubscribeCallback callback = new SubscribeCallback(future); + SubscribeCallback callback = new SubscribeCallback(parentId, options, future); List list = subscribeCallbacks.get(parentId); if (list == null) { list = new ArrayList<>(); subscribeCallbacks.put(parentId, list); } list.add(callback); - browserCompat.subscribe(parentId, createOptions(params), callback); + browserCompat.subscribe(parentId, options, callback); return future; } @@ -199,7 +200,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; } SettableFuture>> future = SettableFuture.create(); - Bundle options = createOptions(params, page, pageSize); + Bundle options = createOptionsWithPagingInfo(params, page, pageSize); browserCompat.subscribe(parentId, options, new GetChildrenCallback(future, parentId)); return future; } @@ -295,7 +296,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; } SettableFuture>> future = SettableFuture.create(); - Bundle options = createOptions(params, page, pageSize); + Bundle options = createOptionsWithPagingInfo(params, page, pageSize); options.putInt(MediaBrowserCompat.EXTRA_PAGE, page); options.putInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, pageSize); browserCompat.search( @@ -367,12 +368,13 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; return browserCompats.get(extras); } - private static Bundle createOptions(@Nullable LibraryParams params) { + private static Bundle createOptionsForSubscription(@Nullable LibraryParams params) { return params == null ? new Bundle() : new Bundle(params.extras); } - private static Bundle createOptions(@Nullable LibraryParams params, int page, int pageSize) { - Bundle options = createOptions(params); + private static Bundle createOptionsWithPagingInfo( + @Nullable LibraryParams params, int page, int pageSize) { + Bundle options = createOptionsForSubscription(params); options.putInt(MediaBrowserCompat.EXTRA_PAGE, page); options.putInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, pageSize); return options; @@ -465,26 +467,33 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; private class SubscribeCallback extends SubscriptionCallback { + private final String subscriptionParentId; + private final Bundle subscriptionOptions; private final SettableFuture> future; - public SubscribeCallback(SettableFuture> future) { + public SubscribeCallback( + String subscriptionParentId, + Bundle subscriptionOptions, + SettableFuture> future) { + this.subscriptionParentId = subscriptionParentId; + this.subscriptionOptions = subscriptionOptions; this.future = future; } @Override public void onError(@Nullable String parentId) { - onErrorInternal(); + onError(subscriptionParentId, subscriptionOptions); } @Override public void onError(@Nullable String parentId, @Nullable Bundle options) { - onErrorInternal(); + onErrorInternal(subscriptionParentId, subscriptionOptions); } @Override public void onChildrenLoaded( @Nullable String parentId, @Nullable List children) { - onChildrenLoadedInternal(parentId, children); + onChildrenLoadedInternal(subscriptionParentId, children); } @Override @@ -492,17 +501,31 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; @Nullable String parentId, @Nullable List children, @Nullable Bundle options) { - onChildrenLoadedInternal(parentId, children); + onChildrenLoadedInternal(subscriptionParentId, children); } - private void onErrorInternal() { + private void onErrorInternal(String parentId, Bundle options) { + if (future.isDone()) { + // Delegate to the browser by calling `onChildrenChanged` that makes the app call + // `getChildren()` for which the service can return the appropriate error code. This makes a + // redundant call to `subscribe` that can be expected to be not expensive as it just returns + // an exception. + getInstance() + .notifyBrowserListener( + listener -> + listener.onChildrenChanged( + getInstance(), + parentId, + Integer.MAX_VALUE, + new LibraryParams.Builder().setExtras(options).build())); + } // Don't need to unsubscribe here, because MediaBrowserServiceCompat can notify children // changed after the initial failure and MediaBrowserCompat could receive the changes. future.set(LibraryResult.ofError(ERROR_UNKNOWN)); } private void onChildrenLoadedInternal( - @Nullable String parentId, @Nullable List children) { + String parentId, @Nullable List children) { if (TextUtils.isEmpty(parentId)) { Log.w(TAG, "SubscribeCallback.onChildrenLoaded(): Ignoring empty parentId"); return; @@ -526,7 +549,6 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; getInstance() .notifyBrowserListener( listener -> { - // TODO(b/193193565): Cache children result for later getChildren() calls. listener.onChildrenChanged(getInstance(), parentId, itemCount, params); }); future.set(LibraryResult.ofVoid()); diff --git a/libraries/test_session_common/src/main/java/androidx/media3/test/session/common/MediaBrowserConstants.java b/libraries/test_session_common/src/main/java/androidx/media3/test/session/common/MediaBrowserConstants.java index 6964ce022c..9521692217 100644 --- a/libraries/test_session_common/src/main/java/androidx/media3/test/session/common/MediaBrowserConstants.java +++ b/libraries/test_session_common/src/main/java/androidx/media3/test/session/common/MediaBrowserConstants.java @@ -50,6 +50,8 @@ public class MediaBrowserConstants { "parent_auth_expired_error_non_fatal"; public static final String PARENT_ID_AUTH_EXPIRED_ERROR_KEY_ERROR_RESOLUTION_ACTION_LABEL = "parent_auth_expired_error_label"; + public static final String PARENT_ID_ALLOW_FIRST_ON_GET_CHILDREN = + "parent_allow_first_on_get_children"; public static final List GET_CHILDREN_RESULT = new ArrayList<>(); public static final int CHILDREN_COUNT = 100; diff --git a/libraries/test_session_common/src/main/java/androidx/media3/test/session/common/MediaBrowserServiceCompatConstants.java b/libraries/test_session_common/src/main/java/androidx/media3/test/session/common/MediaBrowserServiceCompatConstants.java index e31fcd1d46..b5d3314e83 100644 --- a/libraries/test_session_common/src/main/java/androidx/media3/test/session/common/MediaBrowserServiceCompatConstants.java +++ b/libraries/test_session_common/src/main/java/androidx/media3/test/session/common/MediaBrowserServiceCompatConstants.java @@ -32,6 +32,8 @@ public class MediaBrowserServiceCompatConstants { public static final String TEST_SEND_CUSTOM_COMMAND = "sendCustomCommand"; public static final String TEST_MEDIA_ITEMS_WITH_BROWSE_ACTIONS = "getLibraryRoot_withBrowseActions"; + public static final String TEST_SUBSCRIBE_THEN_REJECT_ON_LOAD_CHILDREN = + "subscribe_thenRejectOnLoadChildren"; private MediaBrowserServiceCompatConstants() {} } diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserCompatWithMediaLibraryServiceTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserCompatWithMediaLibraryServiceTest.java index 83fa6e58ef..b9db91f0be 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserCompatWithMediaLibraryServiceTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserCompatWithMediaLibraryServiceTest.java @@ -36,6 +36,7 @@ import static androidx.media3.test.session.common.MediaBrowserConstants.CHILDREN import static androidx.media3.test.session.common.MediaBrowserConstants.CONNECTION_HINTS_KEY_LIBRARY_ERROR_REPLICATION_MODE; import static androidx.media3.test.session.common.MediaBrowserConstants.CUSTOM_ACTION; import static androidx.media3.test.session.common.MediaBrowserConstants.CUSTOM_ACTION_EXTRAS; +import static androidx.media3.test.session.common.MediaBrowserConstants.EXTRAS_KEY_NOTIFY_CHILDREN_CHANGED_MEDIA_ID; import static androidx.media3.test.session.common.MediaBrowserConstants.GET_CHILDREN_RESULT; import static androidx.media3.test.session.common.MediaBrowserConstants.LONG_LIST_COUNT; import static androidx.media3.test.session.common.MediaBrowserConstants.MEDIA_ID_GET_BROWSABLE_ITEM; @@ -43,6 +44,7 @@ import static androidx.media3.test.session.common.MediaBrowserConstants.MEDIA_ID import static androidx.media3.test.session.common.MediaBrowserConstants.MEDIA_ID_GET_ITEM_WITH_METADATA; import static androidx.media3.test.session.common.MediaBrowserConstants.MEDIA_ID_GET_PLAYABLE_ITEM; import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID; +import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_ALLOW_FIRST_ON_GET_CHILDREN; import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_AUTH_EXPIRED_ERROR; import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_AUTH_EXPIRED_ERROR_DEPRECATED; import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_AUTH_EXPIRED_ERROR_KEY_ERROR_RESOLUTION_ACTION_LABEL; @@ -670,7 +672,7 @@ public class MediaBrowserCompatWithMediaLibraryServiceTest @Test public void getChildren_emptyResult() throws Exception { String testParentId = PARENT_ID_NO_CHILDREN; - connectAndWait(/* rootHints= */ Bundle.EMPTY); + connectAndWait(/* rootHints= */ null); CountDownLatch latch = new CountDownLatch(1); AtomicReference> childrenRef = new AtomicReference<>(); @@ -712,8 +714,7 @@ public class MediaBrowserCompatWithMediaLibraryServiceTest } @Test - public void getChildren_browserNotifyChildrenChanged_callsOnChildrenLoadedTwice() - throws Exception { + public void subscribe_browserNotifyChildrenChanged_callsOnChildrenLoadedTwice() throws Exception { String testParentId = SUBSCRIBE_PARENT_ID_2; connectAndWait(/* rootHints= */ Bundle.EMPTY); CountDownLatch latch = new CountDownLatch(2); @@ -743,6 +744,53 @@ public class MediaBrowserCompatWithMediaLibraryServiceTest assertThat(childrenList.get(1)).hasSize(12); } + @Test + public void subscribe_onChildrenChangedWithMaxValue_convertedToOnError() throws Exception { + String testParentId = PARENT_ID_ALLOW_FIRST_ON_GET_CHILDREN; + connectAndWait(/* rootHints= */ Bundle.EMPTY); + CountDownLatch latch = new CountDownLatch(2); + List parentIds = new ArrayList<>(); + List optionsList = new ArrayList<>(); + List> childrenList = new ArrayList<>(); + Bundle requestNotifyChildrenWithDelayBundle = + createNotifyChildrenChangedBundle( + testParentId, + /* itemCount= */ Integer.MAX_VALUE, + /* delayMs= */ 100L, + /* broadcast= */ false); + requestNotifyChildrenWithDelayBundle.putInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, 12); + + browserCompat.subscribe( + testParentId, + requestNotifyChildrenWithDelayBundle, + new SubscriptionCallback() { + @Override + public void onChildrenLoaded(String parentId, List children, Bundle options) { + parentIds.add(parentId); + childrenList.add(children); + optionsList.add(options); + latch.countDown(); + } + + @Override + public void onError(String parentId, Bundle options) { + parentIds.add(parentId); + optionsList.add(options); + latch.countDown(); + } + }); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(parentIds).containsExactly(testParentId, testParentId); + assertThat(childrenList).hasSize(1); + assertThat(childrenList.get(0)).hasSize(12); + assertThat(optionsList).hasSize(2); + assertThat(optionsList.get(0).getString(EXTRAS_KEY_NOTIFY_CHILDREN_CHANGED_MEDIA_ID)) + .isEqualTo(PARENT_ID_ALLOW_FIRST_ON_GET_CHILDREN); + assertThat(optionsList.get(1).getString(EXTRAS_KEY_NOTIFY_CHILDREN_CHANGED_MEDIA_ID)) + .isEqualTo(PARENT_ID_ALLOW_FIRST_ON_GET_CHILDREN); + } + @Test public void getChildren_broadcastNotifyChildrenChanged_callsOnChildrenLoadedTwice() throws Exception { diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserListenerWithMediaBrowserServiceCompatTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserListenerWithMediaBrowserServiceCompatTest.java index 6c09d9b11b..8892dfd505 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserListenerWithMediaBrowserServiceCompatTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserListenerWithMediaBrowserServiceCompatTest.java @@ -35,6 +35,7 @@ import static androidx.media3.test.session.common.MediaBrowserServiceCompatConst import static androidx.media3.test.session.common.MediaBrowserServiceCompatConstants.TEST_MEDIA_ITEMS_WITH_BROWSE_ACTIONS; import static androidx.media3.test.session.common.MediaBrowserServiceCompatConstants.TEST_ON_CHILDREN_CHANGED_SUBSCRIBE_AND_UNSUBSCRIBE; import static androidx.media3.test.session.common.MediaBrowserServiceCompatConstants.TEST_SEND_CUSTOM_COMMAND; +import static androidx.media3.test.session.common.MediaBrowserServiceCompatConstants.TEST_SUBSCRIBE_THEN_REJECT_ON_LOAD_CHILDREN; import static androidx.media3.test.session.common.TestUtils.TIMEOUT_MS; import static com.google.common.truth.Truth.assertThat; import static com.google.common.util.concurrent.MoreExecutors.directExecutor; @@ -65,6 +66,7 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import org.junit.After; import org.junit.Before; @@ -140,6 +142,7 @@ public class MediaBrowserListenerWithMediaBrowserServiceCompatTest { @Test public void connect_useConnectionHints_connectionHintsPassedToLegacyServerOnGetRootAsRootHints() throws Exception { + remoteService.setProxyForTest(TEST_GET_CHILDREN); Bundle connectionHints = new Bundle(); connectionHints.putBoolean(EXTRAS_KEY_SEND_ROOT_HINTS_AS_SESSION_EXTRAS, true); CountDownLatch latch = new CountDownLatch(/* count= */ 1); @@ -610,4 +613,47 @@ public class MediaBrowserListenerWithMediaBrowserServiceCompatTest { assertThat(sessionResultRef.get().resultCode).isEqualTo(SessionResult.RESULT_ERROR_UNKNOWN); assertThat(sessionResultRef.get().extras.getString("key-1")).isEqualTo("error-from-service"); } + + @Test + public void + subscribe_thenNullListOnLoadChildren_exceptionConvertedToOnChildrenChangedIntegerMaxValue() + throws Exception { + remoteService.setProxyForTest(TEST_SUBSCRIBE_THEN_REJECT_ON_LOAD_CHILDREN); + CountDownLatch onChildrenChangedLatch = new CountDownLatch(2); + AtomicBoolean onErrorCalled = new AtomicBoolean(); + List changedParentIds = new ArrayList<>(); + List changedItemCounts = new ArrayList<>(); + MediaBrowser browser = + createBrowser( + new MediaBrowser.Listener() { + @Override + public void onChildrenChanged( + MediaBrowser browser, + String parentId, + int itemCount, + @Nullable LibraryParams params) { + changedParentIds.add(parentId); + changedItemCounts.add(itemCount); + onChildrenChangedLatch.countDown(); + } + + @Override + public void onError(MediaController controller, SessionError sessionError) { + onErrorCalled.set(true); + } + }); + + ListenableFuture> future = + threadTestRule + .getHandler() + .postAndSync(() -> browser.subscribe("parentId", new LibraryParams.Builder().build())); + // Trigger calling onLoadChildren that is rejected. + remoteService.notifyChildrenChanged("parentId"); + + assertThat(future.get().resultCode).isEqualTo(RESULT_SUCCESS); + assertThat(onChildrenChangedLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(changedParentIds).containsExactly("parentId", "parentId"); + assertThat(changedItemCounts).containsExactly(2, Integer.MAX_VALUE).inOrder(); + assertThat(onErrorCalled.get()).isFalse(); + } } diff --git a/libraries/test_session_current/src/main/java/androidx/media3/session/MockMediaBrowserServiceCompat.java b/libraries/test_session_current/src/main/java/androidx/media3/session/MockMediaBrowserServiceCompat.java index db7912ded3..ce0d2b554f 100644 --- a/libraries/test_session_current/src/main/java/androidx/media3/session/MockMediaBrowserServiceCompat.java +++ b/libraries/test_session_current/src/main/java/androidx/media3/session/MockMediaBrowserServiceCompat.java @@ -35,6 +35,7 @@ import static androidx.media3.test.session.common.MediaBrowserServiceCompatConst import static androidx.media3.test.session.common.MediaBrowserServiceCompatConstants.TEST_MEDIA_ITEMS_WITH_BROWSE_ACTIONS; import static androidx.media3.test.session.common.MediaBrowserServiceCompatConstants.TEST_ON_CHILDREN_CHANGED_SUBSCRIBE_AND_UNSUBSCRIBE; import static androidx.media3.test.session.common.MediaBrowserServiceCompatConstants.TEST_SEND_CUSTOM_COMMAND; +import static androidx.media3.test.session.common.MediaBrowserServiceCompatConstants.TEST_SUBSCRIBE_THEN_REJECT_ON_LOAD_CHILDREN; import static java.lang.Math.min; import android.content.Intent; @@ -100,7 +101,6 @@ public class MockMediaBrowserServiceCompat extends MediaBrowserServiceCompat { sessionCompat.setCallback(new MediaSessionCompat.Callback() {}); sessionCompat.setActive(true); setSessionToken(sessionCompat.getSessionToken()); - testBinder = new RemoteMediaBrowserServiceCompatStub(sessionCompat); } @@ -299,6 +299,9 @@ public class MockMediaBrowserServiceCompat extends MediaBrowserServiceCompat { case TEST_MEDIA_ITEMS_WITH_BROWSE_ACTIONS: setProxyForMediaItemsWithBrowseActions(session); break; + case TEST_SUBSCRIBE_THEN_REJECT_ON_LOAD_CHILDREN: + setProxyForSubscribeAndRejectGetChildren(); + break; default: throw new IllegalArgumentException("Unknown testName: " + testName); } @@ -456,6 +459,45 @@ public class MockMediaBrowserServiceCompat extends MediaBrowserServiceCompat { }); } + private void setProxyForSubscribeAndRejectGetChildren() { + setMediaBrowserServiceProxy( + new MockMediaBrowserServiceCompat.Proxy() { + + private boolean isSubscribed; + + @Override + public void onLoadChildren(String parentId, Result> result) { + onLoadChildren(parentId, result, new Bundle()); + } + + @Override + public void onLoadChildren( + String parentId, Result> result, Bundle options) { + if (isSubscribed) { + // Accept the first call that a Media3 browser interprets as a successful + // subscription. Then reject any further access to onLoadChildren(). + result.sendResult(null); + return; + } + isSubscribed = true; + result.sendResult( + ImmutableList.of( + new MediaItem( + new MediaDescriptionCompat.Builder() + .setMediaUri(Uri.parse("http://www.example.com/1")) + .setMediaId("mediaId1") + .build(), + MediaItem.FLAG_PLAYABLE), + new MediaItem( + new MediaDescriptionCompat.Builder() + .setMediaUri(Uri.parse("http://www.example.com/2")) + .setMediaId("mediaId2") + .build(), + MediaItem.FLAG_PLAYABLE))); + } + }); + } + private void getChildren_authenticationError_receivesPlaybackException( MediaSessionCompat session, boolean isFatal) { setMediaBrowserServiceProxy( diff --git a/libraries/test_session_current/src/main/java/androidx/media3/session/MockMediaLibraryService.java b/libraries/test_session_current/src/main/java/androidx/media3/session/MockMediaLibraryService.java index 14f80db5c4..9db26df33a 100644 --- a/libraries/test_session_current/src/main/java/androidx/media3/session/MockMediaLibraryService.java +++ b/libraries/test_session_current/src/main/java/androidx/media3/session/MockMediaLibraryService.java @@ -42,6 +42,7 @@ import static androidx.media3.test.session.common.MediaBrowserConstants.MEDIA_ID import static androidx.media3.test.session.common.MediaBrowserConstants.MEDIA_ID_GET_ITEM_WITH_METADATA; import static androidx.media3.test.session.common.MediaBrowserConstants.MEDIA_ID_GET_PLAYABLE_ITEM; import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID; +import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_ALLOW_FIRST_ON_GET_CHILDREN; import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_AUTH_EXPIRED_ERROR; import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_AUTH_EXPIRED_ERROR_DEPRECATED; import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_AUTH_EXPIRED_ERROR_KEY_ERROR_RESOLUTION_ACTION_LABEL; @@ -227,6 +228,7 @@ public class MockMediaLibraryService extends MediaLibraryService { playlistAddExtras.putString("key-1", "playlist_add"); Bundle radioExtras = new Bundle(); radioExtras.putString("key-1", "radio"); + Log.d("notifyChildrenChanged", "new TestLibrarySessionCallback()"); session = new MediaLibrarySession.Builder( MockMediaLibraryService.this, @@ -284,6 +286,8 @@ public class MockMediaLibraryService extends MediaLibraryService { private class TestLibrarySessionCallback implements MediaLibrarySession.Callback { + private int getChildrenCallCount = 0; + @Override public MediaSession.ConnectionResult onConnect( MediaSession session, ControllerInfo controller) { @@ -351,6 +355,7 @@ public class MockMediaLibraryService extends MediaLibraryService { } switch (mediaId) { case MEDIA_ID_GET_BROWSABLE_ITEM: + case PARENT_ID_ALLOW_FIRST_ON_GET_CHILDREN: case SUBSCRIBE_PARENT_ID_2: return Futures.immediateFuture( LibraryResult.ofItem(createBrowsableMediaItem(mediaId), /* params= */ null)); @@ -380,6 +385,7 @@ public class MockMediaLibraryService extends MediaLibraryService { int page, int pageSize, @Nullable LibraryParams params) { + getChildrenCallCount++; assertLibraryParams(params); if (Objects.equals(parentId, PARENT_ID_NO_CHILDREN)) { return Futures.immediateFuture(LibraryResult.ofItemList(ImmutableList.of(), params)); @@ -399,6 +405,15 @@ public class MockMediaLibraryService extends MediaLibraryService { errorBundle.putString("key", "value"); return Futures.immediateFuture( LibraryResult.ofError(new SessionError(ERROR_BAD_VALUE, "error message", errorBundle))); + } else if (Objects.equals(parentId, PARENT_ID_ALLOW_FIRST_ON_GET_CHILDREN)) { + return getChildrenCallCount == 1 + ? Futures.immediateFuture( + LibraryResult.ofItemList( + getPaginatedResult(GET_CHILDREN_RESULT, page, pageSize), params)) + : Futures.immediateFuture( + LibraryResult.ofError( + LibraryResult.RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED, + new LibraryParams.Builder().build())); } else if (Objects.equals(parentId, PARENT_ID_AUTH_EXPIRED_ERROR) || Objects.equals(parentId, PARENT_ID_AUTH_EXPIRED_ERROR_DEPRECATED) || Objects.equals(parentId, PARENT_ID_SKIP_LIMIT_REACHED_ERROR)) {