From efff1ee2f1f5088ce914d0401a0c08cb65a9b864 Mon Sep 17 00:00:00 2001 From: bachinger Date: Tue, 11 Jun 2024 06:51:13 -0700 Subject: [PATCH] Add SessionError and use it in service results This change adds `SessionError` and uses it in `SessionResult` and `LibraryResult` to report errors to callers. Constructors and factory method that used a simple `errorCode` to construct error variants of `SessionResult` and `LibraryResult` have been overloaded with a variant that uses a `SessionError` instead. While these methods and constructors are supposed to be deprecated, they aren't yet deprecated until the newly added alternative is stabilized. PiperOrigin-RevId: 642254336 --- RELEASENOTES.md | 3 + api.txt | 4 +- .../DemoMediaLibrarySessionCallback.kt | 5 +- .../media3/session/LibraryResult.java | 154 ++++++++++---- .../androidx/media3/session/MediaBrowser.java | 4 +- .../media3/session/MediaBrowserImplBase.java | 12 +- .../session/MediaBrowserImplLegacy.java | 54 ++--- .../media3/session/MediaController.java | 11 +- .../session/MediaControllerImplBase.java | 13 +- .../media3/session/MediaLibraryService.java | 17 +- .../session/MediaLibrarySessionImpl.java | 59 ++++-- .../androidx/media3/session/MediaSession.java | 8 +- .../media3/session/MediaSessionImpl.java | 14 +- .../session/MediaSessionLegacyStub.java | 4 +- .../media3/session/MediaSessionStub.java | 31 ++- .../media3/session/PlayerWrapper.java | 2 +- .../androidx/media3/session/SessionError.java | 194 ++++++++++++++++++ .../media3/session/SessionResult.java | 149 ++++++++++---- .../media3/session/LibraryResultTest.java | 33 ++- .../media3/session/SessionErrorTest.java | 67 ++++++ .../media3/session/SessionResultTest.java | 75 +++++++ .../session/common/MediaBrowserConstants.java | 2 + .../MediaBrowserServiceCompatConstants.java | 2 + ...wserCompatWithMediaLibraryServiceTest.java | 27 ++- ...wserCompatWithMediaSessionServiceTest.java | 6 +- .../session/MediaBrowserListenerTest.java | 27 ++- ...enerWithMediaBrowserServiceCompatTest.java | 20 ++ .../MediaLibrarySessionCallbackTest.java | 15 +- .../session/MediaSessionCallbackTest.java | 8 +- ...CallbackWithMediaControllerCompatTest.java | 4 +- .../MockMediaBrowserServiceCompat.java | 21 ++ .../session/MockMediaLibraryService.java | 33 ++- 32 files changed, 847 insertions(+), 231 deletions(-) create mode 100644 libraries/session/src/main/java/androidx/media3/session/SessionError.java create mode 100644 libraries/session/src/test/java/androidx/media3/session/SessionErrorTest.java create mode 100644 libraries/session/src/test/java/androidx/media3/session/SessionResultTest.java diff --git a/RELEASENOTES.md b/RELEASENOTES.md index e3c47058b1..88a8f7b235 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -21,6 +21,9 @@ * Add `MediaSession.Callback.onPlayerInteractionFinished` to inform sessions when a series of player interactions from a specific controller finished. + * Add `SessionError` and use it in `SessionResult` and `LibraryResult` + instead of the error code to provide more information about the error + and how to resolve the error if possible. * UI: * Add customisation of various icons in `PlayerControlView` through xml attributes to allow different drawables per `PlayerView` instance, diff --git a/api.txt b/api.txt index 2ea1e2eb17..8db8183ab6 100644 --- a/api.txt +++ b/api.txt @@ -1511,7 +1511,7 @@ package androidx.media3.session { field @Nullable public final V value; } - @IntDef({androidx.media3.session.LibraryResult.RESULT_SUCCESS, androidx.media3.session.LibraryResult.RESULT_ERROR_UNKNOWN, androidx.media3.session.LibraryResult.RESULT_ERROR_INVALID_STATE, androidx.media3.session.LibraryResult.RESULT_ERROR_BAD_VALUE, androidx.media3.session.LibraryResult.RESULT_ERROR_PERMISSION_DENIED, androidx.media3.session.LibraryResult.RESULT_ERROR_IO, androidx.media3.session.LibraryResult.RESULT_INFO_SKIPPED, androidx.media3.session.LibraryResult.RESULT_ERROR_SESSION_DISCONNECTED, androidx.media3.session.LibraryResult.RESULT_ERROR_NOT_SUPPORTED, androidx.media3.session.LibraryResult.RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED, androidx.media3.session.LibraryResult.RESULT_ERROR_SESSION_PREMIUM_ACCOUNT_REQUIRED, androidx.media3.session.LibraryResult.RESULT_ERROR_SESSION_CONCURRENT_STREAM_LIMIT, androidx.media3.session.LibraryResult.RESULT_ERROR_SESSION_PARENTAL_CONTROL_RESTRICTED, androidx.media3.session.LibraryResult.RESULT_ERROR_SESSION_NOT_AVAILABLE_IN_REGION, androidx.media3.session.LibraryResult.RESULT_ERROR_SESSION_SKIP_LIMIT_REACHED, androidx.media3.session.LibraryResult.RESULT_ERROR_SESSION_SETUP_REQUIRED}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target(java.lang.annotation.ElementType.TYPE_USE) public static @interface LibraryResult.Code { + @IntDef({androidx.media3.session.LibraryResult.RESULT_SUCCESS, androidx.media3.session.SessionError.INFO_SKIPPED, androidx.media3.session.SessionError.ERROR_UNKNOWN, androidx.media3.session.SessionError.ERROR_INVALID_STATE, androidx.media3.session.SessionError.ERROR_BAD_VALUE, androidx.media3.session.SessionError.ERROR_PERMISSION_DENIED, androidx.media3.session.SessionError.ERROR_IO, androidx.media3.session.SessionError.ERROR_SESSION_DISCONNECTED, androidx.media3.session.SessionError.ERROR_NOT_SUPPORTED, androidx.media3.session.SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED, androidx.media3.session.SessionError.ERROR_SESSION_PREMIUM_ACCOUNT_REQUIRED, androidx.media3.session.SessionError.ERROR_SESSION_CONCURRENT_STREAM_LIMIT, androidx.media3.session.SessionError.ERROR_SESSION_PARENTAL_CONTROL_RESTRICTED, androidx.media3.session.SessionError.ERROR_SESSION_NOT_AVAILABLE_IN_REGION, androidx.media3.session.SessionError.ERROR_SESSION_SKIP_LIMIT_REACHED, androidx.media3.session.SessionError.ERROR_SESSION_SETUP_REQUIRED}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target(java.lang.annotation.ElementType.TYPE_USE) public static @interface LibraryResult.Code { } public final class MediaBrowser extends androidx.media3.session.MediaController { @@ -1866,7 +1866,7 @@ package androidx.media3.session { field @androidx.media3.session.SessionResult.Code public final int resultCode; } - @IntDef({androidx.media3.session.SessionResult.RESULT_SUCCESS, androidx.media3.session.SessionResult.RESULT_ERROR_UNKNOWN, androidx.media3.session.SessionResult.RESULT_ERROR_INVALID_STATE, androidx.media3.session.SessionResult.RESULT_ERROR_BAD_VALUE, androidx.media3.session.SessionResult.RESULT_ERROR_PERMISSION_DENIED, androidx.media3.session.SessionResult.RESULT_ERROR_IO, androidx.media3.session.SessionResult.RESULT_INFO_SKIPPED, androidx.media3.session.SessionResult.RESULT_ERROR_SESSION_DISCONNECTED, androidx.media3.session.SessionResult.RESULT_ERROR_NOT_SUPPORTED, androidx.media3.session.SessionResult.RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED, androidx.media3.session.SessionResult.RESULT_ERROR_SESSION_PREMIUM_ACCOUNT_REQUIRED, androidx.media3.session.SessionResult.RESULT_ERROR_SESSION_CONCURRENT_STREAM_LIMIT, androidx.media3.session.SessionResult.RESULT_ERROR_SESSION_PARENTAL_CONTROL_RESTRICTED, androidx.media3.session.SessionResult.RESULT_ERROR_SESSION_NOT_AVAILABLE_IN_REGION, androidx.media3.session.SessionResult.RESULT_ERROR_SESSION_SKIP_LIMIT_REACHED, androidx.media3.session.SessionResult.RESULT_ERROR_SESSION_SETUP_REQUIRED}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target(java.lang.annotation.ElementType.TYPE_USE) public static @interface SessionResult.Code { + @IntDef({androidx.media3.session.SessionResult.RESULT_SUCCESS, androidx.media3.session.SessionError.INFO_SKIPPED, androidx.media3.session.SessionError.ERROR_UNKNOWN, androidx.media3.session.SessionError.ERROR_INVALID_STATE, androidx.media3.session.SessionError.ERROR_BAD_VALUE, androidx.media3.session.SessionError.ERROR_PERMISSION_DENIED, androidx.media3.session.SessionError.ERROR_IO, androidx.media3.session.SessionError.ERROR_SESSION_DISCONNECTED, androidx.media3.session.SessionError.ERROR_NOT_SUPPORTED, androidx.media3.session.SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED, androidx.media3.session.SessionError.ERROR_SESSION_PREMIUM_ACCOUNT_REQUIRED, androidx.media3.session.SessionError.ERROR_SESSION_CONCURRENT_STREAM_LIMIT, androidx.media3.session.SessionError.ERROR_SESSION_PARENTAL_CONTROL_RESTRICTED, androidx.media3.session.SessionError.ERROR_SESSION_NOT_AVAILABLE_IN_REGION, androidx.media3.session.SessionError.ERROR_SESSION_SKIP_LIMIT_REACHED, androidx.media3.session.SessionError.ERROR_SESSION_SETUP_REQUIRED}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target(java.lang.annotation.ElementType.TYPE_USE) public static @interface SessionResult.Code { } public final class SessionToken { diff --git a/demos/session_service/src/main/java/androidx/media3/demo/session/DemoMediaLibrarySessionCallback.kt b/demos/session_service/src/main/java/androidx/media3/demo/session/DemoMediaLibrarySessionCallback.kt index 9037290dd5..e8561906cc 100644 --- a/demos/session_service/src/main/java/androidx/media3/demo/session/DemoMediaLibrarySessionCallback.kt +++ b/demos/session_service/src/main/java/androidx/media3/demo/session/DemoMediaLibrarySessionCallback.kt @@ -27,6 +27,7 @@ import androidx.media3.session.MediaLibraryService import androidx.media3.session.MediaSession import androidx.media3.session.MediaSession.MediaItemsWithStartPosition import androidx.media3.session.SessionCommand +import androidx.media3.session.SessionError import androidx.media3.session.SessionResult import com.google.common.collect.ImmutableList import com.google.common.util.concurrent.Futures @@ -132,7 +133,7 @@ open class DemoMediaLibrarySessionCallback(context: Context) : MediaItemTree.getItem(mediaId)?.let { return Futures.immediateFuture(LibraryResult.ofItem(it, /* params= */ null)) } - return Futures.immediateFuture(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE)) + return Futures.immediateFuture(LibraryResult.ofError(SessionError.ERROR_BAD_VALUE)) } override fun onGetChildren( @@ -147,7 +148,7 @@ open class DemoMediaLibrarySessionCallback(context: Context) : if (children.isNotEmpty()) { return Futures.immediateFuture(LibraryResult.ofItemList(children, params)) } - return Futures.immediateFuture(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE)) + return Futures.immediateFuture(LibraryResult.ofError(SessionError.ERROR_BAD_VALUE)) } override fun onAddMediaItems( 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 d4ad90736c..bdc7d5b89a 100644 --- a/libraries/session/src/main/java/androidx/media3/session/LibraryResult.java +++ b/libraries/session/src/main/java/androidx/media3/session/LibraryResult.java @@ -54,21 +54,21 @@ public final class LibraryResult implements Bundleable { @Target(TYPE_USE) @IntDef({ RESULT_SUCCESS, - RESULT_ERROR_UNKNOWN, - RESULT_ERROR_INVALID_STATE, - RESULT_ERROR_BAD_VALUE, - RESULT_ERROR_PERMISSION_DENIED, - RESULT_ERROR_IO, - RESULT_INFO_SKIPPED, - RESULT_ERROR_SESSION_DISCONNECTED, - RESULT_ERROR_NOT_SUPPORTED, - RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED, - RESULT_ERROR_SESSION_PREMIUM_ACCOUNT_REQUIRED, - RESULT_ERROR_SESSION_CONCURRENT_STREAM_LIMIT, - RESULT_ERROR_SESSION_PARENTAL_CONTROL_RESTRICTED, - RESULT_ERROR_SESSION_NOT_AVAILABLE_IN_REGION, - RESULT_ERROR_SESSION_SKIP_LIMIT_REACHED, - RESULT_ERROR_SESSION_SETUP_REQUIRED + SessionError.INFO_SKIPPED, + SessionError.ERROR_UNKNOWN, + SessionError.ERROR_INVALID_STATE, + SessionError.ERROR_BAD_VALUE, + SessionError.ERROR_PERMISSION_DENIED, + SessionError.ERROR_IO, + SessionError.ERROR_SESSION_DISCONNECTED, + SessionError.ERROR_NOT_SUPPORTED, + SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED, + SessionError.ERROR_SESSION_PREMIUM_ACCOUNT_REQUIRED, + SessionError.ERROR_SESSION_CONCURRENT_STREAM_LIMIT, + SessionError.ERROR_SESSION_PARENTAL_CONTROL_RESTRICTED, + SessionError.ERROR_SESSION_NOT_AVAILABLE_IN_REGION, + SessionError.ERROR_SESSION_SKIP_LIMIT_REACHED, + SessionError.ERROR_SESSION_SETUP_REQUIRED }) public @interface Code {} @@ -82,56 +82,64 @@ public final class LibraryResult implements Bundleable { */ public static final int RESULT_SUCCESS = 0; + /** Result code representing that the command is skipped. */ + public static final int RESULT_INFO_SKIPPED = SessionError.INFO_SKIPPED; + /** Result code representing that the command is ended with an unknown error. */ - public static final int RESULT_ERROR_UNKNOWN = -1; + public static final int RESULT_ERROR_UNKNOWN = SessionError.ERROR_UNKNOWN; /** * Result code representing that the command cannot be completed because the current state is not * valid for the command. */ - public static final int RESULT_ERROR_INVALID_STATE = -2; + public static final int RESULT_ERROR_INVALID_STATE = SessionError.ERROR_INVALID_STATE; /** Result code representing that an argument is illegal. */ - public static final int RESULT_ERROR_BAD_VALUE = -3; + public static final int RESULT_ERROR_BAD_VALUE = SessionError.ERROR_BAD_VALUE; /** Result code representing that the command is not allowed. */ - public static final int RESULT_ERROR_PERMISSION_DENIED = -4; + public static final int RESULT_ERROR_PERMISSION_DENIED = SessionError.ERROR_PERMISSION_DENIED; /** Result code representing that a file or network related error happened. */ - public static final int RESULT_ERROR_IO = -5; + public static final int RESULT_ERROR_IO = SessionError.ERROR_IO; /** Result code representing that the command is not supported. */ - public static final int RESULT_ERROR_NOT_SUPPORTED = -6; - - /** Result code representing that the command is skipped. */ - public static final int RESULT_INFO_SKIPPED = 1; + public static final int RESULT_ERROR_NOT_SUPPORTED = SessionError.ERROR_NOT_SUPPORTED; /** Result code representing that the session and controller were disconnected. */ - public static final int RESULT_ERROR_SESSION_DISCONNECTED = -100; + public static final int RESULT_ERROR_SESSION_DISCONNECTED = + SessionError.ERROR_SESSION_DISCONNECTED; /** Result code representing that the authentication has expired. */ - public static final int RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED = -102; + public static final int RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED = + SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED; /** Result code representing that a premium account is required. */ - public static final int RESULT_ERROR_SESSION_PREMIUM_ACCOUNT_REQUIRED = -103; + public static final int RESULT_ERROR_SESSION_PREMIUM_ACCOUNT_REQUIRED = + SessionError.ERROR_SESSION_PREMIUM_ACCOUNT_REQUIRED; /** Result code representing that too many concurrent streams are detected. */ - public static final int RESULT_ERROR_SESSION_CONCURRENT_STREAM_LIMIT = -104; + public static final int RESULT_ERROR_SESSION_CONCURRENT_STREAM_LIMIT = + SessionError.ERROR_SESSION_CONCURRENT_STREAM_LIMIT; /** Result code representing that the content is blocked due to parental controls. */ - public static final int RESULT_ERROR_SESSION_PARENTAL_CONTROL_RESTRICTED = -105; + public static final int RESULT_ERROR_SESSION_PARENTAL_CONTROL_RESTRICTED = + SessionError.ERROR_SESSION_PARENTAL_CONTROL_RESTRICTED; /** Result code representing that the content is blocked due to being regionally unavailable. */ - public static final int RESULT_ERROR_SESSION_NOT_AVAILABLE_IN_REGION = -106; + public static final int RESULT_ERROR_SESSION_NOT_AVAILABLE_IN_REGION = + SessionError.ERROR_SESSION_NOT_AVAILABLE_IN_REGION; /** * Result code representing that the application cannot skip any more because the skip limit is * reached. */ - public static final int RESULT_ERROR_SESSION_SKIP_LIMIT_REACHED = -107; + public static final int RESULT_ERROR_SESSION_SKIP_LIMIT_REACHED = + SessionError.ERROR_SESSION_SKIP_LIMIT_REACHED; /** Result code representing that the session needs user's manual intervention. */ - public static final int RESULT_ERROR_SESSION_SETUP_REQUIRED = -108; + public static final int RESULT_ERROR_SESSION_SETUP_REQUIRED = + SessionError.ERROR_SESSION_SETUP_REQUIRED; /** The {@link Code} of this result. */ public final @Code int resultCode; @@ -153,12 +161,16 @@ public final class LibraryResult implements Bundleable { /** The optional parameters. */ @Nullable public final MediaLibraryService.LibraryParams params; + /** The optional session error. */ + @UnstableApi @Nullable public final SessionError sessionError; + /** Creates an instance with {@link #resultCode}{@code ==}{@link #RESULT_SUCCESS}. */ public static LibraryResult ofVoid() { return new LibraryResult<>( RESULT_SUCCESS, SystemClock.elapsedRealtime(), /* params= */ null, + /* sessionError= */ null, /* value= */ null, VALUE_TYPE_VOID); } @@ -169,7 +181,12 @@ public final class LibraryResult implements Bundleable { */ public static LibraryResult ofVoid(@Nullable LibraryParams params) { return new LibraryResult<>( - RESULT_SUCCESS, SystemClock.elapsedRealtime(), params, /* value= */ null, VALUE_TYPE_VOID); + RESULT_SUCCESS, + SystemClock.elapsedRealtime(), + params, + /* sessionError= */ null, + /* value= */ null, + VALUE_TYPE_VOID); } /** @@ -184,7 +201,12 @@ public final class LibraryResult implements Bundleable { public static LibraryResult ofItem(MediaItem item, @Nullable LibraryParams params) { verifyMediaItem(item); return new LibraryResult<>( - RESULT_SUCCESS, SystemClock.elapsedRealtime(), params, item, VALUE_TYPE_ITEM); + RESULT_SUCCESS, + SystemClock.elapsedRealtime(), + params, + /* sessionError= */ null, + item, + VALUE_TYPE_ITEM); } /** @@ -206,6 +228,7 @@ public final class LibraryResult implements Bundleable { RESULT_SUCCESS, SystemClock.elapsedRealtime(), params, + /* sessionError= */ null, ImmutableList.copyOf(items), VALUE_TYPE_ITEM_LIST); } @@ -215,10 +238,13 @@ public final class LibraryResult implements Bundleable { * *

{@code errorCode} must not be {@link #RESULT_SUCCESS}. * + *

Note: This method will be deprecated when {@link #ofError(SessionError)} is promoted to + * stable API status. + * * @param errorCode The error code. */ public static LibraryResult ofError(@Code int errorCode) { - return ofError(errorCode, /* params= */ null); + return ofError(new SessionError(errorCode, SessionError.DEFAULT_ERROR_MESSAGE, Bundle.EMPTY)); } /** @@ -227,15 +253,54 @@ public final class LibraryResult implements Bundleable { * *

{@code errorCode} must not be {@link #RESULT_SUCCESS}. * + *

Note: This method will be deprecated when {@link #ofError(SessionError, LibraryParams)} is + * promoted to stable API status. + * * @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<>( /* resultCode= */ errorCode, SystemClock.elapsedRealtime(), /* params= */ params, + new SessionError(errorCode, SessionError.DEFAULT_ERROR_MESSAGE, Bundle.EMPTY), + /* value= */ null, + VALUE_TYPE_ERROR); + } + + /** + * Creates an instance with a {@link SessionError} to describe the error. The {@link #resultCode} + * is taken from {@link SessionError#code}. + * + * @param sessionError The {@link SessionError}. + */ + @UnstableApi + public static LibraryResult ofError(SessionError sessionError) { + return new LibraryResult<>( + /* resultCode= */ sessionError.code, + SystemClock.elapsedRealtime(), + /* params= */ null, + sessionError, + /* value= */ null, + VALUE_TYPE_ERROR); + } + + /** + * Creates an instance with a {@link SessionError} to describe the error, and the {@linkplain + * LibraryParams parameters sent by the browser}. The {@link #resultCode} is taken from {@link + * SessionError#code}. + * + * @param sessionError The {@link SessionError}. + * @param params The {@link LibraryParams} sent by the browser. + */ + @UnstableApi + public static LibraryResult ofError(SessionError sessionError, LibraryParams params) { + return new LibraryResult<>( + /* resultCode= */ sessionError.code, + SystemClock.elapsedRealtime(), + /* params= */ params, + sessionError, /* value= */ null, VALUE_TYPE_ERROR); } @@ -244,11 +309,13 @@ public final class LibraryResult implements Bundleable { @Code int resultCode, long completionTimeMs, @Nullable LibraryParams params, + @Nullable SessionError sessionError, @Nullable V value, @ValueType int valueType) { this.resultCode = resultCode; this.completionTimeMs = completionTimeMs; this.params = params; + this.sessionError = sessionError; this.value = value; this.valueType = valueType; } @@ -266,6 +333,7 @@ public final class LibraryResult implements Bundleable { private static final String FIELD_PARAMS = Util.intToStringMaxRadix(2); private static final String FIELD_VALUE = Util.intToStringMaxRadix(3); private static final String FIELD_VALUE_TYPE = Util.intToStringMaxRadix(4); + private static final String FIELD_SESSION_ERROR = Util.intToStringMaxRadix(5); // Casting V to ImmutableList is safe if valueType == VALUE_TYPE_ITEM_LIST. @SuppressWarnings("unchecked") @@ -278,6 +346,9 @@ public final class LibraryResult implements Bundleable { if (params != null) { bundle.putBundle(FIELD_PARAMS, params.toBundle()); } + if (sessionError != null) { + bundle.putBundle(FIELD_SESSION_ERROR, sessionError.toBundle()); + } bundle.putInt(FIELD_VALUE_TYPE, valueType); if (value == null) { @@ -391,6 +462,14 @@ public final class LibraryResult implements Bundleable { @Nullable MediaLibraryService.LibraryParams params = paramsBundle == null ? null : LibraryParams.fromBundle(paramsBundle); + @Nullable SessionError sessionError = null; + @Nullable Bundle sessionErrorBundle = bundle.getBundle(FIELD_SESSION_ERROR); + if (sessionErrorBundle != null) { + sessionError = SessionError.fromBundle(sessionErrorBundle); + } else if (resultCode != RESULT_SUCCESS) { + // Result from a session with a library version that doesn't have the SessionError. + sessionError = new SessionError(resultCode, SessionError.DEFAULT_ERROR_MESSAGE); + } @ValueType int valueType = bundle.getInt(FIELD_VALUE_TYPE); @Nullable Object value; switch (valueType) { @@ -416,7 +495,8 @@ public final class LibraryResult implements Bundleable { throw new IllegalStateException(); } - return new LibraryResult<>(resultCode, completionTimeMs, params, value, valueType); + return new LibraryResult<>( + resultCode, completionTimeMs, params, sessionError, value, valueType); } @Documented diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaBrowser.java b/libraries/session/src/main/java/androidx/media3/session/MediaBrowser.java index d039c0e965..b2dc0501f7 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaBrowser.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaBrowser.java @@ -20,7 +20,7 @@ import static androidx.media3.common.util.Assertions.checkNotEmpty; 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.LibraryResult.RESULT_ERROR_SESSION_DISCONNECTED; +import static androidx.media3.session.SessionError.ERROR_SESSION_DISCONNECTED; import android.content.Context; import android.os.Bundle; @@ -422,7 +422,7 @@ public final class MediaBrowser extends MediaController { } private static ListenableFuture> createDisconnectedFuture() { - return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_SESSION_DISCONNECTED)); + return Futures.immediateFuture(LibraryResult.ofError(ERROR_SESSION_DISCONNECTED)); } private void verifyApplicationThread() { diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaBrowserImplBase.java b/libraries/session/src/main/java/androidx/media3/session/MediaBrowserImplBase.java index 347180cc6a..2ef1a32eeb 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaBrowserImplBase.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaBrowserImplBase.java @@ -15,9 +15,6 @@ */ package androidx.media3.session; -import static androidx.media3.session.LibraryResult.RESULT_ERROR_PERMISSION_DENIED; -import static androidx.media3.session.LibraryResult.RESULT_ERROR_SESSION_DISCONNECTED; -import static androidx.media3.session.LibraryResult.RESULT_INFO_SKIPPED; import static androidx.media3.session.SessionCommand.COMMAND_CODE_LIBRARY_GET_CHILDREN; import static androidx.media3.session.SessionCommand.COMMAND_CODE_LIBRARY_GET_ITEM; import static androidx.media3.session.SessionCommand.COMMAND_CODE_LIBRARY_GET_LIBRARY_ROOT; @@ -25,6 +22,9 @@ import static androidx.media3.session.SessionCommand.COMMAND_CODE_LIBRARY_GET_SE import static androidx.media3.session.SessionCommand.COMMAND_CODE_LIBRARY_SEARCH; import static androidx.media3.session.SessionCommand.COMMAND_CODE_LIBRARY_SUBSCRIBE; import static androidx.media3.session.SessionCommand.COMMAND_CODE_LIBRARY_UNSUBSCRIBE; +import static androidx.media3.session.SessionError.ERROR_PERMISSION_DENIED; +import static androidx.media3.session.SessionError.ERROR_SESSION_DISCONNECTED; +import static androidx.media3.session.SessionError.INFO_SKIPPED; import android.content.Context; import android.os.Bundle; @@ -189,20 +189,20 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; IMediaSession iSession = getSessionInterfaceWithSessionCommandIfAble(commandCode); if (iSession != null) { SequencedFuture> result = - sequencedFutureManager.createSequencedFuture(LibraryResult.ofError(RESULT_INFO_SKIPPED)); + sequencedFutureManager.createSequencedFuture(LibraryResult.ofError(INFO_SKIPPED)); try { task.run(iSession, result.getSequenceNumber()); } catch (RemoteException e) { Log.w(TAG, "Cannot connect to the service or the session is gone", e); sequencedFutureManager.setFutureResult( - result.getSequenceNumber(), LibraryResult.ofError(RESULT_ERROR_SESSION_DISCONNECTED)); + result.getSequenceNumber(), LibraryResult.ofError(ERROR_SESSION_DISCONNECTED)); } return result; } else { // Don't create Future with SequencedFutureManager. // Otherwise session would receive discontinued sequence number, and it would make // future work item 'keeping call sequence when session execute commands' impossible. - return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_PERMISSION_DENIED)); + return Futures.immediateFuture(LibraryResult.ofError(ERROR_PERMISSION_DENIED)); } } 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 5cc064a270..28a2bcf981 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaBrowserImplLegacy.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaBrowserImplLegacy.java @@ -15,10 +15,10 @@ */ package androidx.media3.session; -import static androidx.media3.session.LibraryResult.RESULT_ERROR_BAD_VALUE; -import static androidx.media3.session.LibraryResult.RESULT_ERROR_PERMISSION_DENIED; -import static androidx.media3.session.LibraryResult.RESULT_ERROR_SESSION_DISCONNECTED; -import static androidx.media3.session.LibraryResult.RESULT_ERROR_UNKNOWN; +import static androidx.media3.session.SessionError.ERROR_BAD_VALUE; +import static androidx.media3.session.SessionError.ERROR_PERMISSION_DENIED; +import static androidx.media3.session.SessionError.ERROR_SESSION_DISCONNECTED; +import static androidx.media3.session.SessionError.ERROR_UNKNOWN; import android.content.Context; import android.os.Bundle; @@ -92,7 +92,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; public ListenableFuture> getLibraryRoot(@Nullable LibraryParams params) { if (!getInstance() .isSessionCommandAvailable(SessionCommand.COMMAND_CODE_LIBRARY_GET_LIBRARY_ROOT)) { - return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_PERMISSION_DENIED)); + return Futures.immediateFuture(LibraryResult.ofError(ERROR_PERMISSION_DENIED)); } SettableFuture> result = SettableFuture.create(); MediaBrowserCompat browserCompat = getBrowserCompat(params); @@ -117,11 +117,11 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; public ListenableFuture> subscribe( String parentId, @Nullable LibraryParams params) { if (!getInstance().isSessionCommandAvailable(SessionCommand.COMMAND_CODE_LIBRARY_SUBSCRIBE)) { - return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_PERMISSION_DENIED)); + return Futures.immediateFuture(LibraryResult.ofError(ERROR_PERMISSION_DENIED)); } MediaBrowserCompat browserCompat = getBrowserCompat(); if (browserCompat == null) { - return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_SESSION_DISCONNECTED)); + return Futures.immediateFuture(LibraryResult.ofError(ERROR_SESSION_DISCONNECTED)); } SettableFuture> future = SettableFuture.create(); SubscribeCallback callback = new SubscribeCallback(future); @@ -138,17 +138,17 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; @Override public ListenableFuture> unsubscribe(String parentId) { if (!getInstance().isSessionCommandAvailable(SessionCommand.COMMAND_CODE_LIBRARY_UNSUBSCRIBE)) { - return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_PERMISSION_DENIED)); + return Futures.immediateFuture(LibraryResult.ofError(ERROR_PERMISSION_DENIED)); } MediaBrowserCompat browserCompat = getBrowserCompat(); if (browserCompat == null) { - return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_SESSION_DISCONNECTED)); + return Futures.immediateFuture(LibraryResult.ofError(ERROR_SESSION_DISCONNECTED)); } // Note: don't use MediaBrowserCompat#unsubscribe(String) here, to keep the subscription // callback for getChildren. List list = subscribeCallbacks.get(parentId); if (list == null) { - return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_BAD_VALUE)); + return Futures.immediateFuture(LibraryResult.ofError(ERROR_BAD_VALUE)); } for (int i = 0; i < list.size(); i++) { browserCompat.unsubscribe(parentId, list.get(i)); @@ -163,11 +163,11 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; String parentId, int page, int pageSize, @Nullable LibraryParams params) { if (!getInstance() .isSessionCommandAvailable(SessionCommand.COMMAND_CODE_LIBRARY_GET_CHILDREN)) { - return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_PERMISSION_DENIED)); + return Futures.immediateFuture(LibraryResult.ofError(ERROR_PERMISSION_DENIED)); } MediaBrowserCompat browserCompat = getBrowserCompat(); if (browserCompat == null) { - return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_SESSION_DISCONNECTED)); + return Futures.immediateFuture(LibraryResult.ofError(ERROR_SESSION_DISCONNECTED)); } SettableFuture>> future = SettableFuture.create(); @@ -179,11 +179,11 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; @Override public ListenableFuture> getItem(String mediaId) { if (!getInstance().isSessionCommandAvailable(SessionCommand.COMMAND_CODE_LIBRARY_GET_ITEM)) { - return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_PERMISSION_DENIED)); + return Futures.immediateFuture(LibraryResult.ofError(ERROR_PERMISSION_DENIED)); } MediaBrowserCompat browserCompat = getBrowserCompat(); if (browserCompat == null) { - return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_SESSION_DISCONNECTED)); + return Futures.immediateFuture(LibraryResult.ofError(ERROR_SESSION_DISCONNECTED)); } SettableFuture> result = SettableFuture.create(); browserCompat.getItem( @@ -196,13 +196,13 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; LibraryResult.ofItem( LegacyConversions.convertToMediaItem(item), /* params= */ null)); } else { - result.set(LibraryResult.ofError(RESULT_ERROR_BAD_VALUE)); + result.set(LibraryResult.ofError(ERROR_BAD_VALUE)); } } @Override public void onError(String itemId) { - result.set(LibraryResult.ofError(RESULT_ERROR_UNKNOWN)); + result.set(LibraryResult.ofError(ERROR_UNKNOWN)); } }); return result; @@ -212,11 +212,11 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; public ListenableFuture> search( String query, @Nullable LibraryParams params) { if (!getInstance().isSessionCommandAvailable(SessionCommand.COMMAND_CODE_LIBRARY_SEARCH)) { - return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_PERMISSION_DENIED)); + return Futures.immediateFuture(LibraryResult.ofError(ERROR_PERMISSION_DENIED)); } MediaBrowserCompat browserCompat = getBrowserCompat(); if (browserCompat == null) { - return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_SESSION_DISCONNECTED)); + return Futures.immediateFuture(LibraryResult.ofError(ERROR_SESSION_DISCONNECTED)); } browserCompat.search( query, @@ -259,11 +259,11 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; String query, int page, int pageSize, @Nullable LibraryParams params) { if (!getInstance() .isSessionCommandAvailable(SessionCommand.COMMAND_CODE_LIBRARY_GET_SEARCH_RESULT)) { - return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_PERMISSION_DENIED)); + return Futures.immediateFuture(LibraryResult.ofError(ERROR_PERMISSION_DENIED)); } MediaBrowserCompat browserCompat = getBrowserCompat(); if (browserCompat == null) { - return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_SESSION_DISCONNECTED)); + return Futures.immediateFuture(LibraryResult.ofError(ERROR_SESSION_DISCONNECTED)); } SettableFuture>> future = SettableFuture.create(); @@ -285,7 +285,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; @Override public void onError(String query, @Nullable Bundle extrasSent) { - future.set(LibraryResult.ofError(RESULT_ERROR_UNKNOWN)); + future.set(LibraryResult.ofError(ERROR_UNKNOWN)); } }); return future; @@ -339,7 +339,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; MediaBrowserCompat browserCompat = browserCompats.get(params); if (browserCompat == null) { // Shouldn't be happen. Internal error? - result.set(LibraryResult.ofError(RESULT_ERROR_UNKNOWN)); + result.set(LibraryResult.ofError(ERROR_UNKNOWN)); } else { result.set( LibraryResult.ofItem( @@ -356,7 +356,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; @Override public void onConnectionFailed() { // Unknown extra field. - result.set(LibraryResult.ofError(RESULT_ERROR_BAD_VALUE)); + result.set(LibraryResult.ofError(ERROR_BAD_VALUE)); release(); } } @@ -396,7 +396,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; private void onErrorInternal() { // 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(RESULT_ERROR_UNKNOWN)); + future.set(LibraryResult.ofError(ERROR_UNKNOWN)); } private void onChildrenLoadedInternal( @@ -468,7 +468,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; } private void onErrorInternal() { - future.set(LibraryResult.ofError(RESULT_ERROR_UNKNOWN)); + future.set(LibraryResult.ofError(ERROR_UNKNOWN)); } private void onChildrenLoadedInternal( @@ -479,14 +479,14 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; } MediaBrowserCompat browserCompat = getBrowserCompat(); if (browserCompat == null) { - future.set(LibraryResult.ofError(RESULT_ERROR_SESSION_DISCONNECTED)); + future.set(LibraryResult.ofError(ERROR_SESSION_DISCONNECTED)); return; } browserCompat.unsubscribe(this.parentId, GetChildrenCallback.this); if (children == null) { // list are non-Null, so it must be internal error. - future.set(LibraryResult.ofError(RESULT_ERROR_UNKNOWN)); + future.set(LibraryResult.ofError(ERROR_UNKNOWN)); } else { // Don't set extra here, because 'extra' have different meanings between old // API and new API as follows. diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaController.java b/libraries/session/src/main/java/androidx/media3/session/MediaController.java index 08fa5a41ee..fdcd847b97 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaController.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaController.java @@ -21,6 +21,8 @@ import static androidx.media3.common.util.Assertions.checkNotEmpty; 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.SessionError.ERROR_NOT_SUPPORTED; +import static androidx.media3.session.SessionError.ERROR_SESSION_DISCONNECTED; import android.app.PendingIntent; import android.content.Context; @@ -377,7 +379,7 @@ public class MediaController implements Player { */ default ListenableFuture onSetCustomLayout( MediaController controller, List layout) { - return Futures.immediateFuture(new SessionResult(SessionResult.RESULT_ERROR_NOT_SUPPORTED)); + return Futures.immediateFuture(new SessionResult(ERROR_NOT_SUPPORTED)); } /** @@ -416,7 +418,7 @@ public class MediaController implements Player { * Futures#immediateFuture(Object)}. * *

The default implementation returns {@link ListenableFuture} of {@link - * SessionResult#RESULT_ERROR_NOT_SUPPORTED}. + * SessionError#ERROR_NOT_SUPPORTED}. * * @param controller The controller. * @param command The custom command. @@ -425,7 +427,7 @@ public class MediaController implements Player { */ default ListenableFuture onCustomCommand( MediaController controller, SessionCommand command, Bundle args) { - return Futures.immediateFuture(new SessionResult(SessionResult.RESULT_ERROR_NOT_SUPPORTED)); + return Futures.immediateFuture(new SessionResult(SessionError.ERROR_NOT_SUPPORTED)); } /** @@ -2029,8 +2031,7 @@ public class MediaController implements Player { } private static ListenableFuture createDisconnectedFuture() { - return Futures.immediateFuture( - new SessionResult(SessionResult.RESULT_ERROR_SESSION_DISCONNECTED)); + return Futures.immediateFuture(new SessionResult(ERROR_SESSION_DISCONNECTED)); } /* package */ final void runOnApplicationLooper(Runnable runnable) { diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java index e1746fdcef..c489734218 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java @@ -23,6 +23,9 @@ import static androidx.media3.common.util.Assertions.checkStateNotNull; import static androidx.media3.common.util.Util.usToMs; import static androidx.media3.session.MediaUtils.calculateBufferedPercentage; import static androidx.media3.session.MediaUtils.mergePlayerInfo; +import static androidx.media3.session.SessionError.ERROR_PERMISSION_DENIED; +import static androidx.media3.session.SessionError.ERROR_SESSION_DISCONNECTED; +import static androidx.media3.session.SessionError.ERROR_UNKNOWN; import static java.lang.Math.max; import static java.lang.Math.min; @@ -327,8 +330,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; int sequenceNumber = ((SequencedFutureManager.SequencedFuture) future).getSequenceNumber(); pendingMaskingSequencedFutureNumbers.remove(sequenceNumber); - sequencedFutureManager.setFutureResult( - sequenceNumber, new SessionResult(SessionResult.RESULT_ERROR_UNKNOWN)); + sequencedFutureManager.setFutureResult(sequenceNumber, new SessionResult(ERROR_UNKNOWN)); } Log.w(TAG, "Synchronous command takes too long on the session side.", e); // TODO(b/188888693): Let developers know the failure in their code. @@ -377,15 +379,14 @@ import org.checkerframework.checker.nullness.qual.NonNull; Log.w(TAG, "Cannot connect to the service or the session is gone", e); pendingMaskingSequencedFutureNumbers.remove(sequenceNumber); sequencedFutureManager.setFutureResult( - sequenceNumber, new SessionResult(SessionResult.RESULT_ERROR_SESSION_DISCONNECTED)); + sequenceNumber, new SessionResult(ERROR_SESSION_DISCONNECTED)); } return result; } else { // Don't create Future with SequencedFutureManager. // Otherwise session would receive discontinued sequence number, and it would make // future work item 'keeping call sequence when session execute commands' impossible. - return Futures.immediateFuture( - new SessionResult(SessionResult.RESULT_ERROR_PERMISSION_DENIED)); + return Futures.immediateFuture(new SessionResult(ERROR_PERMISSION_DENIED)); } } @@ -2662,7 +2663,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; result = new SessionResult(SessionResult.RESULT_INFO_SKIPPED); } catch (ExecutionException | InterruptedException e) { Log.w(TAG, "Session operation failed", e); - result = new SessionResult(SessionResult.RESULT_ERROR_UNKNOWN); + result = new SessionResult(ERROR_UNKNOWN); } sendControllerResult(seq, result); }, diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaLibraryService.java b/libraries/session/src/main/java/androidx/media3/session/MediaLibraryService.java index 20fcbf53c4..49700ed277 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaLibraryService.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaLibraryService.java @@ -18,9 +18,10 @@ package androidx.media3.session; import static androidx.media3.common.util.Assertions.checkArgument; import static androidx.media3.common.util.Assertions.checkNotEmpty; import static androidx.media3.common.util.Assertions.checkNotNull; -import static androidx.media3.session.LibraryResult.RESULT_ERROR_NOT_SUPPORTED; import static androidx.media3.session.LibraryResult.RESULT_SUCCESS; import static androidx.media3.session.LibraryResult.ofVoid; +import static androidx.media3.session.SessionError.ERROR_BAD_VALUE; +import static androidx.media3.session.SessionError.ERROR_NOT_SUPPORTED; import android.app.PendingIntent; import android.content.Context; @@ -158,7 +159,7 @@ public abstract class MediaLibraryService extends MediaSessionService { */ default ListenableFuture> onGetLibraryRoot( MediaLibrarySession session, ControllerInfo browser, @Nullable LibraryParams params) { - return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_NOT_SUPPORTED)); + return Futures.immediateFuture(LibraryResult.ofError(ERROR_NOT_SUPPORTED)); } /** @@ -181,7 +182,7 @@ public abstract class MediaLibraryService extends MediaSessionService { */ default ListenableFuture> onGetItem( MediaLibrarySession session, ControllerInfo browser, String mediaId) { - return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_NOT_SUPPORTED)); + return Futures.immediateFuture(LibraryResult.ofError(ERROR_NOT_SUPPORTED)); } /** @@ -215,7 +216,7 @@ public abstract class MediaLibraryService extends MediaSessionService { @IntRange(from = 0) int page, @IntRange(from = 1) int pageSize, @Nullable LibraryParams params) { - return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_NOT_SUPPORTED)); + return Futures.immediateFuture(LibraryResult.ofError(ERROR_NOT_SUPPORTED)); } /** @@ -271,9 +272,7 @@ public abstract class MediaLibraryService extends MediaSessionService { // Reject subscription if no browsable item for the parent media ID is returned. return Futures.immediateFuture( LibraryResult.ofError( - result.resultCode != RESULT_SUCCESS - ? result.resultCode - : LibraryResult.RESULT_ERROR_BAD_VALUE)); + result.resultCode != RESULT_SUCCESS ? result.resultCode : ERROR_BAD_VALUE)); } if (browser.getControllerVersion() != ControllerInfo.LEGACY_CONTROLLER_VERSION) { // For legacy browsers, android.service.media.MediaBrowserService already calls @@ -343,7 +342,7 @@ public abstract class MediaLibraryService extends MediaSessionService { ControllerInfo browser, String query, @Nullable LibraryParams params) { - return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_NOT_SUPPORTED)); + return Futures.immediateFuture(LibraryResult.ofError(ERROR_NOT_SUPPORTED)); } /** @@ -381,7 +380,7 @@ public abstract class MediaLibraryService extends MediaSessionService { @IntRange(from = 0) int page, @IntRange(from = 1) int pageSize, @Nullable LibraryParams params) { - return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_NOT_SUPPORTED)); + return Futures.immediateFuture(LibraryResult.ofError(ERROR_NOT_SUPPORTED)); } } 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 fc2fd7e4e3..11d670cfa7 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaLibrarySessionImpl.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaLibrarySessionImpl.java @@ -17,11 +17,14 @@ package androidx.media3.session; import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkState; -import static androidx.media3.session.LibraryResult.RESULT_ERROR_NOT_SUPPORTED; -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 static androidx.media3.session.PlayerWrapper.STATUS_CODE_SUCCESS_COMPAT; +import static androidx.media3.session.SessionError.ERROR_INVALID_STATE; +import static androidx.media3.session.SessionError.ERROR_NOT_SUPPORTED; +import static androidx.media3.session.SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED; +import static androidx.media3.session.SessionError.ERROR_UNKNOWN; import static java.lang.Math.max; import static java.lang.Math.min; @@ -122,7 +125,7 @@ import java.util.concurrent.Future; if (params != null && params.isRecent && isSystemUiController(browser)) { // Advertise support for playback resumption, if enabled. return !canResumePlaybackOnStart() - ? Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_NOT_SUPPORTED)) + ? Futures.immediateFuture(LibraryResult.ofError(ERROR_NOT_SUPPORTED)) : Futures.immediateFuture( LibraryResult.ofItem( new MediaItem.Builder() @@ -156,7 +159,7 @@ import java.util.concurrent.Future; @Nullable LibraryParams params) { if (Objects.equals(parentId, RECENT_LIBRARY_ROOT_MEDIA_ID)) { if (!canResumePlaybackOnStart()) { - return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_NOT_SUPPORTED)); + return Futures.immediateFuture(LibraryResult.ofError(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 @@ -369,25 +372,40 @@ import java.util.concurrent.Future; 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) { + if (setLegacyErrorState(result)) { + // Sync playback state if legacy error state changed. + getSessionCompat().setPlaybackState(playerWrapper.createPlaybackStateCompat()); + } else if (playerWrapper.getLegacyStatusCode() != STATUS_CODE_SUCCESS_COMPAT) { playerWrapper.clearLegacyErrorStatus(); getSessionCompat().setPlaybackState(playerWrapper.createPlaybackStateCompat()); } } + private boolean setLegacyErrorState(LibraryResult result) { + if (result.resultCode == ERROR_SESSION_AUTHENTICATION_EXPIRED + && getPlayerWrapper().getLegacyStatusCode() != ERROR_SESSION_AUTHENTICATION_EXPIRED) { + // Mapping this error to the legacy error state provides backwards compatibility for the + // Automotive OS sign-in. + Bundle bundle = Bundle.EMPTY; + if (result.params != null + && result.params.extras.containsKey(EXTRAS_KEY_ERROR_RESOLUTION_ACTION_INTENT_COMPAT)) { + // Backwards compatibility for Callbacks before SessionError was introduced. + bundle = result.params.extras; + } else if (result.sessionError != null + && result.sessionError.extras.containsKey( + EXTRAS_KEY_ERROR_RESOLUTION_ACTION_INTENT_COMPAT)) { + bundle = result.sessionError.extras; + } + getPlayerWrapper() + .setLegacyErrorStatus( + ERROR_CODE_AUTHENTICATION_EXPIRED_COMPAT, + getContext().getString(R.string.authentication_required), + bundle); + return true; + } + return false; + } + @Nullable private static T tryGetFutureResult(Future future) { checkState(future.isDone()); @@ -436,8 +454,7 @@ import java.util.concurrent.Future; @Override public void onSuccess(MediaSession.MediaItemsWithStartPosition playlist) { if (playlist.mediaItems.isEmpty()) { - settableFuture.set( - LibraryResult.ofError(LibraryResult.RESULT_ERROR_INVALID_STATE, params)); + settableFuture.set(LibraryResult.ofError(ERROR_INVALID_STATE, params)); return; } int sanitizedStartIndex = @@ -449,7 +466,7 @@ import java.util.concurrent.Future; @Override public void onFailure(Throwable t) { - settableFuture.set(LibraryResult.ofError(LibraryResult.RESULT_ERROR_UNKNOWN, params)); + settableFuture.set(LibraryResult.ofError(ERROR_UNKNOWN, params)); Log.e(TAG, "Failed fetching recent media item at boot time: " + t.getMessage(), t); } }, diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java index 95567e0786..ff6e1692d9 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java @@ -19,7 +19,7 @@ import static androidx.annotation.VisibleForTesting.PRIVATE; import static androidx.media3.common.util.Assertions.checkArgument; import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkState; -import static androidx.media3.session.SessionResult.RESULT_ERROR_NOT_SUPPORTED; +import static androidx.media3.session.SessionError.ERROR_NOT_SUPPORTED; import static androidx.media3.session.SessionResult.RESULT_SUCCESS; import android.app.PendingIntent; @@ -1363,7 +1363,7 @@ public class MediaSession { */ default ListenableFuture onSetRating( MediaSession session, ControllerInfo controller, String mediaId, Rating rating) { - return Futures.immediateFuture(new SessionResult(RESULT_ERROR_NOT_SUPPORTED)); + return Futures.immediateFuture(new SessionResult(ERROR_NOT_SUPPORTED)); } /** @@ -1385,7 +1385,7 @@ public class MediaSession { */ default ListenableFuture onSetRating( MediaSession session, ControllerInfo controller, Rating rating) { - return Futures.immediateFuture(new SessionResult(RESULT_ERROR_NOT_SUPPORTED)); + return Futures.immediateFuture(new SessionResult(ERROR_NOT_SUPPORTED)); } /** @@ -1418,7 +1418,7 @@ public class MediaSession { ControllerInfo controller, SessionCommand customCommand, Bundle args) { - return Futures.immediateFuture(new SessionResult(RESULT_ERROR_NOT_SUPPORTED)); + return Futures.immediateFuture(new SessionResult(ERROR_NOT_SUPPORTED)); } /** diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java index df170e4d01..422834dd11 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java @@ -31,9 +31,9 @@ import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkStateNotNull; import static androidx.media3.common.util.Util.postOrRun; import static androidx.media3.session.MediaSessionStub.UNKNOWN_SEQUENCE_NUMBER; -import static androidx.media3.session.SessionResult.RESULT_ERROR_SESSION_DISCONNECTED; -import static androidx.media3.session.SessionResult.RESULT_ERROR_UNKNOWN; -import static androidx.media3.session.SessionResult.RESULT_INFO_SKIPPED; +import static androidx.media3.session.SessionError.ERROR_SESSION_DISCONNECTED; +import static androidx.media3.session.SessionError.ERROR_UNKNOWN; +import static androidx.media3.session.SessionError.INFO_SKIPPED; import android.app.PendingIntent; import android.content.ComponentName; @@ -113,7 +113,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; public static final String TAG = "MediaSessionImpl"; - private static final SessionResult RESULT_WHEN_CLOSED = new SessionResult(RESULT_INFO_SKIPPED); + private static final SessionResult RESULT_WHEN_CLOSED = new SessionResult(INFO_SKIPPED); private final Object lock = new Object(); @@ -1090,7 +1090,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; seq = ((SequencedFuture) future).getSequenceNumber(); } else { if (!isConnected(controller)) { - return Futures.immediateFuture(new SessionResult(RESULT_ERROR_SESSION_DISCONNECTED)); + return Futures.immediateFuture(new SessionResult(ERROR_SESSION_DISCONNECTED)); } // 0 is OK for legacy controllers, because they didn't have sequence numbers. seq = 0; @@ -1104,7 +1104,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; return future; } catch (DeadObjectException e) { onDeadObjectException(controller); - return Futures.immediateFuture(new SessionResult(RESULT_ERROR_SESSION_DISCONNECTED)); + return Futures.immediateFuture(new SessionResult(ERROR_SESSION_DISCONNECTED)); } catch (RemoteException e) { // Currently it's TransactionTooLargeException or DeadSystemException. // We'd better to leave log for those cases because @@ -1113,7 +1113,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; // - DeadSystemException means that errors around it can be ignored. Log.w(TAG, "Exception in " + controller.toString(), e); } - return Futures.immediateFuture(new SessionResult(RESULT_ERROR_UNKNOWN)); + return Futures.immediateFuture(new SessionResult(ERROR_UNKNOWN)); } /** Removes controller. Call this when DeadObjectException is happened with binder call. */ diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java index 093d014a4a..3998a39054 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java @@ -37,7 +37,7 @@ import static androidx.media3.common.util.Util.castNonNull; import static androidx.media3.common.util.Util.postOrRun; import static androidx.media3.session.MediaUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES; import static androidx.media3.session.SessionCommand.COMMAND_CODE_CUSTOM; -import static androidx.media3.session.SessionResult.RESULT_ERROR_UNKNOWN; +import static androidx.media3.session.SessionError.ERROR_UNKNOWN; import static androidx.media3.session.SessionResult.RESULT_INFO_SKIPPED; import static androidx.media3.session.SessionResult.RESULT_SUCCESS; @@ -936,7 +936,7 @@ import org.checkerframework.checker.initialization.qual.Initialized; result = new SessionResult(RESULT_INFO_SKIPPED); } catch (ExecutionException | InterruptedException e) { Log.w(TAG, "Custom command failed", e); - result = new SessionResult(RESULT_ERROR_UNKNOWN); + result = new SessionResult(ERROR_UNKNOWN); } receiver.send(result.resultCode, result.extras); }, diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java index 074757498a..42ac3c4041 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java @@ -53,6 +53,11 @@ import static androidx.media3.session.SessionCommand.COMMAND_CODE_LIBRARY_SEARCH import static androidx.media3.session.SessionCommand.COMMAND_CODE_LIBRARY_SUBSCRIBE; import static androidx.media3.session.SessionCommand.COMMAND_CODE_LIBRARY_UNSUBSCRIBE; import static androidx.media3.session.SessionCommand.COMMAND_CODE_SESSION_SET_RATING; +import static androidx.media3.session.SessionError.ERROR_NOT_SUPPORTED; +import static androidx.media3.session.SessionError.ERROR_PERMISSION_DENIED; +import static androidx.media3.session.SessionError.ERROR_SESSION_DISCONNECTED; +import static androidx.media3.session.SessionError.ERROR_UNKNOWN; +import static androidx.media3.session.SessionError.INFO_SKIPPED; import android.app.PendingIntent; import android.os.Binder; @@ -192,8 +197,8 @@ import java.util.concurrent.ExecutionException; result = new SessionResult( exception.getCause() instanceof UnsupportedOperationException - ? SessionResult.RESULT_ERROR_NOT_SUPPORTED - : SessionResult.RESULT_ERROR_UNKNOWN); + ? ERROR_NOT_SUPPORTED + : ERROR_UNKNOWN); } sendSessionResult(controller, sequenceNumber, result); }); @@ -205,8 +210,7 @@ import java.util.concurrent.ExecutionException; MediaItemPlayerTask mediaItemPlayerTask) { return (sessionImpl, controller, sequenceNumber) -> { if (sessionImpl.isReleased()) { - return Futures.immediateFuture( - new SessionResult(SessionResult.RESULT_ERROR_SESSION_DISCONNECTED)); + return Futures.immediateFuture(new SessionResult(ERROR_SESSION_DISCONNECTED)); } return transformFutureAsync( mediaItemsTask.run(sessionImpl, controller, sequenceNumber), @@ -231,8 +235,7 @@ import java.util.concurrent.ExecutionException; MediaItemsWithStartPositionPlayerTask mediaItemPlayerTask) { return (sessionImpl, controller, sequenceNumber) -> { if (sessionImpl.isReleased()) { - return Futures.immediateFuture( - new SessionResult(SessionResult.RESULT_ERROR_SESSION_DISCONNECTED)); + return Futures.immediateFuture(new SessionResult(ERROR_SESSION_DISCONNECTED)); } return transformFutureAsync( mediaItemsTask.run(sessionImpl, controller, sequenceNumber), @@ -275,10 +278,10 @@ import java.util.concurrent.ExecutionException; result = checkNotNull(future.get(), "LibraryResult must not be null"); } catch (CancellationException e) { Log.w(TAG, "Library operation cancelled", e); - result = LibraryResult.ofError(LibraryResult.RESULT_INFO_SKIPPED); + result = LibraryResult.ofError(INFO_SKIPPED); } catch (ExecutionException | InterruptedException e) { Log.w(TAG, "Library operation failed", e); - result = LibraryResult.ofError(LibraryResult.RESULT_ERROR_UNKNOWN); + result = LibraryResult.ofError(ERROR_UNKNOWN); } sendLibraryResult(controller, sequenceNumber, result); }); @@ -314,9 +317,7 @@ import java.util.concurrent.ExecutionException; () -> { if (!connectedControllersManager.isPlayerCommandAvailable(controller, command)) { sendSessionResult( - controller, - sequenceNumber, - new SessionResult(SessionResult.RESULT_ERROR_PERMISSION_DENIED)); + controller, sequenceNumber, new SessionResult(ERROR_PERMISSION_DENIED)); return; } @SessionResult.Code @@ -393,17 +394,13 @@ import java.util.concurrent.ExecutionException; if (!connectedControllersManager.isSessionCommandAvailable( controller, sessionCommand)) { sendSessionResult( - controller, - sequenceNumber, - new SessionResult(SessionResult.RESULT_ERROR_PERMISSION_DENIED)); + controller, sequenceNumber, new SessionResult(ERROR_PERMISSION_DENIED)); return; } } else { if (!connectedControllersManager.isSessionCommandAvailable(controller, commandCode)) { sendSessionResult( - controller, - sequenceNumber, - new SessionResult(SessionResult.RESULT_ERROR_PERMISSION_DENIED)); + controller, sequenceNumber, new SessionResult(ERROR_PERMISSION_DENIED)); return; } } 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 086a714b42..1cf421b443 100644 --- a/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java +++ b/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java @@ -63,7 +63,7 @@ import java.util.List; */ /* package */ final class PlayerWrapper extends ForwardingPlayer { - private static final int STATUS_CODE_SUCCESS_COMPAT = -1; + /* package */ static final int STATUS_CODE_SUCCESS_COMPAT = -1; private final boolean playIfSuppressed; diff --git a/libraries/session/src/main/java/androidx/media3/session/SessionError.java b/libraries/session/src/main/java/androidx/media3/session/SessionError.java new file mode 100644 index 0000000000..5bca0ac39e --- /dev/null +++ b/libraries/session/src/main/java/androidx/media3/session/SessionError.java @@ -0,0 +1,194 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.session; + +import static java.lang.annotation.ElementType.TYPE_USE; + +import android.os.Bundle; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import androidx.media3.common.PlaybackException; +import androidx.media3.common.util.Assertions; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.common.util.Util; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Objects; + +/** Provides information about a session error. */ +@UnstableApi +public final class SessionError { + + /** + * Info and error result codes. + * + *

    + *
  • Info code: Positive integer + *
  • Error code: Negative integer + *
+ */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @Target(TYPE_USE) + @IntDef({ + INFO_SKIPPED, + ERROR_UNKNOWN, + ERROR_INVALID_STATE, + ERROR_BAD_VALUE, + ERROR_PERMISSION_DENIED, + ERROR_IO, + ERROR_SESSION_DISCONNECTED, + ERROR_NOT_SUPPORTED, + ERROR_SESSION_AUTHENTICATION_EXPIRED, + ERROR_SESSION_PREMIUM_ACCOUNT_REQUIRED, + ERROR_SESSION_CONCURRENT_STREAM_LIMIT, + ERROR_SESSION_PARENTAL_CONTROL_RESTRICTED, + ERROR_SESSION_NOT_AVAILABLE_IN_REGION, + ERROR_SESSION_SKIP_LIMIT_REACHED, + ERROR_SESSION_SETUP_REQUIRED + }) + public @interface Code {} + + /** Info code representing that the command is skipped. */ + public static final int INFO_SKIPPED = 1; + + /** Error code representing that the command is ended with an unknown error. */ + public static final int ERROR_UNKNOWN = -1; + + /** + * Error code representing that the command cannot be completed because the current state is not + * valid for the command. + */ + public static final int ERROR_INVALID_STATE = -2; + + /** Error code representing that an argument is illegal. */ + public static final int ERROR_BAD_VALUE = -3; + + /** Error code representing that the command is not allowed. */ + public static final int ERROR_PERMISSION_DENIED = -4; + + /** Error code representing that a file or network related error happened. */ + public static final int ERROR_IO = -5; + + /** Error code representing that the command is not supported. */ + public static final int ERROR_NOT_SUPPORTED = -6; + + /** Error code representing that the session and controller were disconnected. */ + public static final int ERROR_SESSION_DISCONNECTED = -100; + + /** Error code representing that the authentication has expired. */ + public static final int ERROR_SESSION_AUTHENTICATION_EXPIRED = -102; + + /** Error code representing that a premium account is required. */ + public static final int ERROR_SESSION_PREMIUM_ACCOUNT_REQUIRED = -103; + + /** Error code representing that too many concurrent streams are detected. */ + public static final int ERROR_SESSION_CONCURRENT_STREAM_LIMIT = -104; + + /** Error code representing that the content is blocked due to parental controls. */ + public static final int ERROR_SESSION_PARENTAL_CONTROL_RESTRICTED = -105; + + /** Error code representing that the content is blocked due to being regionally unavailable. */ + public static final int ERROR_SESSION_NOT_AVAILABLE_IN_REGION = -106; + + /** + * Error code representing that the application cannot skip any more because the skip limit is + * reached. + */ + public static final int ERROR_SESSION_SKIP_LIMIT_REACHED = -107; + + /** Error code representing that the session needs user's manual intervention. */ + public static final int ERROR_SESSION_SETUP_REQUIRED = -108; + + /** Default error message. Only used by deprecated methods and for backwards compatibility. */ + public static final String DEFAULT_ERROR_MESSAGE = "no error message provided"; + + public @SessionError.Code int code; + public String message; + public Bundle extras; + + /** + * Creates an instance with {@linkplain Bundle#EMPTY an empty extras bundle}. + * + * @param code The error result code. + * @param message The error message. + * @throws IllegalArgumentException if the result code is not an error result code. + */ + public SessionError(@SessionError.Code int code, String message) { + this(code, message, Bundle.EMPTY); + } + + /** + * Creates an instance. + * + * @param code The error result code. + * @param message The error message. + * @param extras The error extras. + * @throws IllegalArgumentException if the result code is not an error result code. + */ + public SessionError(@SessionError.Code int code, String message, Bundle extras) { + Assertions.checkArgument(code < 0 || code == INFO_SKIPPED); + this.code = code; + this.message = message; + this.extras = extras; + } + + /** Checks the given error for equality while ignoring {@link #extras}. */ + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (!(o instanceof SessionError)) { + return false; + } + SessionError that = (SessionError) o; + return code == that.code && Objects.equals(message, that.message); + } + + @Override + public int hashCode() { + return Objects.hash(code, message); + } + + // Bundleable implementation. + + private static final String FIELD_CODE = Util.intToStringMaxRadix(0); + private static final String FIELD_MESSAGE = Util.intToStringMaxRadix(1); + private static final String FIELD_EXTRAS = Util.intToStringMaxRadix(2); + + /** Returns a {@link Bundle} representing the information stored in this object. */ + public Bundle toBundle() { + Bundle bundle = new Bundle(); + bundle.putInt(FIELD_CODE, code); + bundle.putString(FIELD_MESSAGE, message); + if (!extras.isEmpty()) { + bundle.putBundle(FIELD_EXTRAS, extras); + } + return bundle; + } + + /** Restores a {@code SessionError} from a {@link Bundle}. */ + public static SessionError fromBundle(Bundle bundle) { + int code = + bundle.getInt(FIELD_CODE, /* defaultValue= */ PlaybackException.ERROR_CODE_UNSPECIFIED); + String message = bundle.getString(FIELD_MESSAGE, /* defaultValue= */ ""); + @Nullable Bundle extras = bundle.getBundle(FIELD_EXTRAS); + return new SessionError(code, message, extras == null ? Bundle.EMPTY : extras); + } +} diff --git a/libraries/session/src/main/java/androidx/media3/session/SessionResult.java b/libraries/session/src/main/java/androidx/media3/session/SessionResult.java index 7e239d96d3..0a313bec96 100644 --- a/libraries/session/src/main/java/androidx/media3/session/SessionResult.java +++ b/libraries/session/src/main/java/androidx/media3/session/SessionResult.java @@ -15,6 +15,7 @@ */ package androidx.media3.session; +import static androidx.media3.common.util.Assertions.checkArgument; import static java.lang.annotation.ElementType.TYPE_USE; import android.os.Bundle; @@ -57,21 +58,21 @@ public final class SessionResult implements Bundleable { @Target(TYPE_USE) @IntDef({ RESULT_SUCCESS, - RESULT_ERROR_UNKNOWN, - RESULT_ERROR_INVALID_STATE, - RESULT_ERROR_BAD_VALUE, - RESULT_ERROR_PERMISSION_DENIED, - RESULT_ERROR_IO, - RESULT_INFO_SKIPPED, - RESULT_ERROR_SESSION_DISCONNECTED, - RESULT_ERROR_NOT_SUPPORTED, - RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED, - RESULT_ERROR_SESSION_PREMIUM_ACCOUNT_REQUIRED, - RESULT_ERROR_SESSION_CONCURRENT_STREAM_LIMIT, - RESULT_ERROR_SESSION_PARENTAL_CONTROL_RESTRICTED, - RESULT_ERROR_SESSION_NOT_AVAILABLE_IN_REGION, - RESULT_ERROR_SESSION_SKIP_LIMIT_REACHED, - RESULT_ERROR_SESSION_SETUP_REQUIRED + SessionError.INFO_SKIPPED, + SessionError.ERROR_UNKNOWN, + SessionError.ERROR_INVALID_STATE, + SessionError.ERROR_BAD_VALUE, + SessionError.ERROR_PERMISSION_DENIED, + SessionError.ERROR_IO, + SessionError.ERROR_SESSION_DISCONNECTED, + SessionError.ERROR_NOT_SUPPORTED, + SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED, + SessionError.ERROR_SESSION_PREMIUM_ACCOUNT_REQUIRED, + SessionError.ERROR_SESSION_CONCURRENT_STREAM_LIMIT, + SessionError.ERROR_SESSION_PARENTAL_CONTROL_RESTRICTED, + SessionError.ERROR_SESSION_NOT_AVAILABLE_IN_REGION, + SessionError.ERROR_SESSION_SKIP_LIMIT_REACHED, + SessionError.ERROR_SESSION_SETUP_REQUIRED }) public @interface Code {} @@ -85,56 +86,64 @@ public final class SessionResult implements Bundleable { */ public static final int RESULT_SUCCESS = 0; + /** Result code representing that the command is skipped. */ + public static final int RESULT_INFO_SKIPPED = SessionError.INFO_SKIPPED; + /** Result code representing that the command is ended with an unknown error. */ - public static final int RESULT_ERROR_UNKNOWN = -1; + public static final int RESULT_ERROR_UNKNOWN = SessionError.ERROR_UNKNOWN; /** * Result code representing that the command cannot be completed because the current state is not * valid for the command. */ - public static final int RESULT_ERROR_INVALID_STATE = -2; + public static final int RESULT_ERROR_INVALID_STATE = SessionError.ERROR_INVALID_STATE; /** Result code representing that an argument is illegal. */ - public static final int RESULT_ERROR_BAD_VALUE = -3; + public static final int RESULT_ERROR_BAD_VALUE = SessionError.ERROR_BAD_VALUE; /** Result code representing that the command is not allowed. */ - public static final int RESULT_ERROR_PERMISSION_DENIED = -4; + public static final int RESULT_ERROR_PERMISSION_DENIED = SessionError.ERROR_PERMISSION_DENIED; /** Result code representing that a file or network related error happened. */ - public static final int RESULT_ERROR_IO = -5; + public static final int RESULT_ERROR_IO = SessionError.ERROR_IO; /** Result code representing that the command is not supported. */ - public static final int RESULT_ERROR_NOT_SUPPORTED = -6; - - /** Result code representing that the command is skipped. */ - public static final int RESULT_INFO_SKIPPED = 1; + public static final int RESULT_ERROR_NOT_SUPPORTED = SessionError.ERROR_NOT_SUPPORTED; /** Result code representing that the session and controller were disconnected. */ - public static final int RESULT_ERROR_SESSION_DISCONNECTED = -100; + public static final int RESULT_ERROR_SESSION_DISCONNECTED = + SessionError.ERROR_SESSION_DISCONNECTED; /** Result code representing that the authentication has expired. */ - public static final int RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED = -102; + public static final int RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED = + SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED; /** Result code representing that a premium account is required. */ - public static final int RESULT_ERROR_SESSION_PREMIUM_ACCOUNT_REQUIRED = -103; + public static final int RESULT_ERROR_SESSION_PREMIUM_ACCOUNT_REQUIRED = + SessionError.ERROR_SESSION_PREMIUM_ACCOUNT_REQUIRED; /** Result code representing that too many concurrent streams are detected. */ - public static final int RESULT_ERROR_SESSION_CONCURRENT_STREAM_LIMIT = -104; + public static final int RESULT_ERROR_SESSION_CONCURRENT_STREAM_LIMIT = + SessionError.ERROR_SESSION_CONCURRENT_STREAM_LIMIT; /** Result code representing that the content is blocked due to parental controls. */ - public static final int RESULT_ERROR_SESSION_PARENTAL_CONTROL_RESTRICTED = -105; + public static final int RESULT_ERROR_SESSION_PARENTAL_CONTROL_RESTRICTED = + SessionError.ERROR_SESSION_PARENTAL_CONTROL_RESTRICTED; /** Result code representing that the content is blocked due to being regionally unavailable. */ - public static final int RESULT_ERROR_SESSION_NOT_AVAILABLE_IN_REGION = -106; + public static final int RESULT_ERROR_SESSION_NOT_AVAILABLE_IN_REGION = + SessionError.ERROR_SESSION_NOT_AVAILABLE_IN_REGION; /** * Result code representing that the application cannot skip any more because the skip limit is * reached. */ - public static final int RESULT_ERROR_SESSION_SKIP_LIMIT_REACHED = -107; + public static final int RESULT_ERROR_SESSION_SKIP_LIMIT_REACHED = + SessionError.ERROR_SESSION_SKIP_LIMIT_REACHED; /** Result code representing that the session needs user's manual intervention. */ - public static final int RESULT_ERROR_SESSION_SETUP_REQUIRED = -108; + public static final int RESULT_ERROR_SESSION_SETUP_REQUIRED = + SessionError.ERROR_SESSION_SETUP_REQUIRED; /** The {@link Code} of this result. */ public final @Code int resultCode; @@ -148,9 +157,15 @@ public final class SessionResult implements Bundleable { */ public final long completionTimeMs; + /** The optional session error. */ + @UnstableApi @Nullable public final SessionError sessionError; + /** * Creates an instance with a result code. * + *

Note: Use {@link SessionResult#SessionResult(SessionError)} for errors to provide a + * localized error message for your users. + * * @param resultCode The result code. */ public SessionResult(@Code int resultCode) { @@ -160,17 +175,64 @@ public final class SessionResult implements Bundleable { /** * Creates an instance with a result code and an extra {@link Bundle}. * + *

Note: Use {@link SessionResult#SessionResult(SessionError, Bundle)} for errors to provide a + * localized error message for your users. + * * @param resultCode The result code. * @param extras The extra {@link Bundle}. */ public SessionResult(@Code int resultCode, Bundle extras) { - this(resultCode, extras, SystemClock.elapsedRealtime()); + this( + resultCode, + extras, + /* completionTimeMs= */ SystemClock.elapsedRealtime(), + /* sessionError= */ null); } - private SessionResult(@Code int resultCode, Bundle extras, long completionTimeMs) { + /** + * Creates an instance from a {@link SessionError}. The {@link #resultCode} is taken from {@link + * SessionError#code} and the session result extras {@link Bundle} is empty. + * + * @param sessionError The {@linkplain SessionError session error}. + */ + @UnstableApi + public SessionResult(SessionError sessionError) { + this( + sessionError.code, + Bundle.EMPTY, + /* completionTimeMs= */ SystemClock.elapsedRealtime(), + sessionError); + } + + /** + * Creates an instance from a {@link SessionError} and an extras {@link Bundle}. The {@link + * #resultCode} is taken from the {@link SessionError}. + * + * @param sessionError The {@link SessionError}. + * @param extras The extra {@link Bundle}. + */ + @UnstableApi + public SessionResult(SessionError sessionError, Bundle extras) { + this( + sessionError.code, + extras, + /* completionTimeMs= */ SystemClock.elapsedRealtime(), + sessionError); + } + + private SessionResult( + @Code int resultCode, + Bundle extras, + long completionTimeMs, + @Nullable SessionError sessionError) { + checkArgument(sessionError == null || resultCode < 0); this.resultCode = resultCode; this.extras = new Bundle(extras); this.completionTimeMs = completionTimeMs; + this.sessionError = + sessionError == null && resultCode < 0 + ? new SessionError(resultCode, SessionError.DEFAULT_ERROR_MESSAGE) + : sessionError; } // Bundleable implementation. @@ -178,6 +240,7 @@ public final class SessionResult implements Bundleable { private static final String FIELD_RESULT_CODE = Util.intToStringMaxRadix(0); private static final String FIELD_EXTRAS = Util.intToStringMaxRadix(1); private static final String FIELD_COMPLETION_TIME_MS = Util.intToStringMaxRadix(2); + private static final String FIELD_SESSION_ERROR = Util.intToStringMaxRadix(3); @UnstableApi @Override @@ -186,6 +249,9 @@ public final class SessionResult implements Bundleable { bundle.putInt(FIELD_RESULT_CODE, resultCode); bundle.putBundle(FIELD_EXTRAS, extras); bundle.putLong(FIELD_COMPLETION_TIME_MS, completionTimeMs); + if (sessionError != null) { + bundle.putBundle(FIELD_SESSION_ERROR, sessionError.toBundle()); + } return bundle; } @@ -202,10 +268,21 @@ public final class SessionResult implements Bundleable { /** Restores a {@code SessionResult} from a {@link Bundle}. */ @UnstableApi public static SessionResult fromBundle(Bundle bundle) { - int resultCode = bundle.getInt(FIELD_RESULT_CODE, /* defaultValue= */ RESULT_ERROR_UNKNOWN); + int resultCode = + bundle.getInt(FIELD_RESULT_CODE, /* defaultValue= */ SessionError.ERROR_UNKNOWN); @Nullable Bundle extras = bundle.getBundle(FIELD_EXTRAS); long completionTimeMs = bundle.getLong(FIELD_COMPLETION_TIME_MS, /* defaultValue= */ SystemClock.elapsedRealtime()); - return new SessionResult(resultCode, extras == null ? Bundle.EMPTY : extras, completionTimeMs); + @Nullable SessionError sessionError = null; + @Nullable Bundle sessionErrorBundle = bundle.getBundle(FIELD_SESSION_ERROR); + if (sessionErrorBundle != null) { + sessionError = SessionError.fromBundle(sessionErrorBundle); + } else if (resultCode != RESULT_SUCCESS) { + // Populate the session error if the session is of a library version that doesn't have the + // SessionError yet. + sessionError = new SessionError(resultCode, SessionError.DEFAULT_ERROR_MESSAGE); + } + return new SessionResult( + resultCode, extras == null ? Bundle.EMPTY : extras, completionTimeMs, sessionError); } } diff --git a/libraries/session/src/test/java/androidx/media3/session/LibraryResultTest.java b/libraries/session/src/test/java/androidx/media3/session/LibraryResultTest.java index d95ee08741..50a656d0da 100644 --- a/libraries/session/src/test/java/androidx/media3/session/LibraryResultTest.java +++ b/libraries/session/src/test/java/androidx/media3/session/LibraryResultTest.java @@ -15,7 +15,7 @@ */ package androidx.media3.session; -import static androidx.media3.session.LibraryResult.RESULT_ERROR_NOT_SUPPORTED; +import static androidx.media3.session.SessionError.ERROR_NOT_SUPPORTED; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; @@ -99,8 +99,7 @@ public class LibraryResultTest { @Test public void toBundle_errorResultThatWasUnbundledAsAnUnknownType_noException() { - LibraryResult> libraryResult = - LibraryResult.ofError(LibraryResult.RESULT_ERROR_NOT_SUPPORTED); + LibraryResult> libraryResult = LibraryResult.ofError(ERROR_NOT_SUPPORTED); Bundle errorLibraryResultBundle = libraryResult.toBundle(); LibraryResult libraryResultFromUntyped = LibraryResult.fromUnknownBundle(errorLibraryResultBundle); @@ -109,13 +108,12 @@ public class LibraryResultTest { assertThat(LibraryResult.fromUnknownBundle(bundleOfUntyped).value).isNull(); assertThat(LibraryResult.fromUnknownBundle(bundleOfUntyped).resultCode) - .isEqualTo(RESULT_ERROR_NOT_SUPPORTED); + .isEqualTo(ERROR_NOT_SUPPORTED); } @Test public void toBundle_voidResultThatWasUnbundledAsAnUnknownType_noException() { - LibraryResult> libraryResult = - LibraryResult.ofError(LibraryResult.RESULT_ERROR_NOT_SUPPORTED); + LibraryResult> libraryResult = LibraryResult.ofError(ERROR_NOT_SUPPORTED); Bundle errorLibraryResultBundle = libraryResult.toBundle(); LibraryResult libraryResultFromUntyped = LibraryResult.fromUnknownBundle(errorLibraryResultBundle); @@ -124,6 +122,27 @@ public class LibraryResultTest { assertThat(LibraryResult.fromUnknownBundle(bundleOfUntyped).value).isNull(); assertThat(LibraryResult.fromUnknownBundle(bundleOfUntyped).resultCode) - .isEqualTo(RESULT_ERROR_NOT_SUPPORTED); + .isEqualTo(ERROR_NOT_SUPPORTED); + } + + @Test + public void toBundle_roundTrip_equalsWithOriginal() { + Bundle errorExtras = new Bundle(); + errorExtras.putString("errorKey", "errorValue"); + LibraryResult errorLibraryResult = + LibraryResult.ofError(new SessionError(ERROR_NOT_SUPPORTED, "error message", errorExtras)); + + LibraryResult errorLibraryResultFromBundle = + LibraryResult.fromUnknownBundle(errorLibraryResult.toBundle()); + + assertThat(errorLibraryResultFromBundle.resultCode).isEqualTo(errorLibraryResult.resultCode); + assertThat(errorLibraryResultFromBundle.sessionError) + .isEqualTo(errorLibraryResult.sessionError); + assertThat(errorLibraryResultFromBundle.sessionError.extras.size()).isEqualTo(1); + assertThat(errorLibraryResultFromBundle.sessionError.extras.getString("errorKey")) + .isEqualTo("errorValue"); + assertThat(errorLibraryResultFromBundle.value).isEqualTo(errorLibraryResult.value); + assertThat(errorLibraryResultFromBundle.completionTimeMs) + .isEqualTo(errorLibraryResult.completionTimeMs); } } diff --git a/libraries/session/src/test/java/androidx/media3/session/SessionErrorTest.java b/libraries/session/src/test/java/androidx/media3/session/SessionErrorTest.java new file mode 100644 index 0000000000..c73892a12a --- /dev/null +++ b/libraries/session/src/test/java/androidx/media3/session/SessionErrorTest.java @@ -0,0 +1,67 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.session; + +import static androidx.media3.session.SessionError.ERROR_BAD_VALUE; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import android.os.Bundle; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link SessionError}. */ +@RunWith(AndroidJUnit4.class) +public class SessionErrorTest { + + @Test + public void constructor_twoArguments_usesEmptyBundle() { + SessionError error = new SessionError(ERROR_BAD_VALUE, "error message"); + + assertThat(error.extras.size()).isEqualTo(0); + } + + @Test + public void constructor_withNonErrorCode_throwsIllegalArgumentException() { + assertThrows( + IllegalArgumentException.class, + () -> new SessionError(SessionResult.RESULT_SUCCESS, "error message")); + } + + @Test + public void equals_differentBundles_bundleIgnored() { + Bundle errorBundle1 = new Bundle(); + errorBundle1.putString("key", "value"); + SessionError error1 = new SessionError(ERROR_BAD_VALUE, "error message", errorBundle1); + SessionError error2 = new SessionError(ERROR_BAD_VALUE, "error message"); + + assertThat(error1).isEqualTo(error2); + } + + @Test + public void toBundle_roundTrip_resultsInEqualObjectWithSameBundle() { + Bundle errorBundle = new Bundle(); + errorBundle.putString("key", "value"); + SessionError error = new SessionError(ERROR_BAD_VALUE, "error message", errorBundle); + + SessionError sessionErrorFromBundle = SessionError.fromBundle(error.toBundle()); + + assertThat(sessionErrorFromBundle).isEqualTo(error); + assertThat(sessionErrorFromBundle.extras.size()).isEqualTo(1); + assertThat(sessionErrorFromBundle.extras.getString("key")).isEqualTo("value"); + } +} diff --git a/libraries/session/src/test/java/androidx/media3/session/SessionResultTest.java b/libraries/session/src/test/java/androidx/media3/session/SessionResultTest.java new file mode 100644 index 0000000000..4aa878f365 --- /dev/null +++ b/libraries/session/src/test/java/androidx/media3/session/SessionResultTest.java @@ -0,0 +1,75 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.session; + +import static androidx.media3.session.SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED; +import static androidx.media3.session.SessionError.ERROR_SESSION_CONCURRENT_STREAM_LIMIT; +import static com.google.common.truth.Truth.assertThat; + +import android.os.Bundle; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link SessionResult}. */ +@RunWith(AndroidJUnit4.class) +public class SessionResultTest { + + @Test + public void constructor_errorCodeOnly_createsDefaultSessionError() { + SessionResult sessionResult = new SessionResult(ERROR_SESSION_AUTHENTICATION_EXPIRED); + + assertThat(sessionResult.resultCode).isEqualTo(ERROR_SESSION_AUTHENTICATION_EXPIRED); + assertThat(sessionResult.extras.size()).isEqualTo(0); + assertThat(sessionResult.sessionError.code).isEqualTo(ERROR_SESSION_AUTHENTICATION_EXPIRED); + assertThat(sessionResult.sessionError.message).isEqualTo(SessionError.DEFAULT_ERROR_MESSAGE); + assertThat(sessionResult.sessionError.extras.size()).isEqualTo(0); + } + + @Test + public void constructor_errorCodeAndBundleOnly_createsDefaultSessionError() { + Bundle bundle = new Bundle(); + bundle.putString("key", "value"); + + SessionResult sessionResult = new SessionResult(ERROR_SESSION_CONCURRENT_STREAM_LIMIT, bundle); + + assertThat(sessionResult.resultCode).isEqualTo(ERROR_SESSION_CONCURRENT_STREAM_LIMIT); + assertThat(sessionResult.extras.size()).isEqualTo(1); + assertThat(sessionResult.extras.getString("key")).isEqualTo("value"); + assertThat(sessionResult.sessionError.code).isEqualTo(ERROR_SESSION_CONCURRENT_STREAM_LIMIT); + assertThat(sessionResult.sessionError.message).isEqualTo(SessionError.DEFAULT_ERROR_MESSAGE); + assertThat(sessionResult.sessionError.extras.size()).isEqualTo(0); + } + + @Test + public void toBundle_roundTrip_resultsInEqualObjectWithSameBundle() { + Bundle errorExtras = new Bundle(); + errorExtras.putString("errorKey", "errorValue"); + SessionResult sessionResult = + new SessionResult( + new SessionError(SessionError.ERROR_NOT_SUPPORTED, "error message", errorExtras)); + + SessionResult resultFromBundle = SessionResult.fromBundle(sessionResult.toBundle()); + + assertThat(resultFromBundle.resultCode).isEqualTo(sessionResult.resultCode); + assertThat(resultFromBundle.completionTimeMs).isEqualTo(sessionResult.completionTimeMs); + assertThat(resultFromBundle.sessionError.code).isEqualTo(sessionResult.sessionError.code); + assertThat(resultFromBundle.sessionError.message).isEqualTo(sessionResult.sessionError.message); + assertThat(resultFromBundle.sessionError.extras.size()).isEqualTo(1); + assertThat(resultFromBundle.sessionError.extras.getString("errorKey")).isEqualTo("errorValue"); + assertThat(resultFromBundle.extras.size()).isEqualTo(0); + } +} 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 5ae2024cdc..9628056d4e 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 @@ -38,6 +38,8 @@ public class MediaBrowserConstants { 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_DEPRECATED = + "parent_auth_expired_error_deprecated"; public static final String PARENT_ID_AUTH_EXPIRED_ERROR_KEY_ERROR_RESOLUTION_ACTION_LABEL = "parent_auth_expired_error_label"; 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 31e16aecc8..56c4a6de26 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 @@ -23,6 +23,8 @@ public class MediaBrowserServiceCompatConstants { "testOnChildrenChanged_subscribeAndUnsubscribe"; public static final String TEST_GET_LIBRARY_ROOT = "getLibraryRoot_correctExtraKeyAndValue"; public static final String TEST_GET_CHILDREN = "getChildren_correctMetadataExtras"; + public static final String TEST_GET_CHILDREN_WITH_NULL_LIST = + "onChildrenChanged_withNullChildrenListInLegacyService_convertedToSessionError"; 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 91fa9b843d..52f87090ea 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 @@ -38,6 +38,7 @@ import static androidx.media3.test.session.common.MediaBrowserConstants.MEDIA_ID 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_DEPRECATED; 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; @@ -381,8 +382,20 @@ public class MediaBrowserCompatWithMediaLibraryServiceTest } @Test - public void getChildren_authErrorResult() throws Exception { - String testParentId = PARENT_ID_AUTH_EXPIRED_ERROR; + public void getChildren_authErrorResult_correctPlaybackStateCompatUpdates() throws Exception { + assertGetChildrenAuthenticationRequired(PARENT_ID_AUTH_EXPIRED_ERROR); + } + + @Test + public void getChildren_authErrorResultDeprecated_correctPlaybackStateCompatUpdates() + throws Exception { + // Tests the deprecated approach where apps were expected to pass the error extras back as the + // extras of the LibraryParams of the LibraryResult because the SessionError type didn't then + // exist as part of the LibraryResult. + assertGetChildrenAuthenticationRequired(PARENT_ID_AUTH_EXPIRED_ERROR_DEPRECATED); + } + + public void assertGetChildrenAuthenticationRequired(String testParentId) throws Exception { connectAndWait(/* rootHints= */ Bundle.EMPTY); CountDownLatch errorLatch = new CountDownLatch(1); AtomicReference parentIdRefOnError = new AtomicReference<>(); @@ -399,6 +412,7 @@ public class MediaBrowserCompatWithMediaLibraryServiceTest assertThat(errorLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); assertThat(parentIdRefOnError.get()).isEqualTo(testParentId); + assertThat(firstPlaybackStateCompatReported.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); assertThat(lastReportedPlaybackStateCompat.getState()) .isEqualTo(PlaybackStateCompat.STATE_ERROR); assertThat( @@ -456,12 +470,11 @@ public class MediaBrowserCompatWithMediaLibraryServiceTest } @Test - public void getChildren_nullResult() throws Exception { + public void getChildren_errorLibraryResult() throws Exception { String testParentId = PARENT_ID_ERROR; connectAndWait(/* rootHints= */ Bundle.EMPTY); CountDownLatch latch = new CountDownLatch(1); AtomicReference parentIdRef = new AtomicReference<>(); - AtomicBoolean onChildrenLoadedWithBundleCalled = new AtomicBoolean(); browserCompat.subscribe( testParentId, @@ -471,16 +484,10 @@ public class MediaBrowserCompatWithMediaLibraryServiceTest parentIdRef.set(parentId); latch.countDown(); } - - @Override - public void onChildrenLoaded(String parentId, List children, Bundle options) { - onChildrenLoadedWithBundleCalled.set(true); - } }); assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); assertThat(parentIdRef.get()).isEqualTo(testParentId); - assertThat(onChildrenLoadedWithBundleCalled.get()).isFalse(); } @Test 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 addf1f344d..8bef4ca9f5 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 @@ -58,16 +58,18 @@ public class MediaBrowserCompatWithMediaSessionServiceTest { Context context; TestHandler handler; - MediaBrowserCompat browserCompat; + @Nullable MediaBrowserCompat browserCompat; @Nullable MediaControllerCompat controllerCompat; TestConnectionCallback connectionCallback; @Nullable PlaybackStateCompat lastReportedPlaybackStateCompat; + @Nullable CountDownLatch firstPlaybackStateCompatReported; @Before public void setUp() { context = ApplicationProvider.getApplicationContext(); handler = threadTestRule.getHandler(); connectionCallback = new TestConnectionCallback(); + firstPlaybackStateCompatReported = new CountDownLatch(1); } @After @@ -131,13 +133,13 @@ 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; + firstPlaybackStateCompatReported.countDown(); } }; controllerCompat.registerCallback(controllerCompatCallback); diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserListenerTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserListenerTest.java index ef069a1e6c..caa7cc5eab 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserListenerTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserListenerTest.java @@ -15,11 +15,11 @@ */ package androidx.media3.session; -import static androidx.media3.session.LibraryResult.RESULT_ERROR_BAD_VALUE; import static androidx.media3.session.LibraryResult.RESULT_SUCCESS; import static androidx.media3.session.MediaConstants.EXTRAS_KEY_COMPLETION_STATUS; import static androidx.media3.session.MediaConstants.EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED; import static androidx.media3.session.MockMediaLibraryService.createNotifyChildrenChangedBundle; +import static androidx.media3.session.SessionError.ERROR_BAD_VALUE; import static androidx.media3.test.session.common.CommonConstants.MOCK_MEDIA3_LIBRARY_SERVICE; import static androidx.media3.test.session.common.MediaBrowserConstants.CUSTOM_ACTION_ASSERT_PARAMS; import static androidx.media3.test.session.common.MediaBrowserConstants.LONG_LIST_COUNT; @@ -180,7 +180,7 @@ public class MediaBrowserListenerTest extends MediaControllerListenerTest { .getHandler() .postAndSync(() -> browser.getItem(mediaId)) .get(TIMEOUT_MS, MILLISECONDS); - assertThat(result.resultCode).isEqualTo(RESULT_ERROR_BAD_VALUE); + assertThat(result.resultCode).isEqualTo(ERROR_BAD_VALUE); assertThat(result.value).isNull(); } @@ -246,14 +246,33 @@ public class MediaBrowserListenerTest extends MediaControllerListenerTest { @Test public void getChildren_nullResult() throws Exception { String parentId = MediaBrowserConstants.PARENT_ID_ERROR; - MediaBrowser browser = createBrowser(); + LibraryResult> result = threadTestRule .getHandler() .postAndSync(() -> browser.getChildren(parentId, 1, 1, null)) .get(TIMEOUT_MS, MILLISECONDS); - assertThat(result.resultCode).isNotEqualTo(RESULT_SUCCESS); + + assertThat(result.resultCode).isLessThan(0); + assertThat(result.value).isNull(); + } + + @Test + public void getChildren_errorLibraryResult() throws Exception { + String parentId = MediaBrowserConstants.PARENT_ID_ERROR; + MediaBrowser browser = createBrowser(); + + LibraryResult> result = + threadTestRule + .getHandler() + .postAndSync(() -> browser.getChildren(parentId, 1, 1, null)) + .get(TIMEOUT_MS, MILLISECONDS); + + assertThat(result.resultCode).isLessThan(0); + assertThat(result.sessionError.code).isLessThan(0); + assertThat(result.sessionError.message).isEqualTo("error message"); + assertThat(result.sessionError.extras.getString("key")).isEqualTo("value"); assertThat(result.value).isNull(); } 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 30bd1c83a2..59d7357ca4 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 @@ -27,6 +27,7 @@ import static androidx.media3.test.session.common.MediaBrowserConstants.ROOT_ID; import static androidx.media3.test.session.common.MediaBrowserConstants.ROOT_ID_SUPPORTS_BROWSABLE_CHILDREN_ONLY; import static androidx.media3.test.session.common.MediaBrowserServiceCompatConstants.TEST_CONNECT_REJECTED; import static androidx.media3.test.session.common.MediaBrowserServiceCompatConstants.TEST_GET_CHILDREN; +import static androidx.media3.test.session.common.MediaBrowserServiceCompatConstants.TEST_GET_CHILDREN_WITH_NULL_LIST; import static androidx.media3.test.session.common.MediaBrowserServiceCompatConstants.TEST_GET_LIBRARY_ROOT; import static androidx.media3.test.session.common.MediaBrowserServiceCompatConstants.TEST_ON_CHILDREN_CHANGED_SUBSCRIBE_AND_UNSUBSCRIBE; import static androidx.media3.test.session.common.TestUtils.TIMEOUT_MS; @@ -151,6 +152,25 @@ public class MediaBrowserListenerWithMediaBrowserServiceCompatTest { Thread.sleep(TIMEOUT_MS); } + @Test + public void onChildrenChanged_withNullChildrenListInLegacyService_convertedToSessionError() + throws Exception { + String testParentId = TEST_GET_CHILDREN_WITH_NULL_LIST; + remoteService.setProxyForTest(TEST_GET_CHILDREN_WITH_NULL_LIST); + MediaBrowser browser = createBrowser(/* listener= */ null); + + LibraryResult resultForSubscribe = + threadTestRule + .getHandler() + .postAndSync(() -> browser.subscribe(testParentId, null)) + .get(TIMEOUT_MS, MILLISECONDS); + + assertThat(resultForSubscribe.resultCode).isEqualTo(SessionError.ERROR_UNKNOWN); + assertThat(resultForSubscribe.sessionError.code).isEqualTo(SessionError.ERROR_UNKNOWN); + assertThat(resultForSubscribe.sessionError.message) + .isEqualTo(SessionError.DEFAULT_ERROR_MESSAGE); + } + @Test public void getLibraryRoot_correctExtraKeyAndValue() throws Exception { remoteService.setProxyForTest(TEST_GET_LIBRARY_ROOT); diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaLibrarySessionCallbackTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaLibrarySessionCallbackTest.java index 7361fe5b1e..3ef2e246b9 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaLibrarySessionCallbackTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaLibrarySessionCallbackTest.java @@ -15,8 +15,9 @@ */ package androidx.media3.session; -import static androidx.media3.session.LibraryResult.RESULT_ERROR_NOT_SUPPORTED; -import static androidx.media3.session.LibraryResult.RESULT_ERROR_SESSION_SETUP_REQUIRED; +import static androidx.media3.session.SessionError.ERROR_INVALID_STATE; +import static androidx.media3.session.SessionError.ERROR_NOT_SUPPORTED; +import static androidx.media3.session.SessionError.ERROR_SESSION_SETUP_REQUIRED; import static androidx.media3.test.session.common.MediaBrowserConstants.SUBSCRIBE_PARENT_ID_1; import static androidx.media3.test.session.common.TestUtils.TIMEOUT_MS; import static com.google.common.truth.Truth.assertThat; @@ -166,7 +167,7 @@ public class MediaLibrarySessionCallbackTest { @Nullable LibraryParams params) { latch.countDown(); subscribedControllers.addAll(session.getSubscribedControllers(parentId)); - return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_NOT_SUPPORTED)); + return Futures.immediateFuture(LibraryResult.ofError(ERROR_NOT_SUPPORTED)); } }; MockMediaLibraryService service = new MockMediaLibraryService(); @@ -215,7 +216,7 @@ public class MediaLibrarySessionCallbackTest { int resultCode = browser.subscribe(testParentId, testParams).resultCode; assertThat(session.getSubscribedControllers(testParentId)).isEmpty(); - assertThat(resultCode).isEqualTo(RESULT_ERROR_NOT_SUPPORTED); + assertThat(resultCode).isEqualTo(ERROR_NOT_SUPPORTED); assertThat(session.getSubscribedControllers(testParentId)).isEmpty(); } @@ -234,7 +235,7 @@ public class MediaLibrarySessionCallbackTest { public ListenableFuture> onGetItem( MediaLibrarySession session, ControllerInfo browser, String mediaId) { return Futures.immediateFuture( - LibraryResult.ofError(RESULT_ERROR_SESSION_SETUP_REQUIRED)); + LibraryResult.ofError(ERROR_SESSION_SETUP_REQUIRED)); } }) .setId("testOnSubscribe") @@ -244,7 +245,7 @@ public class MediaLibrarySessionCallbackTest { int resultCode = browser.subscribe(SUBSCRIBE_PARENT_ID_1, testParams).resultCode; - assertThat(resultCode).isEqualTo(RESULT_ERROR_SESSION_SETUP_REQUIRED); + assertThat(resultCode).isEqualTo(ERROR_SESSION_SETUP_REQUIRED); assertThat(session.getSubscribedControllers(SUBSCRIBE_PARENT_ID_1)).isEmpty(); } @@ -413,7 +414,7 @@ public class MediaLibrarySessionCallbackTest { /* params= */ null); assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(recentItem.resultCode).isEqualTo(LibraryResult.RESULT_ERROR_INVALID_STATE); + assertThat(recentItem.resultCode).isEqualTo(ERROR_INVALID_STATE); } @Test diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackTest.java index 723d656974..bb30dfccec 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackTest.java @@ -17,8 +17,8 @@ package androidx.media3.session; import static androidx.media3.common.Player.COMMAND_PLAY_PAUSE; import static androidx.media3.session.MediaTestUtils.createMediaItem; -import static androidx.media3.session.SessionResult.RESULT_ERROR_INVALID_STATE; -import static androidx.media3.session.SessionResult.RESULT_ERROR_PERMISSION_DENIED; +import static androidx.media3.session.SessionError.ERROR_INVALID_STATE; +import static androidx.media3.session.SessionError.ERROR_PERMISSION_DENIED; import static androidx.media3.session.SessionResult.RESULT_INFO_SKIPPED; import static androidx.media3.session.SessionResult.RESULT_SUCCESS; import static androidx.media3.test.session.common.CommonConstants.METADATA_MEDIA_URI; @@ -203,7 +203,7 @@ public class MediaSessionCallbackTest { assertThat(layout).containsExactly(button1Disabled, button2).inOrder(); assertThat(remoteController.sendCustomCommand(button1.sessionCommand, Bundle.EMPTY).resultCode) - .isEqualTo(RESULT_ERROR_PERMISSION_DENIED); + .isEqualTo(ERROR_PERMISSION_DENIED); assertThat(remoteController.sendCustomCommand(button2.sessionCommand, Bundle.EMPTY).resultCode) .isEqualTo(RESULT_SUCCESS); } @@ -369,7 +369,7 @@ public class MediaSessionCallbackTest { assertThat(controllerInfo.isTrusted()).isFalse(); commands.add(command); if (command == Player.COMMAND_PREPARE) { - return RESULT_ERROR_INVALID_STATE; + return ERROR_INVALID_STATE; } return RESULT_SUCCESS; } diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackWithMediaControllerCompatTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackWithMediaControllerCompatTest.java index 2e7704b339..4a8a29de90 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackWithMediaControllerCompatTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackWithMediaControllerCompatTest.java @@ -23,7 +23,7 @@ import static androidx.media3.common.Player.STATE_ENDED; import static androidx.media3.common.Player.STATE_IDLE; import static androidx.media3.common.Player.STATE_READY; import static androidx.media3.common.util.Assertions.checkNotNull; -import static androidx.media3.session.SessionResult.RESULT_ERROR_INVALID_STATE; +import static androidx.media3.session.SessionError.ERROR_INVALID_STATE; import static androidx.media3.session.SessionResult.RESULT_SUCCESS; import static androidx.media3.test.session.common.CommonConstants.SUPPORT_APP_PACKAGE_NAME; import static androidx.media3.test.session.common.TestUtils.LONG_TIMEOUT_MS; @@ -1883,7 +1883,7 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { commands.add(command); if (command == COMMAND_PLAY_PAUSE) { latchForPause.countDown(); - return RESULT_ERROR_INVALID_STATE; + return ERROR_INVALID_STATE; } return RESULT_SUCCESS; } 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 2df04106fe..b188a97046 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 @@ -25,6 +25,7 @@ import static androidx.media3.test.session.common.MediaBrowserConstants.ROOT_ID; import static androidx.media3.test.session.common.MediaBrowserConstants.ROOT_ID_SUPPORTS_BROWSABLE_CHILDREN_ONLY; import static androidx.media3.test.session.common.MediaBrowserServiceCompatConstants.TEST_CONNECT_REJECTED; import static androidx.media3.test.session.common.MediaBrowserServiceCompatConstants.TEST_GET_CHILDREN; +import static androidx.media3.test.session.common.MediaBrowserServiceCompatConstants.TEST_GET_CHILDREN_WITH_NULL_LIST; import static androidx.media3.test.session.common.MediaBrowserServiceCompatConstants.TEST_GET_LIBRARY_ROOT; import static androidx.media3.test.session.common.MediaBrowserServiceCompatConstants.TEST_ON_CHILDREN_CHANGED_SUBSCRIBE_AND_UNSUBSCRIBE; @@ -250,6 +251,9 @@ public class MockMediaBrowserServiceCompat extends MediaBrowserServiceCompat { case TEST_GET_CHILDREN: setProxyForTestGetChildren_correctMetadataExtras(); break; + case TEST_GET_CHILDREN_WITH_NULL_LIST: + setProxyForTestOnChildrenChanged_withNullChildrenListInLegacyService_convertedToSessionError(); + break; default: throw new IllegalArgumentException("Unknown testName: " + testName); } @@ -298,6 +302,23 @@ public class MockMediaBrowserServiceCompat extends MediaBrowserServiceCompat { }); } + private void + setProxyForTestOnChildrenChanged_withNullChildrenListInLegacyService_convertedToSessionError() { + setMediaBrowserServiceProxy( + new MockMediaBrowserServiceCompat.Proxy() { + @Override + public void onLoadChildren(String parentId, Result> result) { + onLoadChildren(parentId, result, new Bundle()); + } + + @Override + public void onLoadChildren( + String parentId, Result> result, Bundle bundle) { + result.sendResult(null); + } + }); + } + private void setProxyForTestGetLibraryRoot_correctExtraKeyAndValue() { setMediaBrowserServiceProxy( new MockMediaBrowserServiceCompat.Proxy() { 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 dc72a77ccc..ab0547476b 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,12 +16,13 @@ package androidx.media3.session; import static androidx.media3.common.util.Assertions.checkNotNull; -import static androidx.media3.session.LibraryResult.RESULT_ERROR_BAD_VALUE; import static androidx.media3.session.MediaConstants.EXTRAS_KEY_COMPLETION_STATUS; 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.MediaConstants.EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED; import static androidx.media3.session.MediaConstants.EXTRA_KEY_ROOT_CHILDREN_BROWSABLE_ONLY; +import static androidx.media3.session.SessionError.ERROR_BAD_VALUE; +import static androidx.media3.session.SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED; import static androidx.media3.test.session.common.CommonConstants.SUPPORT_APP_PACKAGE_NAME; import static androidx.media3.test.session.common.MediaBrowserConstants.CUSTOM_ACTION; import static androidx.media3.test.session.common.MediaBrowserConstants.CUSTOM_ACTION_ASSERT_PARAMS; @@ -37,6 +38,7 @@ import static androidx.media3.test.session.common.MediaBrowserConstants.MEDIA_ID 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_DEPRECATED; 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; @@ -319,7 +321,7 @@ public class MockMediaLibraryService extends MediaLibraryService { LibraryResult.ofItem(createMediaItemWithMetadata(mediaId), /* params= */ null)); default: // fall out } - return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_BAD_VALUE)); + return Futures.immediateFuture(LibraryResult.ofError(ERROR_BAD_VALUE)); } @Override @@ -345,8 +347,12 @@ public class MockMediaLibraryService extends MediaLibraryService { } return Futures.immediateFuture(LibraryResult.ofItemList(list, params)); } else if (Objects.equals(parentId, PARENT_ID_ERROR)) { - return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_BAD_VALUE)); - } else if (Objects.equals(parentId, PARENT_ID_AUTH_EXPIRED_ERROR)) { + Bundle errorBundle = new Bundle(); + errorBundle.putString("key", "value"); + return Futures.immediateFuture( + LibraryResult.ofError(new SessionError(ERROR_BAD_VALUE, "error message", errorBundle))); + } else if (Objects.equals(parentId, PARENT_ID_AUTH_EXPIRED_ERROR) + || Objects.equals(parentId, PARENT_ID_AUTH_EXPIRED_ERROR_DEPRECATED)) { Bundle bundle = new Bundle(); Intent signInIntent = new Intent("action"); int flags = Util.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0; @@ -357,12 +363,17 @@ public class MockMediaLibraryService extends MediaLibraryService { 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())); + return Objects.equals(parentId, PARENT_ID_AUTH_EXPIRED_ERROR) + ? Futures.immediateFuture( + LibraryResult.ofError( + new SessionError(ERROR_SESSION_AUTHENTICATION_EXPIRED, "error message", bundle), + new LibraryParams.Builder().build())) + : Futures.immediateFuture( + LibraryResult.ofError( + ERROR_SESSION_AUTHENTICATION_EXPIRED, + new LibraryParams.Builder().setExtras(bundle).build())); } - return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_BAD_VALUE, params)); + return Futures.immediateFuture(LibraryResult.ofError(ERROR_BAD_VALUE, params)); } @Override @@ -451,7 +462,7 @@ public class MockMediaLibraryService extends MediaLibraryService { return Futures.immediateFuture(LibraryResult.ofItemList(ImmutableList.of(), params)); } else { // SEARCH_QUERY_ERROR will be handled here. - return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_BAD_VALUE)); + return Futures.immediateFuture(LibraryResult.ofError(ERROR_BAD_VALUE)); } } @@ -474,7 +485,7 @@ public class MockMediaLibraryService extends MediaLibraryService { return Futures.immediateFuture(new SessionResult(SessionResult.RESULT_SUCCESS)); default: // fall out } - return Futures.immediateFuture(new SessionResult(SessionResult.RESULT_ERROR_BAD_VALUE)); + return Futures.immediateFuture(new SessionResult(ERROR_BAD_VALUE)); } private void assertLibraryParams(@Nullable LibraryParams params) {