From 3ac7e0e84eaa9aea1541ae4bfc85380135a93f7c Mon Sep 17 00:00:00 2001 From: bachinger Date: Tue, 29 Mar 2022 17:00:52 +0100 Subject: [PATCH] Update error state of legacy playback state if authentication fails This change adds the ability to update the error code of the PlaybackStateCompat in cases we need this for backwards compatibility. It is applied in the least intrusive way because normally, return values of a service method should not change the state of the `PlaybackStateCompat`, just because it has nothing to do with the playback state but rather with the state of the `MediaLibrarySession`. For this reason only the error code `RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED` is taken into account while all other error codes are not mapped to the `PlaybackStateCompat'. PiperOrigin-RevId: 438038852 --- .../media3/session/LibraryResult.java | 17 ++- .../media3/session/MediaConstants.java | 27 +++++ .../session/MediaLibrarySessionImpl.java | 100 ++++++++++++------ .../media3/session/PlayerWrapper.java | 54 ++++++++++ .../session/src/main/res/values/strings.xml | 1 + .../session/common/MediaBrowserConstants.java | 3 + ...wserCompatWithMediaLibraryServiceTest.java | 44 ++++++++ ...wserCompatWithMediaSessionServiceTest.java | 25 +++++ .../session/MockMediaLibraryService.java | 21 ++++ 9 files changed, 256 insertions(+), 36 deletions(-) diff --git a/libraries/session/src/main/java/androidx/media3/session/LibraryResult.java b/libraries/session/src/main/java/androidx/media3/session/LibraryResult.java index 17c921eff4..f602e35379 100644 --- a/libraries/session/src/main/java/androidx/media3/session/LibraryResult.java +++ b/libraries/session/src/main/java/androidx/media3/session/LibraryResult.java @@ -218,11 +218,24 @@ public final class LibraryResult implements Bundleable { * @param errorCode The error code. */ public static LibraryResult ofError(@Code int errorCode) { + return ofError(errorCode, /* params= */ null); + } + + /** + * Creates an instance with an unsuccessful {@link Code result code} and {@link LibraryParams} to + * describe the error. + * + *

{@code errorCode} must not be {@link #RESULT_SUCCESS}. + * + * @param errorCode The error code. + * @param params The optional parameters to describe the error. + */ + public static LibraryResult ofError(@Code int errorCode, @Nullable LibraryParams params) { checkArgument(errorCode != RESULT_SUCCESS); return new LibraryResult<>( - errorCode, + /* resultCode= */ errorCode, SystemClock.elapsedRealtime(), - /* params= */ null, + /* params= */ params, /* value= */ null, VALUE_TYPE_ERROR); } diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaConstants.java b/libraries/session/src/main/java/androidx/media3/session/MediaConstants.java index 217757283d..9ad21b8784 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaConstants.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaConstants.java @@ -113,6 +113,33 @@ public final class MediaConstants { */ public static final String MEDIA_URI_QUERY_URI = "uri"; + /** + * The extras key for the localized error resolution string. + * + *

See {@link + * androidx.media.utils.MediaConstants#PLAYBACK_STATE_EXTRAS_KEY_ERROR_RESOLUTION_ACTION_LABEL}. + */ + public static final String EXTRAS_KEY_ERROR_RESOLUTION_ACTION_LABEL_COMPAT = + "android.media.extras.ERROR_RESOLUTION_ACTION_LABEL"; + /** + * The extras key for the error resolution intent. + * + *

See {@link + * androidx.media.utils.MediaConstants#PLAYBACK_STATE_EXTRAS_KEY_ERROR_RESOLUTION_ACTION_INTENT}. + */ + public static final String EXTRAS_KEY_ERROR_RESOLUTION_ACTION_INTENT_COMPAT = + "android.media.extras.ERROR_RESOLUTION_ACTION_INTENT"; + + /** The legacy status code for successful execution. */ + public static final int STATUS_CODE_SUCCESS_COMPAT = -1; + + /** + * The legacy error code for expired authentication. + * + *

See {@code PlaybackStateCompat#ERROR_CODE_AUTHENTICATION_EXPIRED}. + */ + public static final int ERROR_CODE_AUTHENTICATION_EXPIRED_COMPAT = 3; + /* package */ static final String SESSION_COMMAND_ON_EXTRAS_CHANGED = "androidx.media3.session.SESSION_COMMAND_ON_EXTRAS_CHANGED"; /* package */ static final String SESSION_COMMAND_ON_CAPTIONING_ENABLED_CHANGED = diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaLibrarySessionImpl.java b/libraries/session/src/main/java/androidx/media3/session/MediaLibrarySessionImpl.java index f33347f961..5faa6bdc12 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaLibrarySessionImpl.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaLibrarySessionImpl.java @@ -18,7 +18,10 @@ package androidx.media3.session; import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkState; import static androidx.media3.common.util.Assertions.checkStateNotNull; +import static androidx.media3.session.LibraryResult.RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED; 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 android.app.PendingIntent; import android.content.Context; @@ -118,19 +121,17 @@ import java.util.concurrent.Future; public ListenableFuture> onGetLibraryRootOnHandler( ControllerInfo browser, @Nullable LibraryParams params) { - // onGetLibraryRoot is defined to return a non-null result but it's implemented by applications, - // so we explicitly null-check the result to fail early if an app accidentally returns null. - return checkNotNull( - callback.onGetLibraryRoot(instance, browser, params), - "onGetLibraryRoot must return non-null future"); - } - - public ListenableFuture> onGetItemOnHandler( - ControllerInfo browser, String mediaId) { - // onGetItem is defined to return a non-null result but it's implemented by applications, - // so we explicitly null-check the result to fail early if an app accidentally returns null. - return checkNotNull( - callback.onGetItem(instance, browser, mediaId), "onGetItem must return non-null future"); + ListenableFuture> future = + callback.onGetLibraryRoot(instance, browser, params); + future.addListener( + () -> { + @Nullable LibraryResult result = tryGetFutureResult(future); + if (result != null) { + maybeUpdateLegacyErrorState(result); + } + }, + MoreExecutors.directExecutor()); + return future; } public ListenableFuture>> onGetChildrenOnHandler( @@ -139,16 +140,13 @@ import java.util.concurrent.Future; int page, int pageSize, @Nullable LibraryParams params) { - // onGetChildren is defined to return a non-null result but it's implemented by applications, - // so we explicitly null-check the result to fail early if an app accidentally returns null. ListenableFuture>> future = - checkNotNull( - callback.onGetChildren(instance, browser, parentId, page, pageSize, params), - "onGetChildren must return non-null future"); + callback.onGetChildren(instance, browser, parentId, page, pageSize, params); future.addListener( () -> { @Nullable LibraryResult> result = tryGetFutureResult(future); if (result != null) { + maybeUpdateLegacyErrorState(result); verifyResultItems(result, pageSize); } }, @@ -156,6 +154,21 @@ import java.util.concurrent.Future; return future; } + public ListenableFuture> onGetItemOnHandler( + ControllerInfo browser, String mediaId) { + ListenableFuture> future = + callback.onGetItem(instance, browser, mediaId); + future.addListener( + () -> { + @Nullable LibraryResult result = tryGetFutureResult(future); + if (result != null) { + maybeUpdateLegacyErrorState(result); + } + }, + MoreExecutors.directExecutor()); + return future; + } + public ListenableFuture> onSubscribeOnHandler( ControllerInfo browser, String parentId, @Nullable LibraryParams params) { ControllerCb controller = checkStateNotNull(browser.getControllerCb()); @@ -193,12 +206,8 @@ import java.util.concurrent.Future; public ListenableFuture> onUnsubscribeOnHandler( ControllerInfo browser, String parentId) { - // onUnsubscribe is defined to return a non-null result but it's implemented by applications, - // so we explicitly null-check the result to fail early if an app accidentally returns null. ListenableFuture> future = - checkNotNull( - callback.onUnsubscribe(instance, browser, parentId), - "onUnsubscribe must return non-null future"); + callback.onUnsubscribe(instance, browser, parentId); future.addListener( () -> removeSubscription(checkStateNotNull(browser.getControllerCb()), parentId), @@ -209,11 +218,17 @@ import java.util.concurrent.Future; public ListenableFuture> onSearchOnHandler( ControllerInfo browser, String query, @Nullable LibraryParams params) { - // onSearch is defined to return a non-null result but it's implemented by applications, - // so we explicitly null-check the result to fail early if an app accidentally returns null. - return checkNotNull( - callback.onSearch(instance, browser, query, params), - "onSearch must return non-null future"); + ListenableFuture> future = + callback.onSearch(instance, browser, query, params); + future.addListener( + () -> { + @Nullable LibraryResult result = tryGetFutureResult(future); + if (result != null) { + maybeUpdateLegacyErrorState(result); + } + }, + MoreExecutors.directExecutor()); + return future; } public ListenableFuture>> onGetSearchResultOnHandler( @@ -222,17 +237,13 @@ import java.util.concurrent.Future; int page, int pageSize, @Nullable LibraryParams params) { - // onGetSearchResult is defined to return a non-null result but it's implemented by - // applications, so we explicitly null-check the result to fail early if an app accidentally - // returns null. ListenableFuture>> future = - checkNotNull( - callback.onGetSearchResult(instance, browser, query, page, pageSize, params), - "onGetSearchResult must return non-null future"); + callback.onGetSearchResult(instance, browser, query, page, pageSize, params); future.addListener( () -> { @Nullable LibraryResult> result = tryGetFutureResult(future); if (result != null) { + maybeUpdateLegacyErrorState(result); verifyResultItems(result, pageSize); } }, @@ -277,6 +288,27 @@ import java.util.concurrent.Future; return true; } + private void maybeUpdateLegacyErrorState(LibraryResult result) { + PlayerWrapper playerWrapper = getPlayerWrapper(); + if (result.resultCode == RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED + && result.params != null + && result.params.extras.containsKey(EXTRAS_KEY_ERROR_RESOLUTION_ACTION_INTENT_COMPAT)) { + // Mapping this error to the legacy error state provides backwards compatibility for the + // Automotive OS sign-in. + MediaSessionCompat mediaSessionCompat = getSessionCompat(); + if (playerWrapper.getLegacyStatusCode() != RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED) { + playerWrapper.setLegacyErrorStatus( + ERROR_CODE_AUTHENTICATION_EXPIRED_COMPAT, + getContext().getString(R.string.authentication_required), + result.params.extras); + mediaSessionCompat.setPlaybackState(playerWrapper.createPlaybackStateCompat()); + } + } else if (playerWrapper.getLegacyStatusCode() != RESULT_SUCCESS) { + playerWrapper.clearLegacyErrorStatus(); + getSessionCompat().setPlaybackState(playerWrapper.createPlaybackStateCompat()); + } + } + @Nullable private static T tryGetFutureResult(Future future) { checkState(future.isDone()); diff --git a/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java b/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java index cdac091033..36fd98bfa2 100644 --- a/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java +++ b/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java @@ -15,10 +15,13 @@ */ package androidx.media3.session; +import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkState; import static androidx.media3.common.util.Util.postOrRun; +import static androidx.media3.session.MediaConstants.STATUS_CODE_SUCCESS_COMPAT; import android.media.AudioManager; +import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.SystemClock; @@ -52,8 +55,46 @@ import java.util.List; */ /* package */ class PlayerWrapper extends ForwardingPlayer { + private int legacyStatusCode; + @Nullable private String legacyErrorMessage; + @Nullable private Bundle legacyErrorExtras; + public PlayerWrapper(Player player) { super(player); + legacyStatusCode = STATUS_CODE_SUCCESS_COMPAT; + } + + /** + * Sets the legacy error code. + * + *

This sets the legacy {@link PlaybackStateCompat} to {@link PlaybackStateCompat#STATE_ERROR} + * and calls {@link PlaybackStateCompat.Builder#setErrorMessage(int, CharSequence)} and {@link + * PlaybackStateCompat.Builder#setExtras(Bundle)} with the given arguments. + * + *

Use {@link #clearLegacyErrorStatus()} to clear the error state and to resume to the actual + * playback state reflecting the player. + * + * @param errorCode The legacy error code. + * @param errorMessage The legacy error message. + * @param extras The extras. + */ + public void setLegacyErrorStatus(int errorCode, String errorMessage, Bundle extras) { + checkState(errorCode != STATUS_CODE_SUCCESS_COMPAT); + legacyStatusCode = errorCode; + legacyErrorMessage = errorMessage; + legacyErrorExtras = extras; + } + + /** Returns the legacy status code. */ + public int getLegacyStatusCode() { + return legacyStatusCode; + } + + /** Clears the legacy error status. */ + public void clearLegacyErrorStatus() { + legacyStatusCode = STATUS_CODE_SUCCESS_COMPAT; + legacyErrorMessage = null; + legacyErrorExtras = null; } @Override @@ -702,6 +743,19 @@ import java.util.List; } public PlaybackStateCompat createPlaybackStateCompat() { + if (legacyStatusCode != STATUS_CODE_SUCCESS_COMPAT) { + return new PlaybackStateCompat.Builder() + .setState( + PlaybackStateCompat.STATE_ERROR, + /* position= */ PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, + /* playbackSpeed= */ 0, + /* updateTime= */ SystemClock.elapsedRealtime()) + .setActions(0) + .setBufferedPosition(0) + .setErrorMessage(legacyStatusCode, checkNotNull(legacyErrorMessage)) + .setExtras(checkNotNull(legacyErrorExtras)) + .build(); + } @Nullable PlaybackException playerError = getPlayerError(); int state = MediaUtils.convertToPlaybackStateCompatState( diff --git a/libraries/session/src/main/res/values/strings.xml b/libraries/session/src/main/res/values/strings.xml index 4b5b6c86e0..06eef42afe 100644 --- a/libraries/session/src/main/res/values/strings.xml +++ b/libraries/session/src/main/res/values/strings.xml @@ -28,4 +28,5 @@ Seek back Seek forward + Authentication required 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 6a132efbc1..b20ec27ab2 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 @@ -36,6 +36,9 @@ public class MediaBrowserConstants { public static final String PARENT_ID_LONG_LIST = "parent_id_long_list"; public static final String PARENT_ID_NO_CHILDREN = "parent_id_no_children"; public static final String PARENT_ID_ERROR = "parent_id_error"; + public static final String PARENT_ID_AUTH_EXPIRED_ERROR = "parent_auth_expired_error"; + public static final String PARENT_ID_AUTH_EXPIRED_ERROR_KEY_ERROR_RESOLUTION_ACTION_LABEL = + "parent_auth_expired_error_label"; public static final List GET_CHILDREN_RESULT = new ArrayList<>(); public static final int CHILDREN_COUNT = 100; 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 7bb315e509..aadc857a3c 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 @@ -32,6 +32,8 @@ 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_AUTH_EXPIRED_ERROR; +import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_AUTH_EXPIRED_ERROR_KEY_ERROR_RESOLUTION_ACTION_LABEL; import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_ERROR; import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_LONG_LIST; import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_NO_CHILDREN; @@ -59,6 +61,7 @@ import android.support.v4.media.MediaBrowserCompat.MediaItem; import android.support.v4.media.MediaBrowserCompat.SearchCallback; import android.support.v4.media.MediaBrowserCompat.SubscriptionCallback; import android.support.v4.media.MediaDescriptionCompat; +import android.support.v4.media.session.PlaybackStateCompat; import android.text.TextUtils; import androidx.media3.test.session.common.TestUtils; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -301,6 +304,47 @@ public class MediaBrowserCompatWithMediaLibraryServiceTest assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); } + @Test + public void getChildren_authErrorResult() throws InterruptedException { + String testParentId = PARENT_ID_AUTH_EXPIRED_ERROR; + connectAndWait(); + CountDownLatch errorLatch = new CountDownLatch(1); + browserCompat.subscribe( + testParentId, + new SubscriptionCallback() { + @Override + public void onError(String parentId) { + assertThat(parentId).isEqualTo(testParentId); + errorLatch.countDown(); + } + }); + assertThat(errorLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(lastReportedPlaybackStateCompat).isNotNull(); + assertThat(lastReportedPlaybackStateCompat.getState()) + .isEqualTo(PlaybackStateCompat.STATE_ERROR); + assertThat( + lastReportedPlaybackStateCompat + .getExtras() + .getString(MediaConstants.EXTRAS_KEY_ERROR_RESOLUTION_ACTION_LABEL_COMPAT)) + .isEqualTo(PARENT_ID_AUTH_EXPIRED_ERROR_KEY_ERROR_RESOLUTION_ACTION_LABEL); + + CountDownLatch successLatch = new CountDownLatch(1); + browserCompat.subscribe( + PARENT_ID, + new SubscriptionCallback() { + @Override + public void onChildrenLoaded(String parentId, List children) { + assertThat(parentId).isEqualTo(PARENT_ID); + successLatch.countDown(); + } + }); + assertThat(successLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + // Any successful calls remove the error state, + assertThat(lastReportedPlaybackStateCompat.getState()) + .isNotEqualTo(PlaybackStateCompat.STATE_ERROR); + assertThat(lastReportedPlaybackStateCompat.getExtras()).isNull(); + } + @Test public void getChildren_emptyResult() throws InterruptedException { String testParentId = PARENT_ID_NO_CHILDREN; diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserCompatWithMediaSessionServiceTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserCompatWithMediaSessionServiceTest.java index 18315ca674..64db577d13 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserCompatWithMediaSessionServiceTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserCompatWithMediaSessionServiceTest.java @@ -25,6 +25,8 @@ import android.content.ComponentName; import android.content.Context; import android.support.v4.media.MediaBrowserCompat; import android.support.v4.media.session.MediaControllerCompat; +import android.support.v4.media.session.PlaybackStateCompat; +import androidx.annotation.Nullable; import androidx.media3.test.session.common.HandlerThreadTestRule; import androidx.media3.test.session.common.TestHandler; import androidx.test.core.app.ApplicationProvider; @@ -56,7 +58,9 @@ public class MediaBrowserCompatWithMediaSessionServiceTest { Context context; TestHandler handler; MediaBrowserCompat browserCompat; + @Nullable MediaControllerCompat controllerCompat; TestConnectionCallback connectionCallback; + @Nullable PlaybackStateCompat lastReportedPlaybackStateCompat; @Before public void setUp() throws Exception { @@ -117,6 +121,8 @@ public class MediaBrowserCompatWithMediaSessionServiceTest { public final CountDownLatch suspendedLatch = new CountDownLatch(1); public final CountDownLatch failedLatch = new CountDownLatch(1); + @Nullable MediaControllerCompat.Callback controllerCompatCallback; + TestConnectionCallback() { super(); } @@ -124,19 +130,38 @@ public class MediaBrowserCompatWithMediaSessionServiceTest { @Override public void onConnected() { super.onConnected(); + // Make browser's internal handler to be initialized with test thread. + controllerCompat = new MediaControllerCompat(context, browserCompat.getSessionToken()); + controllerCompatCallback = + new MediaControllerCompat.Callback() { + @Override + public void onPlaybackStateChanged(PlaybackStateCompat state) { + lastReportedPlaybackStateCompat = state; + } + }; + controllerCompat.registerCallback(controllerCompatCallback); connectedLatch.countDown(); } @Override public void onConnectionSuspended() { super.onConnectionSuspended(); + unregisterControllerCallback(); suspendedLatch.countDown(); } @Override public void onConnectionFailed() { super.onConnectionFailed(); + unregisterControllerCallback(); failedLatch.countDown(); } + + private void unregisterControllerCallback() { + if (controllerCompat != null && controllerCompatCallback != null) { + controllerCompat.unregisterCallback(controllerCompatCallback); + } + controllerCompatCallback = null; + } } } 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 5b08c544bb..49550bbb05 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 @@ -16,6 +16,8 @@ package androidx.media3.session; import static androidx.media3.common.util.Assertions.checkNotNull; +import static androidx.media3.session.MediaConstants.EXTRAS_KEY_ERROR_RESOLUTION_ACTION_INTENT_COMPAT; +import static androidx.media3.session.MediaConstants.EXTRAS_KEY_ERROR_RESOLUTION_ACTION_LABEL_COMPAT; import static androidx.media3.session.MediaTestUtils.assertLibraryParamsEquals; import static androidx.media3.test.session.common.CommonConstants.SUPPORT_APP_PACKAGE_NAME; import static androidx.media3.test.session.common.MediaBrowserConstants.CUSTOM_ACTION; @@ -30,6 +32,8 @@ import static androidx.media3.test.session.common.MediaBrowserConstants.MEDIA_ID import static androidx.media3.test.session.common.MediaBrowserConstants.NOTIFY_CHILDREN_CHANGED_EXTRAS; import static androidx.media3.test.session.common.MediaBrowserConstants.NOTIFY_CHILDREN_CHANGED_ITEM_COUNT; import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID; +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_KEY_ERROR_RESOLUTION_ACTION_LABEL; import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_ERROR; import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_LONG_LIST; import static androidx.media3.test.session.common.MediaBrowserConstants.ROOT_EXTRAS; @@ -47,8 +51,10 @@ import static androidx.media3.test.session.common.MediaBrowserConstants.SUBSCRIB import static androidx.media3.test.session.common.MediaBrowserConstants.SUBSCRIBE_ID_NOTIFY_CHILDREN_CHANGED_TO_ONE_WITH_NON_SUBSCRIBED_ID; import static java.util.concurrent.TimeUnit.MILLISECONDS; +import android.app.PendingIntent; import android.app.Service; import android.content.Context; +import android.content.Intent; import android.os.Bundle; import android.os.HandlerThread; import androidx.annotation.GuardedBy; @@ -232,6 +238,21 @@ public class MockMediaLibraryService extends MediaLibraryService { return Futures.immediateFuture(LibraryResult.ofItemList(list, params)); } else if (PARENT_ID_ERROR.equals(parentId)) { return Futures.immediateFuture(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE)); + } else if (PARENT_ID_AUTH_EXPIRED_ERROR.equals(parentId)) { + Bundle bundle = new Bundle(); + Intent signInIntent = new Intent("action"); + int flags = Util.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0; + bundle.putParcelable( + EXTRAS_KEY_ERROR_RESOLUTION_ACTION_INTENT_COMPAT, + PendingIntent.getActivity( + getApplicationContext(), /* requestCode= */ 0, signInIntent, flags)); + bundle.putString( + EXTRAS_KEY_ERROR_RESOLUTION_ACTION_LABEL_COMPAT, + PARENT_ID_AUTH_EXPIRED_ERROR_KEY_ERROR_RESOLUTION_ACTION_LABEL); + return Futures.immediateFuture( + LibraryResult.ofError( + LibraryResult.RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED, + new LibraryParams.Builder().setExtras(bundle).build())); } // Includes the case of PARENT_ID_NO_CHILDREN. return Futures.immediateFuture(LibraryResult.ofItemList(ImmutableList.of(), params));