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));