diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 2039c5af59..bfbbe540f6 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -38,6 +38,13 @@ * Propagate extras passed to media3's `MediaSession[Builder].setSessionExtras()` to a media1 controller's `PlaybackStateCompat.getExtras()`. + * Map fatal and non-fatal errors to and from the platform session. A + `PlaybackException` is mapped to a fatal error state of the + `PlaybackStateCompat`. A `SessionError` sent to the media notification + controller with `MediaSession.sendError(ControllerInfo, SessionError)` + is mapped to a non-fatal error in `PlaybackStateCompat` which means that + error code and message are set but the state of the plaftorm session + remains different to `STATE_ERROR`. * 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 f239bc5f02..4c03f14089 100644 --- a/api.txt +++ b/api.txt @@ -624,12 +624,17 @@ package androidx.media3.common { field public static final int ERROR_CODE_AUDIO_TRACK_OFFLOAD_INIT_FAILED = 5004; // 0x138c field public static final int ERROR_CODE_AUDIO_TRACK_OFFLOAD_WRITE_FAILED = 5003; // 0x138b field public static final int ERROR_CODE_AUDIO_TRACK_WRITE_FAILED = 5002; // 0x138a + field public static final int ERROR_CODE_AUTHENTICATION_EXPIRED = -102; // 0xffffff9a + field public static final int ERROR_CODE_BAD_VALUE = -3; // 0xfffffffd field public static final int ERROR_CODE_BEHIND_LIVE_WINDOW = 1002; // 0x3ea + field public static final int ERROR_CODE_CONCURRENT_STREAM_LIMIT = -104; // 0xffffff98 + field public static final int ERROR_CODE_CONTENT_ALREADY_PLAYING = -110; // 0xffffff92 field public static final int ERROR_CODE_DECODER_INIT_FAILED = 4001; // 0xfa1 field public static final int ERROR_CODE_DECODER_QUERY_FAILED = 4002; // 0xfa2 field public static final int ERROR_CODE_DECODING_FAILED = 4003; // 0xfa3 field public static final int ERROR_CODE_DECODING_FORMAT_EXCEEDS_CAPABILITIES = 4004; // 0xfa4 field public static final int ERROR_CODE_DECODING_FORMAT_UNSUPPORTED = 4005; // 0xfa5 + field public static final int ERROR_CODE_DISCONNECTED = -100; // 0xffffff9c field public static final int ERROR_CODE_DRM_CONTENT_ERROR = 6003; // 0x1773 field public static final int ERROR_CODE_DRM_DEVICE_REVOKED = 6007; // 0x1777 field public static final int ERROR_CODE_DRM_DISALLOWED_OPERATION = 6005; // 0x1775 @@ -639,7 +644,9 @@ package androidx.media3.common { field public static final int ERROR_CODE_DRM_SCHEME_UNSUPPORTED = 6001; // 0x1771 field public static final int ERROR_CODE_DRM_SYSTEM_ERROR = 6006; // 0x1776 field public static final int ERROR_CODE_DRM_UNSPECIFIED = 6000; // 0x1770 + field public static final int ERROR_CODE_END_OF_PLAYLIST = -109; // 0xffffff93 field public static final int ERROR_CODE_FAILED_RUNTIME_CHECK = 1004; // 0x3ec + field public static final int ERROR_CODE_INVALID_STATE = -2; // 0xfffffffe field public static final int ERROR_CODE_IO_BAD_HTTP_STATUS = 2004; // 0x7d4 field public static final int ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED = 2007; // 0x7d7 field public static final int ERROR_CODE_IO_FILE_NOT_FOUND = 2005; // 0x7d5 @@ -649,18 +656,25 @@ package androidx.media3.common { field public static final int ERROR_CODE_IO_NO_PERMISSION = 2006; // 0x7d6 field public static final int ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE = 2008; // 0x7d8 field public static final int ERROR_CODE_IO_UNSPECIFIED = 2000; // 0x7d0 + field public static final int ERROR_CODE_NOT_AVAILABLE_IN_REGION = -106; // 0xffffff96 + field public static final int ERROR_CODE_NOT_SUPPORTED = -6; // 0xfffffffa + field public static final int ERROR_CODE_PARENTAL_CONTROL_RESTRICTED = -105; // 0xffffff97 field public static final int ERROR_CODE_PARSING_CONTAINER_MALFORMED = 3001; // 0xbb9 field public static final int ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED = 3003; // 0xbbb field public static final int ERROR_CODE_PARSING_MANIFEST_MALFORMED = 3002; // 0xbba field public static final int ERROR_CODE_PARSING_MANIFEST_UNSUPPORTED = 3004; // 0xbbc + field public static final int ERROR_CODE_PERMISSION_DENIED = -4; // 0xfffffffc + field public static final int ERROR_CODE_PREMIUM_ACCOUNT_REQUIRED = -103; // 0xffffff99 field public static final int ERROR_CODE_REMOTE_ERROR = 1001; // 0x3e9 + field public static final int ERROR_CODE_SETUP_REQUIRED = -108; // 0xffffff94 + field public static final int ERROR_CODE_SKIP_LIMIT_REACHED = -107; // 0xffffff95 field public static final int ERROR_CODE_TIMEOUT = 1003; // 0x3eb field public static final int ERROR_CODE_UNSPECIFIED = 1000; // 0x3e8 field @androidx.media3.common.PlaybackException.ErrorCode public final int errorCode; field public final long timestampMs; } - @IntDef(open=true, value={androidx.media3.common.PlaybackException.ERROR_CODE_UNSPECIFIED, androidx.media3.common.PlaybackException.ERROR_CODE_REMOTE_ERROR, androidx.media3.common.PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW, androidx.media3.common.PlaybackException.ERROR_CODE_TIMEOUT, androidx.media3.common.PlaybackException.ERROR_CODE_FAILED_RUNTIME_CHECK, androidx.media3.common.PlaybackException.ERROR_CODE_IO_UNSPECIFIED, androidx.media3.common.PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT, androidx.media3.common.PlaybackException.ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE, androidx.media3.common.PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS, androidx.media3.common.PlaybackException.ERROR_CODE_IO_FILE_NOT_FOUND, androidx.media3.common.PlaybackException.ERROR_CODE_IO_NO_PERMISSION, androidx.media3.common.PlaybackException.ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED, androidx.media3.common.PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE, androidx.media3.common.PlaybackException.ERROR_CODE_PARSING_CONTAINER_MALFORMED, androidx.media3.common.PlaybackException.ERROR_CODE_PARSING_MANIFEST_MALFORMED, androidx.media3.common.PlaybackException.ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED, androidx.media3.common.PlaybackException.ERROR_CODE_PARSING_MANIFEST_UNSUPPORTED, androidx.media3.common.PlaybackException.ERROR_CODE_DECODER_INIT_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DECODER_QUERY_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DECODING_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DECODING_FORMAT_EXCEEDS_CAPABILITIES, androidx.media3.common.PlaybackException.ERROR_CODE_DECODING_FORMAT_UNSUPPORTED, androidx.media3.common.PlaybackException.ERROR_CODE_AUDIO_TRACK_INIT_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_AUDIO_TRACK_WRITE_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_AUDIO_TRACK_OFFLOAD_INIT_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_AUDIO_TRACK_OFFLOAD_WRITE_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_UNSPECIFIED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_SCHEME_UNSUPPORTED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_PROVISIONING_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_CONTENT_ERROR, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_LICENSE_ACQUISITION_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_DISALLOWED_OPERATION, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_SYSTEM_ERROR, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_DEVICE_REVOKED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_LICENSE_EXPIRED}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.TYPE_USE}) public static @interface PlaybackException.ErrorCode { + @IntDef(open=true, value={androidx.media3.common.PlaybackException.ERROR_CODE_INVALID_STATE, androidx.media3.common.PlaybackException.ERROR_CODE_BAD_VALUE, androidx.media3.common.PlaybackException.ERROR_CODE_PERMISSION_DENIED, androidx.media3.common.PlaybackException.ERROR_CODE_NOT_SUPPORTED, androidx.media3.common.PlaybackException.ERROR_CODE_DISCONNECTED, androidx.media3.common.PlaybackException.ERROR_CODE_AUTHENTICATION_EXPIRED, androidx.media3.common.PlaybackException.ERROR_CODE_PREMIUM_ACCOUNT_REQUIRED, androidx.media3.common.PlaybackException.ERROR_CODE_CONCURRENT_STREAM_LIMIT, androidx.media3.common.PlaybackException.ERROR_CODE_PARENTAL_CONTROL_RESTRICTED, androidx.media3.common.PlaybackException.ERROR_CODE_NOT_AVAILABLE_IN_REGION, androidx.media3.common.PlaybackException.ERROR_CODE_SKIP_LIMIT_REACHED, androidx.media3.common.PlaybackException.ERROR_CODE_SETUP_REQUIRED, androidx.media3.common.PlaybackException.ERROR_CODE_END_OF_PLAYLIST, androidx.media3.common.PlaybackException.ERROR_CODE_CONTENT_ALREADY_PLAYING, androidx.media3.common.PlaybackException.ERROR_CODE_UNSPECIFIED, androidx.media3.common.PlaybackException.ERROR_CODE_REMOTE_ERROR, androidx.media3.common.PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW, androidx.media3.common.PlaybackException.ERROR_CODE_TIMEOUT, androidx.media3.common.PlaybackException.ERROR_CODE_FAILED_RUNTIME_CHECK, androidx.media3.common.PlaybackException.ERROR_CODE_IO_UNSPECIFIED, androidx.media3.common.PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT, androidx.media3.common.PlaybackException.ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE, androidx.media3.common.PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS, androidx.media3.common.PlaybackException.ERROR_CODE_IO_FILE_NOT_FOUND, androidx.media3.common.PlaybackException.ERROR_CODE_IO_NO_PERMISSION, androidx.media3.common.PlaybackException.ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED, androidx.media3.common.PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE, androidx.media3.common.PlaybackException.ERROR_CODE_PARSING_CONTAINER_MALFORMED, androidx.media3.common.PlaybackException.ERROR_CODE_PARSING_MANIFEST_MALFORMED, androidx.media3.common.PlaybackException.ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED, androidx.media3.common.PlaybackException.ERROR_CODE_PARSING_MANIFEST_UNSUPPORTED, androidx.media3.common.PlaybackException.ERROR_CODE_DECODER_INIT_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DECODER_QUERY_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DECODING_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DECODING_FORMAT_EXCEEDS_CAPABILITIES, androidx.media3.common.PlaybackException.ERROR_CODE_DECODING_FORMAT_UNSUPPORTED, androidx.media3.common.PlaybackException.ERROR_CODE_AUDIO_TRACK_INIT_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_AUDIO_TRACK_WRITE_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_AUDIO_TRACK_OFFLOAD_INIT_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_AUDIO_TRACK_OFFLOAD_WRITE_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_UNSPECIFIED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_SCHEME_UNSUPPORTED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_PROVISIONING_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_CONTENT_ERROR, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_LICENSE_ACQUISITION_FAILED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_DISALLOWED_OPERATION, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_SYSTEM_ERROR, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_DEVICE_REVOKED, androidx.media3.common.PlaybackException.ERROR_CODE_DRM_LICENSE_EXPIRED}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.TYPE_USE}) public static @interface PlaybackException.ErrorCode { } public final class PlaybackParameters { @@ -1511,7 +1525,7 @@ package androidx.media3.session { field @Nullable public final V value; } - @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 { + @IntDef({androidx.media3.session.LibraryResult.RESULT_SUCCESS, androidx.media3.session.SessionError.INFO_CANCELLED, 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 +1880,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.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 { + @IntDef({androidx.media3.session.SessionResult.RESULT_SUCCESS, androidx.media3.session.SessionError.INFO_CANCELLED, 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/libraries/common/src/main/java/androidx/media3/common/PlaybackException.java b/libraries/common/src/main/java/androidx/media3/common/PlaybackException.java index aac8674093..fd0df1e064 100644 --- a/libraries/common/src/main/java/androidx/media3/common/PlaybackException.java +++ b/libraries/common/src/main/java/androidx/media3/common/PlaybackException.java @@ -54,6 +54,20 @@ public class PlaybackException extends Exception { @IntDef( open = true, value = { + ERROR_CODE_INVALID_STATE, + ERROR_CODE_BAD_VALUE, + ERROR_CODE_PERMISSION_DENIED, + ERROR_CODE_NOT_SUPPORTED, + ERROR_CODE_DISCONNECTED, + ERROR_CODE_AUTHENTICATION_EXPIRED, + ERROR_CODE_PREMIUM_ACCOUNT_REQUIRED, + ERROR_CODE_CONCURRENT_STREAM_LIMIT, + ERROR_CODE_PARENTAL_CONTROL_RESTRICTED, + ERROR_CODE_NOT_AVAILABLE_IN_REGION, + ERROR_CODE_SKIP_LIMIT_REACHED, + ERROR_CODE_SETUP_REQUIRED, + ERROR_CODE_END_OF_PLAYLIST, + ERROR_CODE_CONTENT_ALREADY_PLAYING, ERROR_CODE_UNSPECIFIED, ERROR_CODE_REMOTE_ERROR, ERROR_CODE_BEHIND_LIVE_WINDOW, @@ -93,6 +107,50 @@ public class PlaybackException extends Exception { }) public @interface ErrorCode {} + // Policy errors (-1 to -999) + + /** Caused by a command that cannot be completed because the current state is not valid. */ + public static final int ERROR_CODE_INVALID_STATE = -2; + + /** Caused by an argument that is illegal. */ + public static final int ERROR_CODE_BAD_VALUE = -3; + + /** Caused by a command that is not allowed. */ + public static final int ERROR_CODE_PERMISSION_DENIED = -4; + + /** Caused by a command that is not supported. */ + public static final int ERROR_CODE_NOT_SUPPORTED = -6; + + /** Caused by a disconnected component. */ + public static final int ERROR_CODE_DISCONNECTED = -100; + + /** Caused by expired authentication. */ + public static final int ERROR_CODE_AUTHENTICATION_EXPIRED = -102; + + /** Caused by a premium account that is required but the user is not subscribed. */ + public static final int ERROR_CODE_PREMIUM_ACCOUNT_REQUIRED = -103; + + /** Caused by too many concurrent streams. */ + public static final int ERROR_CODE_CONCURRENT_STREAM_LIMIT = -104; + + /** Caused by the content being blocked due to parental controls. */ + public static final int ERROR_CODE_PARENTAL_CONTROL_RESTRICTED = -105; + + /** Caused by the content being blocked due to being regionally unavailable. */ + public static final int ERROR_CODE_NOT_AVAILABLE_IN_REGION = -106; + + /** Caused by the skip limit that is exhausted. */ + public static final int ERROR_CODE_SKIP_LIMIT_REACHED = -107; + + /** Caused by playback that needs manual user intervention. */ + public static final int ERROR_CODE_SETUP_REQUIRED = -108; + + /** Caused by navigation that failed because the playlist was exhausted. */ + public static final int ERROR_CODE_END_OF_PLAYLIST = -109; + + /** Caused by a request for content that was already playing. */ + public static final int ERROR_CODE_CONTENT_ALREADY_PLAYING = -110; + // Miscellaneous errors (1xxx). /** Caused by an error whose cause could not be identified. */ @@ -286,6 +344,34 @@ public class PlaybackException extends Exception { /** Returns the name of a given {@code errorCode}. */ public static String getErrorCodeName(@ErrorCode int errorCode) { switch (errorCode) { + case ERROR_CODE_INVALID_STATE: + return "ERROR_CODE_INVALID_STATE"; + case ERROR_CODE_BAD_VALUE: + return "ERROR_CODE_BAD_VALUE"; + case ERROR_CODE_PERMISSION_DENIED: + return "ERROR_CODE_PERMISSION_DENIED"; + case ERROR_CODE_NOT_SUPPORTED: + return "ERROR_CODE_NOT_SUPPORTED"; + case ERROR_CODE_DISCONNECTED: + return "ERROR_CODE_DISCONNECTED"; + case ERROR_CODE_AUTHENTICATION_EXPIRED: + return "ERROR_CODE_AUTHENTICATION_EXPIRED"; + case ERROR_CODE_PREMIUM_ACCOUNT_REQUIRED: + return "ERROR_CODE_PREMIUM_ACCOUNT_REQUIRED"; + case ERROR_CODE_CONCURRENT_STREAM_LIMIT: + return "ERROR_CODE_CONCURRENT_STREAM_LIMIT"; + case ERROR_CODE_PARENTAL_CONTROL_RESTRICTED: + return "ERROR_CODE_PARENTAL_CONTROL_RESTRICTED"; + case ERROR_CODE_NOT_AVAILABLE_IN_REGION: + return "ERROR_CODE_NOT_AVAILABLE_IN_REGION"; + case ERROR_CODE_SKIP_LIMIT_REACHED: + return "ERROR_CODE_SKIP_LIMIT_REACHED"; + case ERROR_CODE_SETUP_REQUIRED: + return "ERROR_CODE_SETUP_REQUIRED"; + case ERROR_CODE_END_OF_PLAYLIST: + return "ERROR_CODE_END_OF_PLAYLIST"; + case ERROR_CODE_CONTENT_ALREADY_PLAYING: + return "ERROR_CODE_CONTENT_ALREADY_PLAYING"; case ERROR_CODE_UNSPECIFIED: return "ERROR_CODE_UNSPECIFIED"; case ERROR_CODE_REMOTE_ERROR: @@ -387,6 +473,9 @@ public class PlaybackException extends Exception { /** The value of {@link SystemClock#elapsedRealtime()} when this exception was created. */ public final long timestampMs; + /** An extras {@link Bundle}. */ + @UnstableApi public final Bundle extras; + /** * Creates an instance. * @@ -398,7 +487,25 @@ public class PlaybackException extends Exception { @UnstableApi public PlaybackException( @Nullable String message, @Nullable Throwable cause, @ErrorCode int errorCode) { - this(message, cause, errorCode, Clock.DEFAULT.elapsedRealtime()); + this(message, cause, errorCode, Bundle.EMPTY, Clock.DEFAULT.elapsedRealtime()); + } + + /** + * Creates an instance. + * + * @param errorCode A number which identifies the cause of the error. May be one of the {@link + * ErrorCode ErrorCodes}. + * @param cause See {@link #getCause()}. + * @param message See {@link #getMessage()}. + * @param extras An optional {@link Bundle}. + */ + @UnstableApi + public PlaybackException( + @Nullable String message, + @Nullable Throwable cause, + @ErrorCode int errorCode, + Bundle extras) { + this(message, cause, errorCode, extras, Clock.DEFAULT.elapsedRealtime()); } /** Creates a new instance using the fields obtained from the given {@link Bundle}. */ @@ -409,6 +516,7 @@ public class PlaybackException extends Exception { /* cause= */ getCauseFromBundle(bundle), /* errorCode= */ bundle.getInt( FIELD_INT_ERROR_CODE, /* defaultValue= */ ERROR_CODE_UNSPECIFIED), + /* extras= */ getExtrasFromBundle(bundle), /* timestampMs= */ bundle.getLong( FIELD_LONG_TIMESTAMP_MS, /* defaultValue= */ SystemClock.elapsedRealtime())); } @@ -419,9 +527,11 @@ public class PlaybackException extends Exception { @Nullable String message, @Nullable Throwable cause, @ErrorCode int errorCode, + Bundle extras, long timestampMs) { super(message, cause); this.errorCode = errorCode; + this.extras = extras; this.timestampMs = timestampMs; } @@ -462,6 +572,7 @@ public class PlaybackException extends Exception { private static final String FIELD_STRING_MESSAGE = Util.intToStringMaxRadix(2); private static final String FIELD_STRING_CAUSE_CLASS_NAME = Util.intToStringMaxRadix(3); private static final String FIELD_STRING_CAUSE_MESSAGE = Util.intToStringMaxRadix(4); + private static final String FIELD_BUNDLE_EXTRAS = Util.intToStringMaxRadix(5); /** * Defines a minimum field ID value for subclasses to use when implementing {@link #toBundle()} @@ -486,6 +597,7 @@ public class PlaybackException extends Exception { bundle.putInt(FIELD_INT_ERROR_CODE, errorCode); bundle.putLong(FIELD_LONG_TIMESTAMP_MS, timestampMs); bundle.putString(FIELD_STRING_MESSAGE, getMessage()); + bundle.putBundle(FIELD_BUNDLE_EXTRAS, extras); @Nullable Throwable cause = getCause(); if (cause != null) { bundle.putString(FIELD_STRING_CAUSE_CLASS_NAME, cause.getClass().getName()); @@ -507,6 +619,11 @@ public class PlaybackException extends Exception { return new RemoteException(message); } + private static Bundle getExtrasFromBundle(Bundle bundle) { + Bundle extras = bundle.getBundle(FIELD_BUNDLE_EXTRAS); + return extras != null ? extras : Bundle.EMPTY; + } + @Nullable private static Throwable getCauseFromBundle(Bundle bundle) { @Nullable String causeClassName = bundle.getString(FIELD_STRING_CAUSE_CLASS_NAME); diff --git a/libraries/common/src/test/java/androidx/media3/common/PlaybackExceptionTest.java b/libraries/common/src/test/java/androidx/media3/common/PlaybackExceptionTest.java index d8d6ee48e4..848218a1a8 100644 --- a/libraries/common/src/test/java/androidx/media3/common/PlaybackExceptionTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/PlaybackExceptionTest.java @@ -30,13 +30,18 @@ public class PlaybackExceptionTest { @Test public void roundTripViaBundle_yieldsEqualInstance() { + Bundle extras = new Bundle(); + extras.putInt("intKey", 123); PlaybackException before = new PlaybackException( /* message= */ "test", /* cause= */ new IOException(/* message= */ "io"), - PlaybackException.ERROR_CODE_IO_FILE_NOT_FOUND); + PlaybackException.ERROR_CODE_IO_FILE_NOT_FOUND, + extras); PlaybackException after = PlaybackException.fromBundle(before.toBundle()); assertPlaybackExceptionsAreEquivalent(before, after); + assertThat(after.extras.size()).isEqualTo(1); + assertThat(after.extras.getInt("intKey")).isEqualTo(123); } // Backward compatibility tests. @@ -51,6 +56,7 @@ public class PlaybackExceptionTest { "message", expectedCause, PlaybackException.ERROR_CODE_AUDIO_TRACK_INIT_FAILED, + Bundle.EMPTY, /* timestampMs= */ 1000); Bundle bundle = new Bundle(); @@ -71,6 +77,7 @@ public class PlaybackExceptionTest { "message", cause, PlaybackException.ERROR_CODE_DECODING_FAILED, + Bundle.EMPTY, /* timestampMs= */ 2000); Bundle bundle = exception.toBundle(); @@ -89,6 +96,7 @@ public class PlaybackExceptionTest { "message", expectedCause, PlaybackException.ERROR_CODE_AUDIO_TRACK_INIT_FAILED, + Bundle.EMPTY, /* timestampMs= */ 1000); Bundle bundle = new Bundle(); diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlaybackException.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlaybackException.java index 7adfd5dd52..555f60ea1c 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlaybackException.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlaybackException.java @@ -279,7 +279,7 @@ public final class ExoPlaybackException extends PlaybackException { @Nullable MediaPeriodId mediaPeriodId, long timestampMs, boolean isRecoverable) { - super(message, cause, errorCode, timestampMs); + super(message, cause, errorCode, Bundle.EMPTY, timestampMs); Assertions.checkArgument(!isRecoverable || type == TYPE_RENDERER); Assertions.checkArgument(cause != null || type == TYPE_REMOTE); this.type = type; diff --git a/libraries/session/src/main/aidl/androidx/media3/session/IMediaController.aidl b/libraries/session/src/main/aidl/androidx/media3/session/IMediaController.aidl index 97e6fcb6c3..21914d90c7 100644 --- a/libraries/session/src/main/aidl/androidx/media3/session/IMediaController.aidl +++ b/libraries/session/src/main/aidl/androidx/media3/session/IMediaController.aidl @@ -48,7 +48,7 @@ oneway interface IMediaController { void onRenderedFirstFrame(int seq) = 3010; void onExtrasChanged(int seq, in Bundle extras) = 3011; void onSessionActivityChanged(int seq, in PendingIntent pendingIntent) = 3013; - void onError(int seq, int errorCode, String errorMessage, in Bundle errorExtras) = 3014; + void onError(int seq, in Bundle sessionError) = 3014; // Next Id for MediaController: 3015 void onChildrenChanged( diff --git a/libraries/session/src/main/java/androidx/media3/session/LegacyConversions.java b/libraries/session/src/main/java/androidx/media3/session/LegacyConversions.java index be273558f8..a621f6eb17 100644 --- a/libraries/session/src/main/java/androidx/media3/session/LegacyConversions.java +++ b/libraries/session/src/main/java/androidx/media3/session/LegacyConversions.java @@ -76,7 +76,6 @@ import androidx.media3.common.Timeline.Period; import androidx.media3.common.Timeline.Window; import androidx.media3.common.util.Log; import androidx.media3.common.util.Util; -import androidx.media3.session.MediaControllerImplLegacy.NonFatalErrorInfo; import androidx.media3.session.MediaLibraryService.LibraryParams; import androidx.media3.session.legacy.AudioAttributesCompat; import androidx.media3.session.legacy.MediaBrowserCompat; @@ -154,53 +153,122 @@ import java.util.concurrent.TimeoutException; } } - private static final ImmutableSet FATAL_LEGACY_ERROR_CODES = - ImmutableSet.of( - PlaybackStateCompat.ERROR_CODE_UNKNOWN_ERROR, - PlaybackStateCompat.ERROR_CODE_AUTHENTICATION_EXPIRED, - PlaybackStateCompat.ERROR_CODE_PREMIUM_ACCOUNT_REQUIRED, - PlaybackStateCompat.ERROR_CODE_CONCURRENT_STREAM_LIMIT, - PlaybackStateCompat.ERROR_CODE_PARENTAL_CONTROL_RESTRICTED, - PlaybackStateCompat.ERROR_CODE_NOT_AVAILABLE_IN_REGION, - PlaybackStateCompat.ERROR_CODE_SKIP_LIMIT_REACHED); - /** Converts {@link PlaybackStateCompat} to {@link PlaybackException}. */ @Nullable public static PlaybackException convertToPlaybackException( @Nullable PlaybackStateCompat playbackStateCompat) { if (playbackStateCompat == null - || playbackStateCompat.getState() != PlaybackStateCompat.STATE_ERROR - || !FATAL_LEGACY_ERROR_CODES.contains(playbackStateCompat.getErrorCode())) { + || playbackStateCompat.getState() != PlaybackStateCompat.STATE_ERROR) { return null; } - StringBuilder stringBuilder = new StringBuilder(); - if (!TextUtils.isEmpty(playbackStateCompat.getErrorMessage())) { - stringBuilder.append(playbackStateCompat.getErrorMessage().toString()).append(", "); - } - stringBuilder.append("code=").append(playbackStateCompat.getErrorCode()); - String errorMessage = stringBuilder.toString(); + @Nullable CharSequence errorMessage = playbackStateCompat.getErrorMessage(); + @Nullable Bundle playbackStateCompatExtras = playbackStateCompat.getExtras(); return new PlaybackException( - errorMessage, /* cause= */ null, PlaybackException.ERROR_CODE_REMOTE_ERROR); + errorMessage != null ? errorMessage.toString() : null, + /* cause= */ null, + convertToPlaybackExceptionErrorCode(playbackStateCompat.getErrorCode()), + playbackStateCompatExtras != null ? playbackStateCompatExtras : Bundle.EMPTY); } - /** Converts {@link PlaybackStateCompat} to {@link NonFatalErrorInfo}. */ + /** Converts {@link PlaybackStateCompat} to {@link SessionError}. */ @Nullable - public static NonFatalErrorInfo convertToNonFatalErrorInfo( - @Nullable PlaybackStateCompat playbackStateCompat, String errorMessageFallback) { + public static SessionError convertToSessionError( + @Nullable PlaybackStateCompat playbackStateCompat) { if (playbackStateCompat == null - || playbackStateCompat.getState() != PlaybackStateCompat.STATE_ERROR - || FATAL_LEGACY_ERROR_CODES.contains(playbackStateCompat.getErrorCode())) { + || playbackStateCompat.getState() == PlaybackStateCompat.STATE_ERROR + || playbackStateCompat.getErrorCode() == PlaybackStateCompat.ERROR_CODE_UNKNOWN_ERROR + || playbackStateCompat.getErrorMessage() == null) { return null; } @Nullable Bundle playbackStateCompatExtras = playbackStateCompat.getExtras(); - return new NonFatalErrorInfo( - playbackStateCompat.getErrorCode(), - !TextUtils.isEmpty(playbackStateCompat.getErrorMessage()) - ? playbackStateCompat.getErrorMessage().toString() - : errorMessageFallback, + return new SessionError( + convertToSessionErrorCode(playbackStateCompat.getErrorCode()), + checkNotNull(playbackStateCompat.getErrorMessage()).toString(), playbackStateCompatExtras != null ? playbackStateCompatExtras : Bundle.EMPTY); } + private static @SessionError.Code int convertToSessionErrorCode( + @PlaybackStateCompat.ErrorCode int errorCode) { + switch (errorCode) { + case PlaybackStateCompat.ERROR_CODE_ACTION_ABORTED: + return SessionError.INFO_CANCELLED; + case PlaybackStateCompat.ERROR_CODE_APP_ERROR: + return SessionError.ERROR_INVALID_STATE; + case PlaybackStateCompat.ERROR_CODE_AUTHENTICATION_EXPIRED: + return SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED; + case PlaybackStateCompat.ERROR_CODE_CONTENT_ALREADY_PLAYING: + return SessionError.ERROR_SESSION_CONTENT_ALREADY_PLAYING; + case PlaybackStateCompat.ERROR_CODE_CONCURRENT_STREAM_LIMIT: + return SessionError.ERROR_SESSION_CONCURRENT_STREAM_LIMIT; + case PlaybackStateCompat.ERROR_CODE_END_OF_QUEUE: + return SessionError.ERROR_SESSION_END_OF_PLAYLIST; + case PlaybackStateCompat.ERROR_CODE_NOT_AVAILABLE_IN_REGION: + return SessionError.ERROR_SESSION_NOT_AVAILABLE_IN_REGION; + case PlaybackStateCompat.ERROR_CODE_NOT_SUPPORTED: + return SessionError.ERROR_NOT_SUPPORTED; + case PlaybackStateCompat.ERROR_CODE_PARENTAL_CONTROL_RESTRICTED: + return SessionError.ERROR_SESSION_PARENTAL_CONTROL_RESTRICTED; + case PlaybackStateCompat.ERROR_CODE_PREMIUM_ACCOUNT_REQUIRED: + return SessionError.ERROR_SESSION_PREMIUM_ACCOUNT_REQUIRED; + case PlaybackStateCompat.ERROR_CODE_SKIP_LIMIT_REACHED: + return SessionError.ERROR_SESSION_SKIP_LIMIT_REACHED; + default: + return SessionError.ERROR_UNKNOWN; + } + } + + private static @PlaybackException.ErrorCode int convertToPlaybackExceptionErrorCode( + @PlaybackStateCompat.ErrorCode int errorCode) { + @PlaybackException.ErrorCode + int playbackExceptionErrorCode = convertToSessionErrorCode(errorCode); + switch (playbackExceptionErrorCode) { + case SessionError.ERROR_UNKNOWN: + return PlaybackException.ERROR_CODE_UNSPECIFIED; + case SessionError.ERROR_IO: + return PlaybackException.ERROR_CODE_IO_UNSPECIFIED; + default: + return playbackExceptionErrorCode; + } + } + + /** Converts {@link SessionError.Code} to {@link PlaybackStateCompat.ErrorCode}. */ + @PlaybackStateCompat.ErrorCode + public static int convertToLegacyErrorCode(@SessionError.Code int errorCode) { + switch (errorCode) { + case SessionError.INFO_CANCELLED: + return PlaybackStateCompat.ERROR_CODE_ACTION_ABORTED; + case SessionError.ERROR_INVALID_STATE: + return PlaybackStateCompat.ERROR_CODE_APP_ERROR; + case SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED: + return PlaybackStateCompat.ERROR_CODE_AUTHENTICATION_EXPIRED; + case SessionError.ERROR_SESSION_CONTENT_ALREADY_PLAYING: + return PlaybackStateCompat.ERROR_CODE_CONTENT_ALREADY_PLAYING; + case SessionError.ERROR_SESSION_CONCURRENT_STREAM_LIMIT: + return PlaybackStateCompat.ERROR_CODE_CONCURRENT_STREAM_LIMIT; + case SessionError.ERROR_SESSION_END_OF_PLAYLIST: + return PlaybackStateCompat.ERROR_CODE_END_OF_QUEUE; + case SessionError.ERROR_SESSION_NOT_AVAILABLE_IN_REGION: + return PlaybackStateCompat.ERROR_CODE_NOT_AVAILABLE_IN_REGION; + case SessionError.ERROR_NOT_SUPPORTED: + return PlaybackStateCompat.ERROR_CODE_NOT_SUPPORTED; + case SessionError.ERROR_SESSION_PARENTAL_CONTROL_RESTRICTED: + return PlaybackStateCompat.ERROR_CODE_PARENTAL_CONTROL_RESTRICTED; + case SessionError.ERROR_SESSION_PREMIUM_ACCOUNT_REQUIRED: + return PlaybackStateCompat.ERROR_CODE_PREMIUM_ACCOUNT_REQUIRED; + case SessionError.ERROR_SESSION_SKIP_LIMIT_REACHED: + return PlaybackStateCompat.ERROR_CODE_SKIP_LIMIT_REACHED; + case SessionError.ERROR_UNKNOWN: // fall through + default: + return PlaybackStateCompat.ERROR_CODE_UNKNOWN_ERROR; + } + } + + /** Converts {@link PlaybackException} to {@link PlaybackStateCompat.ErrorCode}. */ + @PlaybackStateCompat.ErrorCode + public static int convertToLegacyErrorCode(PlaybackException playbackException) { + return convertToLegacyErrorCode(playbackException.errorCode); + } + public static MediaBrowserCompat.MediaItem convertToBrowserItem( MediaItem item, @Nullable Bitmap artworkBitmap) { MediaDescriptionCompat description = convertToMediaDescriptionCompat(item, artworkBitmap); 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 690cc5e73e..05e5b84993 100644 --- a/libraries/session/src/main/java/androidx/media3/session/LibraryResult.java +++ b/libraries/session/src/main/java/androidx/media3/session/LibraryResult.java @@ -53,7 +53,7 @@ public final class LibraryResult { @Target(TYPE_USE) @IntDef({ RESULT_SUCCESS, - SessionError.INFO_SKIPPED, + SessionError.INFO_CANCELLED, SessionError.ERROR_UNKNOWN, SessionError.ERROR_INVALID_STATE, SessionError.ERROR_BAD_VALUE, @@ -82,7 +82,7 @@ public final class LibraryResult { 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; + public static final int RESULT_INFO_SKIPPED = SessionError.INFO_CANCELLED; /** Result code representing that the command is ended with an unknown error. */ public static final int RESULT_ERROR_UNKNOWN = SessionError.ERROR_UNKNOWN; 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 2ef1a32eeb..381ac84d96 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaBrowserImplBase.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaBrowserImplBase.java @@ -24,7 +24,7 @@ import static androidx.media3.session.SessionCommand.COMMAND_CODE_LIBRARY_SUBSCR 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 static androidx.media3.session.SessionError.INFO_CANCELLED; import android.content.Context; import android.os.Bundle; @@ -189,7 +189,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; IMediaSession iSession = getSessionInterfaceWithSessionCommandIfAble(commandCode); if (iSession != null) { SequencedFuture> result = - sequencedFutureManager.createSequencedFuture(LibraryResult.ofError(INFO_SKIPPED)); + sequencedFutureManager.createSequencedFuture(LibraryResult.ofError(INFO_CANCELLED)); try { task.run(iSession, result.getSequenceNumber()); } catch (RemoteException e) { 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 be85e1dccd..bb68ebc8ae 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaController.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaController.java @@ -451,51 +451,24 @@ public class MediaController implements Player { /** * Called when an non-fatal error {@linkplain - * MediaSession#sendError(MediaSession.ControllerInfo, int, int, Bundle) sent by the session} is + * MediaSession#sendError(MediaSession.ControllerInfo, SessionError) sent by the session} is * received. * - *

When connected to a legacy or platform session, this callback is called each time the - * callback {@link + *

When connected to a legacy or platform session, this callback is called when {@link * android.media.session.MediaController.Callback#onPlaybackStateChanged(PlaybackState)} is - * called in {@linkplain PlaybackState#STATE_ERROR state error} with a non-fatal error code. + * called with an error code and an error message while the playback state is different to + * {@linkplain PlaybackState#STATE_ERROR state error}. * - *

Non-fatal legacy error codes: - * - *

    - *
  • {@code PlaybackStateCompat.ERROR_CODE_ACTION_ABORTED} - *
  • {@code PlaybackStateCompat.ERROR_CODE_APP_ERROR} - *
  • {@code PlaybackStateCompat.ERROR_CODE_CONTENT_ALREADY_PLAYING} - *
  • {@code PlaybackStateCompat.ERROR_CODE_END_OF_QUEUE} - *
  • {@code PlaybackStateCompat.ERROR_CODE_NOT_SUPPORTED} - *
  • In addition, all other error code values not defined as fatal errors in {@code - * android.support.v4.media.session.PlaybackStateCompat} are handled as non-fatal. - *
- * - *

Fatal legacy error codes of the {@link PlaybackState} in state {@link - * PlaybackState#STATE_ERROR} are converted to a player error. See {@link + *

Fatal playback errors are reported to {@link * Player.Listener#onPlayerError(PlaybackException)} and {@link - * Player.Listener#onPlayerErrorChanged(PlaybackException)}. - * - *

Fatal legacy error codes: - * - *

    - *
  • {@code PlaybackStateCompat.ERROR_CODE_UNKNOWN_ERROR} - *
  • {@code PlaybackStateCompat.ERROR_CODE_AUTHENTICATION_EXPIRED} - *
  • {@code PlaybackStateCompat.ERROR_CODE_PREMIUM_ACCOUNT_REQUIRED} - *
  • {@code PlaybackStateCompat.ERROR_CODE_CONCURRENT_STREAM_LIMIT} - *
  • {@code PlaybackStateCompat.ERROR_CODE_PARENTAL_CONTROL_RESTRICTED} - *
  • {@code PlaybackStateCompat.ERROR_CODE_NOT_AVAILABLE_IN_REGION} - *
  • {@code PlaybackStateCompat.ERROR_CODE_SKIP_LIMIT_REACHED} - *
+ * Player.Listener#onPlayerErrorChanged(PlaybackException)} of listeners {@linkplain + * #addListener(Player.Listener) registered on the controller}. * * @param controller The {@link MediaController} that received the error. - * @param errorCode The error code. - * @param errorMessage The localized error message. - * @param errorExtras A bundle with additional custom error data. + * @param sessionError The session error. */ @UnstableApi - default void onError( - MediaController controller, int errorCode, String errorMessage, Bundle errorExtras) {} + default void onError(MediaController controller, SessionError sessionError) {} } /* package */ interface ConnectionCallback { 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 c489734218..0bccdce336 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java @@ -2890,13 +2890,12 @@ import org.checkerframework.checker.nullness.qual.NonNull; listener -> listener.onSessionActivityChanged(getInstance(), sessionActivity)); } - public void onError(int seq, int errorCode, String errorMessage, Bundle errorExtras) { + public void onError(int seq, SessionError sessionError) { if (!isConnected()) { return; } getInstance() - .notifyControllerListener( - listener -> listener.onError(getInstance(), errorCode, errorMessage, errorExtras)); + .notifyControllerListener(listener -> listener.onError(getInstance(), sessionError)); } public void onRenderedFirstFrame() { diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java index cdc4cf316d..2f29b9465f 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java @@ -193,7 +193,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; controllerInfo.availablePlayerCommands, controllerInfo.customLayout, controllerInfo.sessionExtras, - /* errorInfo= */ null); + /* sessionError= */ null); updateStateMaskedControllerInfo( maskedControllerInfo, /* discontinuityReason= */ null, @@ -259,7 +259,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; controllerInfo.availablePlayerCommands, controllerInfo.customLayout, controllerInfo.sessionExtras, - /* errorInfo= */ null); + /* sessionError= */ null); updateStateMaskedControllerInfo( maskedControllerInfo, /* discontinuityReason= */ null, @@ -382,7 +382,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; controllerInfo.availablePlayerCommands, controllerInfo.customLayout, controllerInfo.sessionExtras, - /* errorInfo= */ null); + /* sessionError= */ null); updateStateMaskedControllerInfo( maskedControllerInfo, discontinuityReason, mediaItemTransitionReason); } @@ -542,7 +542,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; controllerInfo.availablePlayerCommands, controllerInfo.customLayout, controllerInfo.sessionExtras, - /* errorInfo= */ null); + /* sessionError= */ null); updateStateMaskedControllerInfo( maskedControllerInfo, /* discontinuityReason= */ null, @@ -563,7 +563,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; controllerInfo.availablePlayerCommands, controllerInfo.customLayout, controllerInfo.sessionExtras, - /* errorInfo= */ null); + /* sessionError= */ null); updateStateMaskedControllerInfo( maskedControllerInfo, /* discontinuityReason= */ null, @@ -657,7 +657,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; controllerInfo.availablePlayerCommands, controllerInfo.customLayout, controllerInfo.sessionExtras, - /* errorInfo= */ null); + /* sessionError= */ null); updateStateMaskedControllerInfo( maskedControllerInfo, /* discontinuityReason= */ null, @@ -722,7 +722,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; controllerInfo.availablePlayerCommands, controllerInfo.customLayout, controllerInfo.sessionExtras, - /* errorInfo= */ null); + /* sessionError= */ null); updateStateMaskedControllerInfo( maskedControllerInfo, /* discontinuityReason= */ null, @@ -776,7 +776,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; controllerInfo.availablePlayerCommands, controllerInfo.customLayout, controllerInfo.sessionExtras, - /* errorInfo= */ null); + /* sessionError= */ null); updateStateMaskedControllerInfo( maskedControllerInfo, /* discontinuityReason= */ null, @@ -844,7 +844,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; controllerInfo.availablePlayerCommands, controllerInfo.customLayout, controllerInfo.sessionExtras, - /* errorInfo= */ null); + /* sessionError= */ null); updateStateMaskedControllerInfo( maskedControllerInfo, /* discontinuityReason= */ null, @@ -958,7 +958,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; controllerInfo.availablePlayerCommands, controllerInfo.customLayout, controllerInfo.sessionExtras, - /* errorInfo= */ null); + /* sessionError= */ null); updateStateMaskedControllerInfo( maskedControllerInfo, /* discontinuityReason= */ null, @@ -986,7 +986,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; controllerInfo.availablePlayerCommands, controllerInfo.customLayout, controllerInfo.sessionExtras, - /* errorInfo= */ null); + /* sessionError= */ null); updateStateMaskedControllerInfo( maskedControllerInfo, /* discontinuityReason= */ null, @@ -1123,7 +1123,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; controllerInfo.availablePlayerCommands, controllerInfo.customLayout, controllerInfo.sessionExtras, - /* errorInfo= */ null); + /* sessionError= */ null); updateStateMaskedControllerInfo( maskedControllerInfo, /* discontinuityReason= */ null, @@ -1156,7 +1156,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; controllerInfo.availablePlayerCommands, controllerInfo.customLayout, controllerInfo.sessionExtras, - /* errorInfo= */ null); + /* sessionError= */ null); updateStateMaskedControllerInfo( maskedControllerInfo, /* discontinuityReason= */ null, @@ -1188,7 +1188,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; controllerInfo.availablePlayerCommands, controllerInfo.customLayout, controllerInfo.sessionExtras, - /* errorInfo= */ null); + /* sessionError= */ null); updateStateMaskedControllerInfo( maskedControllerInfo, /* discontinuityReason= */ null, @@ -1223,7 +1223,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; controllerInfo.availablePlayerCommands, controllerInfo.customLayout, controllerInfo.sessionExtras, - /* errorInfo= */ null); + /* sessionError= */ null); updateStateMaskedControllerInfo( maskedControllerInfo, /* discontinuityReason= */ null, @@ -1262,7 +1262,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; controllerInfo.availablePlayerCommands, controllerInfo.customLayout, controllerInfo.sessionExtras, - /* errorInfo= */ null); + /* sessionError= */ null); updateStateMaskedControllerInfo( maskedControllerInfo, /* discontinuityReason= */ null, @@ -1555,8 +1555,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; controllerCompat.isSessionReady(), controllerCompat.getRatingType(), getInstance().getTimeDiffMs(), - getRoutingControllerId(controllerCompat), - context); + getRoutingControllerId(controllerCompat)); Pair<@NullableType Integer, @NullableType Integer> reasons = calculateDiscontinuityAndTransitionReason( legacyPlayerInfo, @@ -1748,15 +1747,10 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; listener.onCustomLayoutChanged(getInstance(), newControllerInfo.customLayout); }); } - if (newControllerInfo.nonFatalErrorInfo != null) { + if (newControllerInfo.sessionError != null) { getInstance() .notifyControllerListener( - listener -> - listener.onError( - getInstance(), - newControllerInfo.nonFatalErrorInfo.errorCode, - newControllerInfo.nonFatalErrorInfo.errorMessage, - newControllerInfo.nonFatalErrorInfo.errorExtras)); + listener -> listener.onError(getInstance(), newControllerInfo.sessionError)); } listeners.flushEvents(); } @@ -1781,18 +1775,6 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; // Ignore return value of the future because legacy session cannot get result back. } - /* package */ static class NonFatalErrorInfo { - public final int errorCode; - public final String errorMessage; - public final Bundle errorExtras; - - public NonFatalErrorInfo(int errorCode, String errorMessage, Bundle errorExtras) { - this.errorCode = errorCode; - this.errorMessage = errorMessage; - this.errorExtras = errorExtras; - } - } - private class ConnectionCallback extends MediaBrowserCompat.ConnectionCallback { @Override @@ -1913,7 +1895,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; controllerInfo.availablePlayerCommands, controllerInfo.customLayout, extras, - /* errorInfo= */ null); + /* sessionError= */ null); getInstance() .notifyControllerListener(listener -> listener.onExtrasChanged(getInstance(), extras)); } @@ -1976,8 +1958,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; boolean isSessionReady, @RatingCompat.Style int ratingType, long timeDiffMs, - @Nullable String routingControllerId, - Context context) { + @Nullable String routingControllerId) { QueueTimeline currentTimeline; MediaMetadata mediaMetadata; int currentMediaItemIndex; @@ -2094,10 +2075,8 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; PlaybackException playerError = LegacyConversions.convertToPlaybackException(newLegacyPlayerInfo.playbackStateCompat); - NonFatalErrorInfo nonFatalErrorInfo = - LegacyConversions.convertToNonFatalErrorInfo( - newLegacyPlayerInfo.playbackStateCompat, - context.getString(R.string.legacy_error_message_fallback)); + SessionError sessionError = + LegacyConversions.convertToSessionError(newLegacyPlayerInfo.playbackStateCompat); long currentPositionMs = LegacyConversions.convertToCurrentPositionMs( @@ -2166,7 +2145,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; customLayout, newLegacyPlayerInfo.sessionExtras, playerError, - nonFatalErrorInfo, + sessionError, durationMs, currentPositionMs, bufferedPositionMs, @@ -2335,7 +2314,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; ImmutableList customLayout, Bundle sessionExtras, @Nullable PlaybackException playerError, - @Nullable NonFatalErrorInfo nonFatalErrorInfo, + @Nullable SessionError sessionError, long durationMs, long currentPositionMs, long bufferedPositionMs, @@ -2410,7 +2389,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; availablePlayerCommands, customLayout, sessionExtras, - /* errorInfo= */ nonFatalErrorInfo); + sessionError); } private static PositionInfo createPositionInfo( @@ -2621,7 +2600,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; public final Commands availablePlayerCommands; public final ImmutableList customLayout; public final Bundle sessionExtras; - @Nullable public final NonFatalErrorInfo nonFatalErrorInfo; + @Nullable public final SessionError sessionError; public ControllerInfo() { playerInfo = PlayerInfo.DEFAULT.copyWithTimeline(QueueTimeline.DEFAULT); @@ -2629,7 +2608,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; availablePlayerCommands = Commands.EMPTY; customLayout = ImmutableList.of(); sessionExtras = Bundle.EMPTY; - nonFatalErrorInfo = null; + sessionError = null; } public ControllerInfo( @@ -2638,13 +2617,13 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; Commands availablePlayerCommands, ImmutableList customLayout, @Nullable Bundle sessionExtras, - @Nullable NonFatalErrorInfo nonFatalErrorInfo) { + @Nullable SessionError sessionError) { this.playerInfo = playerInfo; this.availableSessionCommands = availableSessionCommands; this.availablePlayerCommands = availablePlayerCommands; this.customLayout = customLayout; this.sessionExtras = sessionExtras == null ? Bundle.EMPTY : sessionExtras; - this.nonFatalErrorInfo = nonFatalErrorInfo; + this.sessionError = sessionError; } } } diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaControllerStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaControllerStub.java index c8211f9a2a..320c4339b4 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaControllerStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaControllerStub.java @@ -38,7 +38,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; private static final String TAG = "MediaControllerStub"; /** The version of the IMediaController interface. */ - public static final int VERSION_INT = 4; + public static final int VERSION_INT = 5; private final WeakReference controller; @@ -265,10 +265,15 @@ import org.checkerframework.checker.nullness.qual.NonNull; } @Override - public void onError(int seq, int errorCode, String errorMessage, Bundle errorExtras) - throws RemoteException { - dispatchControllerTaskOnHandler( - controller -> controller.onError(seq, errorCode, errorMessage, errorExtras)); + public void onError(int seq, Bundle sessionError) throws RemoteException { + SessionError error; + try { + error = SessionError.fromBundle(sessionError); + } catch (RuntimeException e) { + Log.w(TAG, "Ignoring malformed Bundle for SessionError", e); + return; + } + dispatchControllerTaskOnHandler(controller -> controller.onError(seq, error)); } @Override 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 11d670cfa7..e70fb69537 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaLibrarySessionImpl.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaLibrarySessionImpl.java @@ -20,7 +20,6 @@ import static androidx.media3.common.util.Assertions.checkState; 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; @@ -372,18 +371,20 @@ import java.util.concurrent.Future; private void maybeUpdateLegacyErrorState(LibraryResult result) { PlayerWrapper playerWrapper = getPlayerWrapper(); - if (setLegacyErrorState(result)) { + if (setLegacyAuthenticationExpiredErrorState(result)) { // Sync playback state if legacy error state changed. getSessionCompat().setPlaybackState(playerWrapper.createPlaybackStateCompat()); - } else if (playerWrapper.getLegacyStatusCode() != STATUS_CODE_SUCCESS_COMPAT) { + } else if (playerWrapper.getLegacyError() != null && result.resultCode == RESULT_SUCCESS) { playerWrapper.clearLegacyErrorStatus(); getSessionCompat().setPlaybackState(playerWrapper.createPlaybackStateCompat()); } } - private boolean setLegacyErrorState(LibraryResult result) { + private boolean setLegacyAuthenticationExpiredErrorState(LibraryResult result) { + PlayerWrapper playerWrapper = getPlayerWrapper(); + PlayerWrapper.LegacyError legacyError = playerWrapper.getLegacyError(); if (result.resultCode == ERROR_SESSION_AUTHENTICATION_EXPIRED - && getPlayerWrapper().getLegacyStatusCode() != ERROR_SESSION_AUTHENTICATION_EXPIRED) { + && (legacyError == null || legacyError.code != 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; @@ -396,11 +397,11 @@ import java.util.concurrent.Future; 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); + playerWrapper.setLegacyError( + /* isFatal= */ true, + ERROR_CODE_AUTHENTICATION_EXPIRED_COMPAT, + getContext().getString(R.string.authentication_required), + bundle); return true; } return false; 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 ff6e1692d9..a75902e2fa 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java @@ -1138,6 +1138,9 @@ public class MediaSession { /** * Sends a non-fatal error to the given controller. * + *

This will call {@link MediaController.Listener#onError(MediaController, SessionError)} of + * the given connected controller. + * *

Use {@linkplain MediaSession#getMediaNotificationControllerInfo()} to set the error of the * {@linkplain android.media.session.PlaybackState playback state} of the legacy platform session. * @@ -1146,33 +1149,28 @@ public class MediaSession { * ControllerInfo#LEGACY_CONTROLLER_VERSION}, an {@link IllegalArgumentException} is thrown. * * @param controllerInfo The controller to send the error to. - * @param errorCode The error code. - * @param errorMessageResId A {@code R.string} resource ID. - * @param errorExtras A error extras bundle to send additional data. + * @param sessionError The session error. * @exception IllegalArgumentException thrown if an error is attempted to be sent to a legacy * controller. */ @UnstableApi - public final void sendError( - ControllerInfo controllerInfo, int errorCode, int errorMessageResId, Bundle errorExtras) { + public final void sendError(ControllerInfo controllerInfo, SessionError sessionError) { checkArgument( controllerInfo.getControllerVersion() != ControllerInfo.LEGACY_CONTROLLER_VERSION); - impl.sendError(controllerInfo, errorCode, errorMessageResId, errorExtras); + impl.sendError(controllerInfo, sessionError); } /** * Sends a non-fatal error to all connected Media3 controllers. * - *

See {@link #sendError(ControllerInfo, int, int, Bundle)} for sending an error to a specific + *

See {@link #sendError(ControllerInfo, SessionError)} for sending an error to a specific * controller only. * - * @param errorCode The error code. - * @param errorMessageResourceId A {@code R.string} resource ID of a localized error message. - * @param errorExtras An error extras bundle to send additional data. + * @param sessionError The session error. */ @UnstableApi - public final void sendError(int errorCode, int errorMessageResourceId, Bundle errorExtras) { - impl.sendError(errorCode, errorMessageResourceId, errorExtras); + public final void sendError(SessionError sessionError) { + impl.sendError(sessionError); } /* package */ final MediaSessionCompat getSessionCompat() { @@ -2004,8 +2002,7 @@ public class MediaSession { default void onRenderedFirstFrame(int seq) throws RemoteException {} - default void onError(int seq, int errorCode, String errorMessage, Bundle errorExtras) - throws RemoteException {} + default void onError(int seq, SessionError sessionError) throws RemoteException {} } /** 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 6d3e64bbf7..678a0d4561 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java @@ -33,7 +33,7 @@ import static androidx.media3.common.util.Util.postOrRun; import static androidx.media3.session.MediaSessionStub.UNKNOWN_SEQUENCE_NUMBER; 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 static androidx.media3.session.SessionError.INFO_CANCELLED; 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(INFO_SKIPPED); + private static final SessionResult RESULT_WHEN_CLOSED = new SessionResult(INFO_CANCELLED); private final Object lock = new Object(); @@ -625,31 +625,25 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; controller, (cb, seq) -> cb.sendCustomCommand(seq, command, args)); } - public void sendError( - ControllerInfo controllerInfo, - int errorCode, - int errorMessageResourceId, - Bundle errorExtras) { + public void sendError(ControllerInfo controllerInfo, SessionError sessionError) { if (controllerInfo.getInterfaceVersion() < 4) { // IMediaController.onError introduced with interface version 4. return; } - String errorMessage = context.getString(errorMessageResourceId); dispatchRemoteControllerTaskWithoutReturn( - controllerInfo, - (callback, seq) -> callback.onError(seq, errorCode, errorMessage, errorExtras)); + controllerInfo, (callback, seq) -> callback.onError(seq, sessionError)); if (isMediaNotificationController(controllerInfo)) { dispatchRemoteControllerTaskToLegacyStub( - (callback, seq) -> callback.onError(seq, errorCode, errorMessage, errorExtras)); + (callback, seq) -> callback.onError(seq, sessionError)); } } - public void sendError(int errorCode, int errorMessageResourceId, Bundle errorExtras) { + public void sendError(SessionError sessionError) { // Send error messages only to Media3 controllers. ImmutableList connectedControllers = sessionStub.getConnectedControllersManager().getConnectedControllers(); for (int i = 0; i < connectedControllers.size(); i++) { - sendError(connectedControllers.get(i), errorCode, errorMessageResourceId, errorExtras); + sendError(connectedControllers.get(i), sessionError); } } 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 543371d660..400dbd35da 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java @@ -1118,9 +1118,13 @@ import org.checkerframework.checker.initialization.qual.Initialized; } @Override - public void onError(int seq, int errorCode, String errorMessage, Bundle errorExtras) { + public void onError(int seq, SessionError sessionError) { PlayerWrapper playerWrapper = sessionImpl.getPlayerWrapper(); - playerWrapper.setLegacyErrorStatus(errorCode, errorMessage, errorExtras); + playerWrapper.setLegacyError( + /* isFatal= */ false, + LegacyConversions.convertToLegacyErrorCode(sessionError.code), + sessionError.message, + sessionError.extras); sessionCompat.setPlaybackState(playerWrapper.createPlaybackStateCompat()); playerWrapper.clearLegacyErrorStatus(); sessionCompat.setPlaybackState(playerWrapper.createPlaybackStateCompat()); 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 42ac3c4041..619413e36f 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java @@ -57,7 +57,7 @@ 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 static androidx.media3.session.SessionError.INFO_CANCELLED; import android.app.PendingIntent; import android.os.Binder; @@ -278,7 +278,7 @@ 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(INFO_SKIPPED); + result = LibraryResult.ofError(INFO_CANCELLED); } catch (ExecutionException | InterruptedException e) { Log.w(TAG, "Library operation failed", e); result = LibraryResult.ofError(ERROR_UNKNOWN); @@ -2141,9 +2141,8 @@ import java.util.concurrent.ExecutionException; } @Override - public void onError(int sequenceNumber, int errorCode, String errorMessage, Bundle errorExtras) - throws RemoteException { - iController.onError(sequenceNumber, errorCode, errorMessage, errorExtras); + public void onError(int sequenceNumber, SessionError sessionError) throws RemoteException { + iController.onError(sequenceNumber, sessionError.toBundle()); } @Override 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 46dd513c24..e8c3a09a69 100644 --- a/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java +++ b/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java @@ -50,7 +50,6 @@ import androidx.media3.common.VideoSize; import androidx.media3.common.text.CueGroup; import androidx.media3.common.util.Log; import androidx.media3.common.util.Size; -import androidx.media3.common.util.Util; import androidx.media3.session.legacy.MediaSessionCompat; import androidx.media3.session.legacy.PlaybackStateCompat; import androidx.media3.session.legacy.VolumeProviderCompat; @@ -64,13 +63,28 @@ import java.util.List; */ /* package */ final class PlayerWrapper extends ForwardingPlayer { - /* package */ static final int STATUS_CODE_SUCCESS_COMPAT = -1; + /** Describes a legacy error. */ + public static final class LegacyError { + public final boolean isFatal; + @PlaybackStateCompat.ErrorCode public final int code; + @Nullable public final String message; + public final Bundle extras; + + /** Creates an instance. */ + private LegacyError( + boolean isFatal, + @PlaybackStateCompat.ErrorCode int code, + @Nullable String message, + @Nullable Bundle extras) { + this.isFatal = isFatal; + this.code = code; + this.message = message; + this.extras = extras != null ? extras : Bundle.EMPTY; + } + } private final boolean playIfSuppressed; - - private int legacyStatusCode; - @Nullable private String legacyErrorMessage; - @Nullable private Bundle legacyErrorExtras; + @Nullable private LegacyError legacyError; @Nullable private Bundle legacyExtras; private ImmutableList customLayout; private SessionCommands availableSessionCommands; @@ -89,7 +103,6 @@ import java.util.List; this.availableSessionCommands = availableSessionCommands; this.availablePlayerCommands = availablePlayerCommands; this.legacyExtras = legacyExtras; - legacyStatusCode = STATUS_CODE_SUCCESS_COMPAT; } public void setAvailableCommands( @@ -128,36 +141,37 @@ import java.util.List; } /** - * Sets the legacy error code. + * Sets the legacy error that will be used when the next {@linkplain #createPlaybackStateCompat() + * legacy playback state is created}. * *

This sets the legacy {@link PlaybackStateCompat} to {@link PlaybackStateCompat#STATE_ERROR} - * and calls {@link PlaybackStateCompat.Builder#setErrorMessage(int, CharSequence)} and {@link - * PlaybackStateCompat.Builder#setExtras(Bundle)} with the given arguments. + * if the error is fatal, calls {@link PlaybackStateCompat.Builder#setErrorMessage(int, + * CharSequence)} and includes the entries of the extras in the {@link Bundle} set with {@link + * PlaybackStateCompat.Builder#setExtras(Bundle)}. * - *

Use {@link #clearLegacyErrorStatus()} to clear the error state and to resume to the actual - * playback state reflecting the player. + *

Use {@link #clearLegacyErrorStatus()} to clear the error. * + * @param isFatal Whether the legacy error is fatal. * @param errorCode The legacy error code. * @param errorMessage The legacy error message. * @param extras The extras. */ - public void setLegacyErrorStatus(int errorCode, String errorMessage, Bundle extras) { - checkState(errorCode != STATUS_CODE_SUCCESS_COMPAT); - legacyStatusCode = errorCode; - legacyErrorMessage = errorMessage; - legacyErrorExtras = extras; + public void setLegacyError(boolean isFatal, int errorCode, String errorMessage, Bundle extras) { + legacyError = new LegacyError(isFatal, errorCode, errorMessage, extras); } - /** Returns the legacy status code. */ - public int getLegacyStatusCode() { - return legacyStatusCode; + /** Returns the legacy error or null if not set. */ + @Nullable + public LegacyError getLegacyError() { + return legacyError; } - /** Clears the legacy error status. */ + /** + * Clears the legacy error to resolve the error when {@linkplain #createPlaybackStateCompat() + * creating} the next legacy playback state. + */ public void clearLegacyErrorStatus() { - legacyStatusCode = STATUS_CODE_SUCCESS_COMPAT; - legacyErrorMessage = null; - legacyErrorExtras = null; + legacyError = null; } @Override @@ -1016,24 +1030,23 @@ import java.util.List; } public PlaybackStateCompat createPlaybackStateCompat() { - if (legacyStatusCode != STATUS_CODE_SUCCESS_COMPAT) { - Bundle extras; + LegacyError legacyError = this.legacyError; + if (legacyError != null && legacyError.isFatal) { + Bundle extras = new Bundle(legacyError.extras); if (legacyExtras != null) { - extras = new Bundle(checkNotNull(legacyErrorExtras)); extras.putAll(legacyExtras); - } else { - extras = checkNotNull(legacyErrorExtras); } return new PlaybackStateCompat.Builder() .setState( PlaybackStateCompat.STATE_ERROR, /* position= */ PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, - /* playbackSpeed= */ 0, + /* playbackSpeed= */ .0f, /* updateTime= */ SystemClock.elapsedRealtime()) .setActions(0) .setBufferedPosition(0) - .setErrorMessage(legacyStatusCode, checkNotNull(legacyErrorMessage)) .setExtras(extras) + .setErrorMessage(legacyError.code, checkNotNull(legacyError.message)) + .setExtras(legacyError.extras) .build(); } @Nullable PlaybackException playerError = getPlayerError(); @@ -1051,7 +1064,10 @@ import java.util.List; : MediaSessionCompat.QueueItem.UNKNOWN_ID; float playbackSpeed = getPlaybackParameters().speed; float sessionPlaybackSpeed = isPlaying() ? playbackSpeed : 0f; - Bundle extras = legacyExtras != null ? new Bundle(legacyExtras) : new Bundle(); + Bundle extras = legacyError != null ? new Bundle(legacyError.extras) : new Bundle(); + if (legacyExtras != null && !legacyExtras.isEmpty()) { + extras.putAll(legacyExtras); + } extras.putFloat(EXTRAS_KEY_PLAYBACK_SPEED_COMPAT, playbackSpeed); @Nullable MediaItem currentMediaItem = getCurrentMediaItemWithCommandCheck(); if (currentMediaItem != null && !MediaItem.DEFAULT_MEDIA_ID.equals(currentMediaItem.mediaId)) { @@ -1092,7 +1108,9 @@ import java.util.List; } if (playerError != null) { builder.setErrorMessage( - PlaybackStateCompat.ERROR_CODE_UNKNOWN_ERROR, Util.castNonNull(playerError.getMessage())); + LegacyConversions.convertToLegacyErrorCode(playerError), playerError.getMessage()); + } else if (legacyError != null) { + builder.setErrorMessage(legacyError.code, legacyError.message); } return builder.build(); } diff --git a/libraries/session/src/main/java/androidx/media3/session/SessionError.java b/libraries/session/src/main/java/androidx/media3/session/SessionError.java index 5bca0ac39e..27ab62f562 100644 --- a/libraries/session/src/main/java/androidx/media3/session/SessionError.java +++ b/libraries/session/src/main/java/androidx/media3/session/SessionError.java @@ -46,26 +46,32 @@ public final class SessionError { @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_DISCONNECTED, ERROR_SESSION_AUTHENTICATION_EXPIRED, ERROR_SESSION_PREMIUM_ACCOUNT_REQUIRED, ERROR_SESSION_CONCURRENT_STREAM_LIMIT, + ERROR_SESSION_CONTENT_ALREADY_PLAYING, + ERROR_SESSION_END_OF_PLAYLIST, ERROR_SESSION_PARENTAL_CONTROL_RESTRICTED, ERROR_SESSION_NOT_AVAILABLE_IN_REGION, ERROR_SESSION_SKIP_LIMIT_REACHED, - ERROR_SESSION_SETUP_REQUIRED + ERROR_SESSION_SETUP_REQUIRED, + INFO_CANCELLED }) public @interface Code {} - /** Info code representing that the command is skipped. */ - public static final int INFO_SKIPPED = 1; + // Info codes (> 0). + + /** Info code representing that the command was cancelled. */ + public static final int INFO_CANCELLED = 1; + + // Error codes (< 0). /** Error code representing that the command is ended with an unknown error. */ public static final int ERROR_UNKNOWN = -1; @@ -74,49 +80,106 @@ public final class SessionError { * 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; + public static final int ERROR_INVALID_STATE = PlaybackException.ERROR_CODE_INVALID_STATE; /** Error code representing that an argument is illegal. */ - public static final int ERROR_BAD_VALUE = -3; + public static final int ERROR_BAD_VALUE = PlaybackException.ERROR_CODE_BAD_VALUE; /** Error code representing that the command is not allowed. */ - public static final int ERROR_PERMISSION_DENIED = -4; + public static final int ERROR_PERMISSION_DENIED = PlaybackException.ERROR_CODE_PERMISSION_DENIED; /** 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; + public static final int ERROR_NOT_SUPPORTED = PlaybackException.ERROR_CODE_NOT_SUPPORTED; /** Error code representing that the session and controller were disconnected. */ - public static final int ERROR_SESSION_DISCONNECTED = -100; + public static final int ERROR_SESSION_DISCONNECTED = PlaybackException.ERROR_CODE_DISCONNECTED; /** Error code representing that the authentication has expired. */ - public static final int ERROR_SESSION_AUTHENTICATION_EXPIRED = -102; + public static final int ERROR_SESSION_AUTHENTICATION_EXPIRED = + PlaybackException.ERROR_CODE_AUTHENTICATION_EXPIRED; /** Error code representing that a premium account is required. */ - public static final int ERROR_SESSION_PREMIUM_ACCOUNT_REQUIRED = -103; + public static final int ERROR_SESSION_PREMIUM_ACCOUNT_REQUIRED = + PlaybackException.ERROR_CODE_PREMIUM_ACCOUNT_REQUIRED; /** Error code representing that too many concurrent streams are detected. */ - public static final int ERROR_SESSION_CONCURRENT_STREAM_LIMIT = -104; + public static final int ERROR_SESSION_CONCURRENT_STREAM_LIMIT = + PlaybackException.ERROR_CODE_CONCURRENT_STREAM_LIMIT; /** Error code representing that the content is blocked due to parental controls. */ - public static final int ERROR_SESSION_PARENTAL_CONTROL_RESTRICTED = -105; + public static final int ERROR_SESSION_PARENTAL_CONTROL_RESTRICTED = + PlaybackException.ERROR_CODE_PARENTAL_CONTROL_RESTRICTED; /** Error code representing that the content is blocked due to being regionally unavailable. */ - public static final int ERROR_SESSION_NOT_AVAILABLE_IN_REGION = -106; + public static final int ERROR_SESSION_NOT_AVAILABLE_IN_REGION = + PlaybackException.ERROR_CODE_NOT_AVAILABLE_IN_REGION; /** * 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; + public static final int ERROR_SESSION_SKIP_LIMIT_REACHED = + PlaybackException.ERROR_CODE_SKIP_LIMIT_REACHED; /** Error code representing that the session needs user's manual intervention. */ - public static final int ERROR_SESSION_SETUP_REQUIRED = -108; + public static final int ERROR_SESSION_SETUP_REQUIRED = + PlaybackException.ERROR_CODE_SETUP_REQUIRED; + + /** Error code representing that navigation failed because the the playlist was exhausted. */ + public static final int ERROR_SESSION_END_OF_PLAYLIST = + PlaybackException.ERROR_CODE_END_OF_PLAYLIST; + + /** Error code representing that the requested content is already playing. */ + public static final int ERROR_SESSION_CONTENT_ALREADY_PLAYING = + PlaybackException.ERROR_CODE_CONTENT_ALREADY_PLAYING; /** Default error message. Only used by deprecated methods and for backwards compatibility. */ - public static final String DEFAULT_ERROR_MESSAGE = "no error message provided"; + /* package */ static final String DEFAULT_ERROR_MESSAGE = "no error message provided"; + + /** Returns the name of a given error code. */ + public static String getErrorCodeName(@Code int errorCode) { + switch (errorCode) { + case ERROR_UNKNOWN: + return "ERROR_UNKNOWN"; + case ERROR_INVALID_STATE: + return "ERROR_INVALID_STATE"; + case ERROR_BAD_VALUE: + return "ERROR_BAD_VALUE"; + case ERROR_PERMISSION_DENIED: + return "ERROR_PERMISSION_DENIED"; + case ERROR_IO: + return "ERROR_IO"; + case ERROR_NOT_SUPPORTED: + return "ERROR_NOT_SUPPORTED"; + case ERROR_SESSION_DISCONNECTED: + return "ERROR_SESSION_DISCONNECTED"; + case ERROR_SESSION_AUTHENTICATION_EXPIRED: + return "ERROR_SESSION_AUTHENTICATION_EXPIRED"; + case ERROR_SESSION_PREMIUM_ACCOUNT_REQUIRED: + return "ERROR_SESSION_PREMIUM_ACCOUNT_REQUIRED"; + case ERROR_SESSION_CONCURRENT_STREAM_LIMIT: + return "ERROR_SESSION_CONCURRENT_STREAM_LIMIT"; + case ERROR_SESSION_CONTENT_ALREADY_PLAYING: + return "ERROR_SESSION_CONTENT_ALREADY_PLAYING"; + case ERROR_SESSION_END_OF_PLAYLIST: + return "ERROR_SESSION_END_OF_PLAYLIST"; + case ERROR_SESSION_PARENTAL_CONTROL_RESTRICTED: + return "ERROR_SESSION_PARENTAL_CONTROL_RESTRICTED"; + case ERROR_SESSION_NOT_AVAILABLE_IN_REGION: + return "ERROR_SESSION_NOT_AVAILABLE_IN_REGION"; + case ERROR_SESSION_SKIP_LIMIT_REACHED: + return "ERROR_SESSION_SKIP_LIMIT_REACHED"; + case ERROR_SESSION_SETUP_REQUIRED: + return "ERROR_SESSION_SETUP_REQUIRED"; + case INFO_CANCELLED: + return "INFO_CANCELLED"; + default: + return "invalid error code"; + } + } public @SessionError.Code int code; public String message; @@ -142,7 +205,7 @@ public final class SessionError { * @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); + Assertions.checkArgument(code < 0 || code == INFO_CANCELLED); this.code = code; this.message = message; this.extras = 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 a739c51014..ffaeac9345 100644 --- a/libraries/session/src/main/java/androidx/media3/session/SessionResult.java +++ b/libraries/session/src/main/java/androidx/media3/session/SessionResult.java @@ -57,7 +57,7 @@ public final class SessionResult { @Target(TYPE_USE) @IntDef({ RESULT_SUCCESS, - SessionError.INFO_SKIPPED, + SessionError.INFO_CANCELLED, SessionError.ERROR_UNKNOWN, SessionError.ERROR_INVALID_STATE, SessionError.ERROR_BAD_VALUE, @@ -86,7 +86,7 @@ public final class SessionResult { 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; + public static final int RESULT_INFO_SKIPPED = SessionError.INFO_CANCELLED; /** Result code representing that the command is ended with an unknown error. */ public static final int RESULT_ERROR_UNKNOWN = SessionError.ERROR_UNKNOWN; diff --git a/libraries/session/src/main/java/androidx/media3/session/legacy/PlaybackStateCompat.java b/libraries/session/src/main/java/androidx/media3/session/legacy/PlaybackStateCompat.java index db371d82ea..7395292926 100644 --- a/libraries/session/src/main/java/androidx/media3/session/legacy/PlaybackStateCompat.java +++ b/libraries/session/src/main/java/androidx/media3/session/legacy/PlaybackStateCompat.java @@ -443,6 +443,7 @@ public final class PlaybackStateCompat implements Parcelable { */ public static final int SHUFFLE_MODE_GROUP = 2; + /** Supported error codes. */ @IntDef({ ERROR_CODE_UNKNOWN_ERROR, ERROR_CODE_APP_ERROR, @@ -458,7 +459,7 @@ public final class PlaybackStateCompat implements Parcelable { ERROR_CODE_END_OF_QUEUE }) @Retention(RetentionPolicy.SOURCE) - private @interface ErrorCode {} + public @interface ErrorCode {} /** * This is the default error code and indicates that none of the other error codes applies. The diff --git a/libraries/test_session_common/src/main/aidl/androidx/media3/test/session/common/IRemoteMediaSession.aidl b/libraries/test_session_common/src/main/aidl/androidx/media3/test/session/common/IRemoteMediaSession.aidl index 9caf8212a2..f749757953 100644 --- a/libraries/test_session_common/src/main/aidl/androidx/media3/test/session/common/IRemoteMediaSession.aidl +++ b/libraries/test_session_common/src/main/aidl/androidx/media3/test/session/common/IRemoteMediaSession.aidl @@ -34,7 +34,7 @@ interface IRemoteMediaSession { void setCustomLayout(String sessionId, in List layout); void setSessionExtras(String sessionId, in Bundle extras); void setSessionExtrasForController(String sessionId, in String controllerKey, in Bundle extras); - void sendError(String sessionId, String controllerKey, int errorCode, int errorMessageResId, in Bundle errorExtras); + void sendError(String sessionId, String controllerKey, in Bundle SessionError); void setSessionActivity(String sessionId, in PendingIntent sessionActivity); // Player Methods diff --git a/libraries/test_session_common/src/main/aidl/androidx/media3/test/session/common/IRemoteMediaSessionCompat.aidl b/libraries/test_session_common/src/main/aidl/androidx/media3/test/session/common/IRemoteMediaSessionCompat.aidl index d9f1ce736d..b51384d961 100644 --- a/libraries/test_session_common/src/main/aidl/androidx/media3/test/session/common/IRemoteMediaSessionCompat.aidl +++ b/libraries/test_session_common/src/main/aidl/androidx/media3/test/session/common/IRemoteMediaSessionCompat.aidl @@ -42,6 +42,5 @@ interface IRemoteMediaSessionCompat { void sendSessionEvent(String sessionTag, String event, in Bundle extras); void setCaptioningEnabled(String sessionTag, boolean enabled); void setSessionExtras(String sessionTag, in Bundle extras); - void sendError(String sessionTag, int errorCode, int errorMessageIntRes, in Bundle errorExtras); int getCallbackMethodCount(String sessionTag, String methodName); } 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 9628056d4e..e1f3d19711 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 @@ -40,6 +40,8 @@ public class MediaBrowserConstants { 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_NON_FATAL = + "parent_auth_expired_error_non_fatal"; public static final String PARENT_ID_AUTH_EXPIRED_ERROR_KEY_ERROR_RESOLUTION_ACTION_LABEL = "parent_auth_expired_error_label"; 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 56c4a6de26..acb63444c1 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 @@ -25,6 +25,10 @@ public class MediaBrowserServiceCompatConstants { public static final String TEST_GET_CHILDREN = "getChildren_correctMetadataExtras"; public static final String TEST_GET_CHILDREN_WITH_NULL_LIST = "onChildrenChanged_withNullChildrenListInLegacyService_convertedToSessionError"; + public static final String TEST_GET_CHILDREN_FATAL_AUTHENTICATION_ERROR = + "getLibraryRoot_fatalAuthenticationError_receivesPlaybackException"; + public static final String TEST_GET_CHILDREN_NON_FATAL_AUTHENTICATION_ERROR = + "getLibraryRoot_nonFatalAuthenticationError_receivesPlaybackException"; 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 52f87090ea..9d6b320183 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 @@ -395,13 +395,13 @@ public class MediaBrowserCompatWithMediaLibraryServiceTest assertGetChildrenAuthenticationRequired(PARENT_ID_AUTH_EXPIRED_ERROR_DEPRECATED); } - public void assertGetChildrenAuthenticationRequired(String testParentId) throws Exception { + public void assertGetChildrenAuthenticationRequired(String authExpiredParentId) throws Exception { connectAndWait(/* rootHints= */ Bundle.EMPTY); CountDownLatch errorLatch = new CountDownLatch(1); AtomicReference parentIdRefOnError = new AtomicReference<>(); browserCompat.subscribe( - testParentId, + authExpiredParentId, new SubscriptionCallback() { @Override public void onError(String parentId) { @@ -411,7 +411,7 @@ public class MediaBrowserCompatWithMediaLibraryServiceTest }); assertThat(errorLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(parentIdRefOnError.get()).isEqualTo(testParentId); + assertThat(parentIdRefOnError.get()).isEqualTo(authExpiredParentId); assertThat(firstPlaybackStateCompatReported.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); assertThat(lastReportedPlaybackStateCompat.getState()) .isEqualTo(PlaybackStateCompat.STATE_ERROR); 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 caa7cc5eab..e70b120904 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 @@ -259,8 +259,8 @@ public class MediaBrowserListenerTest extends MediaControllerListenerTest { } @Test - public void getChildren_errorLibraryResult() throws Exception { - String parentId = MediaBrowserConstants.PARENT_ID_ERROR; + public void getChildren_authExpiredErrorLibraryResult() throws Exception { + String parentId = MediaBrowserConstants.PARENT_ID_AUTH_EXPIRED_ERROR; MediaBrowser browser = createBrowser(); LibraryResult> result = @@ -272,7 +272,11 @@ public class MediaBrowserListenerTest extends MediaControllerListenerTest { 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.sessionError.extras.getString( + MediaConstants.EXTRAS_KEY_ERROR_RESOLUTION_ACTION_LABEL_COMPAT)) + .isEqualTo( + MediaBrowserConstants.PARENT_ID_AUTH_EXPIRED_ERROR_KEY_ERROR_RESOLUTION_ACTION_LABEL); 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 59d7357ca4..830a7122ec 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,8 @@ 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_FATAL_AUTHENTICATION_ERROR; +import static androidx.media3.test.session.common.MediaBrowserServiceCompatConstants.TEST_GET_CHILDREN_NON_FATAL_AUTHENTICATION_ERROR; 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; @@ -40,12 +42,16 @@ import android.os.Bundle; import androidx.annotation.Nullable; import androidx.media.MediaBrowserServiceCompat; import androidx.media3.common.MediaItem; +import androidx.media3.common.PlaybackException; +import androidx.media3.common.Player; import androidx.media3.session.MediaLibraryService.LibraryParams; import androidx.media3.test.session.common.HandlerThreadTestRule; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.LargeTest; import com.google.common.collect.ImmutableList; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import org.junit.After; @@ -264,4 +270,70 @@ public class MediaBrowserListenerWithMediaBrowserServiceCompatTest { assertThat(status).isEqualTo(expectedStatus); } } + + @Test + public void getChildren_fatalAuthenticationErrorOfLegacySessionApp_receivesPlaybackException() + throws Exception { + remoteService.setProxyForTest(TEST_GET_CHILDREN_FATAL_AUTHENTICATION_ERROR); + MediaBrowser browser = createBrowser(/* listener= */ null); + List playbackExceptions = new ArrayList<>(); + CountDownLatch playbackErrorLatch = new CountDownLatch(1); + browser.addListener( + new Player.Listener() { + @Override + public void onPlayerError(PlaybackException error) { + playbackExceptions.add(error); + playbackErrorLatch.countDown(); + } + }); + + LibraryResult> libraryResult = + threadTestRule + .getHandler() + .postAndSync( + () -> + browser.getChildren( + PARENT_ID, /* page= */ 4, /* pageSize= */ 10, /* params= */ null)) + .get(TIMEOUT_MS, MILLISECONDS); + + assertThat(playbackErrorLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(libraryResult.sessionError.code).isEqualTo(SessionError.ERROR_UNKNOWN); + assertThat(playbackExceptions).hasSize(1); + assertThat(playbackExceptions.get(0).errorCode) + .isEqualTo(PlaybackException.ERROR_CODE_AUTHENTICATION_EXPIRED); + } + + @Test + public void getChildren_nonFatalAuthenticationErrorOfLegacySessionApp_receivesSessionError() + throws Exception { + remoteService.setProxyForTest(TEST_GET_CHILDREN_NON_FATAL_AUTHENTICATION_ERROR); + List sessionErrors = new ArrayList<>(); + CountDownLatch sessionErrorLatch = new CountDownLatch(1); + MediaBrowser browser = + createBrowser( + /* listener= */ new MediaBrowser.Listener() { + @Override + public void onError(MediaController controller, SessionError sessionError) { + sessionErrors.add(sessionError); + sessionErrorLatch.countDown(); + } + }); + + LibraryResult> libraryResult = + threadTestRule + .getHandler() + .postAndSync( + () -> + browser.getChildren( + PARENT_ID, /* page= */ 4, /* pageSize= */ 10, /* params= */ null)) + .get(TIMEOUT_MS, MILLISECONDS); + + assertThat(sessionErrorLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(libraryResult.sessionError).isNull(); + assertThat(libraryResult.value).hasSize(1); + assertThat(libraryResult.value.get(0).mediaId).isEqualTo("mediaId"); + assertThat(sessionErrors).hasSize(1); + assertThat(sessionErrors.get(0).code) + .isEqualTo(PlaybackException.ERROR_CODE_AUTHENTICATION_EXPIRED); + } } diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerCompatCallbackWithMediaSessionTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerCompatCallbackWithMediaSessionTest.java index d9d5769b97..64b2bfaeeb 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerCompatCallbackWithMediaSessionTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerCompatCallbackWithMediaSessionTest.java @@ -184,7 +184,7 @@ public class MediaControllerCompatCallbackWithMediaSessionTest { new PlaybackException( /* message= */ "testremote", /* cause= */ null, - PlaybackException.ERROR_CODE_REMOTE_ERROR); + PlaybackException.ERROR_CODE_AUTHENTICATION_EXPIRED); Bundle playerConfig = new RemoteMediaSession.MockPlayerConfigBuilder().setPlayerError(testPlayerError).build(); session.setPlayer(playerConfig); @@ -201,7 +201,11 @@ public class MediaControllerCompatCallbackWithMediaSessionTest { handler); assertThat(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue(); - assertPlaybackStateCompatErrorEquals(controller.getPlaybackState(), testPlayerError); + PlaybackStateCompat state = controller.getPlaybackState(); + assertThat(state.getState()).isEqualTo(PlaybackStateCompat.STATE_ERROR); + assertThat(state.getErrorCode()) + .isEqualTo(PlaybackStateCompat.ERROR_CODE_AUTHENTICATION_EXPIRED); + assertThat(state.getErrorMessage().toString()).isEqualTo(testPlayerError.getMessage()); } @Test @@ -210,7 +214,7 @@ public class MediaControllerCompatCallbackWithMediaSessionTest { new PlaybackException( /* message= */ "player error", /* cause= */ null, - PlaybackException.ERROR_CODE_UNSPECIFIED); + PlaybackException.ERROR_CODE_AUTHENTICATION_EXPIRED); CountDownLatch latch = new CountDownLatch(1); AtomicReference playbackStateCompatRef = new AtomicReference<>(); @@ -227,7 +231,133 @@ public class MediaControllerCompatCallbackWithMediaSessionTest { session.getMockPlayer().notifyPlayerError(testPlayerError); assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); PlaybackStateCompat state = playbackStateCompatRef.get(); - assertPlaybackStateCompatErrorEquals(state, testPlayerError); + assertThat(state.getState()).isEqualTo(PlaybackStateCompat.STATE_ERROR); + assertThat(state.getErrorCode()) + .isEqualTo(PlaybackStateCompat.ERROR_CODE_AUTHENTICATION_EXPIRED); + assertThat(state.getErrorMessage().toString()).isEqualTo(testPlayerError.getMessage()); + } + + @Test + public void sendError_toAllControllers_onPlaybackStateChangedToErrorStateAndWithCorrectErrorData() + throws Exception { + CountDownLatch latch = new CountDownLatch(2); + List playbackStates = new ArrayList<>(); + MediaControllerCompat.Callback callback = + new MediaControllerCompat.Callback() { + @Override + public void onPlaybackStateChanged(PlaybackStateCompat state) { + playbackStates.add(state); + latch.countDown(); + } + }; + controllerCompat.registerCallback(callback, handler); + Bundle sessionExtras = new Bundle(); + sessionExtras.putString("initialKey", "initialValue"); + session.setSessionExtras(sessionExtras); + PlaybackStateCompat initialPlaybackStateCompat = controllerCompat.getPlaybackState(); + Bundle errorBundle = new Bundle(); + errorBundle.putInt("errorKey", 99); + + session.sendError( + /* controllerKey= */ null, + new SessionError( + /* code= */ SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED, + /* message= */ ApplicationProvider.getApplicationContext() + .getString(R.string.authentication_required), + errorBundle)); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(playbackStates).hasSize(3); + + // Skip the playback state from the first setSessionExtras() call. + PlaybackStateCompat errorPlaybackStateCompat = playbackStates.get(1); + assertThat(errorPlaybackStateCompat.getState()).isNotEqualTo(PlaybackStateCompat.STATE_ERROR); + assertThat(errorPlaybackStateCompat.getState()) + .isEqualTo(initialPlaybackStateCompat.getState()); + assertThat(errorPlaybackStateCompat.getErrorCode()) + .isEqualTo(PlaybackStateCompat.ERROR_CODE_AUTHENTICATION_EXPIRED); + assertThat(errorPlaybackStateCompat.getErrorMessage().toString()) + .isEqualTo(context.getString(R.string.authentication_required)); + assertThat(errorPlaybackStateCompat.getExtras()).hasSize(3); + assertThat(errorPlaybackStateCompat.getExtras()).integer("errorKey").isEqualTo(99); + assertThat(errorPlaybackStateCompat.getExtras()).string("initialKey").isEqualTo("initialValue"); + assertThat(errorPlaybackStateCompat.getExtras()).containsKey("EXO_SPEED"); + + PlaybackStateCompat resolvedPlaybackStateCompat = playbackStates.get(2); + assertThat(resolvedPlaybackStateCompat.getState()) + .isEqualTo(initialPlaybackStateCompat.getState()); + assertThat(resolvedPlaybackStateCompat.getErrorCode()) + .isEqualTo(initialPlaybackStateCompat.getErrorCode()); + assertThat(resolvedPlaybackStateCompat.getErrorMessage()).isNull(); + assertThat(resolvedPlaybackStateCompat.getActions()) + .isEqualTo(initialPlaybackStateCompat.getActions()); + assertThat( + TestUtils.equals( + resolvedPlaybackStateCompat.getExtras(), initialPlaybackStateCompat.getExtras())) + .isTrue(); + } + + @Test + public void + sendError_toMediaNotificationController_onPlaybackStateChangedToErrorStateAndWithCorrectErrorData() + throws Exception { + CountDownLatch latch = new CountDownLatch(2); + List playbackStates = new ArrayList<>(); + MediaControllerCompat.Callback callback = + new MediaControllerCompat.Callback() { + @Override + public void onPlaybackStateChanged(PlaybackStateCompat state) { + playbackStates.add(state); + latch.countDown(); + } + }; + controllerCompat.registerCallback(callback, handler); + Bundle sessionExtras = new Bundle(); + sessionExtras.putString("initialKey", "initialValue"); + session.setSessionExtras(/* controllerKey= */ NOTIFICATION_CONTROLLER_KEY, sessionExtras); + PlaybackStateCompat initialPlaybackStateCompat = controllerCompat.getPlaybackState(); + Bundle errorBundle = new Bundle(); + errorBundle.putInt("errorKey", 99); + + session.sendError( + /* controllerKey= */ NOTIFICATION_CONTROLLER_KEY, + new SessionError( + SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED, + /* message= */ ApplicationProvider.getApplicationContext() + .getString(R.string.authentication_required), + errorBundle)); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(playbackStates).hasSize(3); + + // Skip the playback state from the first setSessionExtras() call. + PlaybackStateCompat errorPlaybackStateCompat = playbackStates.get(1); + assertThat(errorPlaybackStateCompat.getState()) + .isEqualTo(initialPlaybackStateCompat.getState()); + assertThat(errorPlaybackStateCompat.getState()).isNotEqualTo(PlaybackStateCompat.STATE_ERROR); + assertThat(errorPlaybackStateCompat.getErrorCode()) + .isEqualTo(PlaybackStateCompat.ERROR_CODE_AUTHENTICATION_EXPIRED); + assertThat(errorPlaybackStateCompat.getErrorMessage().toString()) + .isEqualTo(context.getString(R.string.authentication_required)); + assertThat(errorPlaybackStateCompat.getActions()) + .isEqualTo(initialPlaybackStateCompat.getActions()); + assertThat(errorPlaybackStateCompat.getExtras()).hasSize(3); + assertThat(errorPlaybackStateCompat.getExtras()).string("initialKey").isEqualTo("initialValue"); + assertThat(errorPlaybackStateCompat.getExtras()).integer("errorKey").isEqualTo(99); + assertThat(errorPlaybackStateCompat.getExtras()).containsKey("EXO_SPEED"); + + PlaybackStateCompat resolvedPlaybackStateCompat = playbackStates.get(2); + assertThat(resolvedPlaybackStateCompat.getState()) + .isEqualTo(initialPlaybackStateCompat.getState()); + assertThat(resolvedPlaybackStateCompat.getErrorCode()) + .isEqualTo(initialPlaybackStateCompat.getErrorCode()); + assertThat(resolvedPlaybackStateCompat.getErrorMessage()).isNull(); + assertThat(resolvedPlaybackStateCompat.getActions()) + .isEqualTo(initialPlaybackStateCompat.getActions()); + assertThat( + TestUtils.equals( + resolvedPlaybackStateCompat.getExtras(), initialPlaybackStateCompat.getExtras())) + .isTrue(); } @Test @@ -1096,118 +1226,6 @@ public class MediaControllerCompatCallbackWithMediaSessionTest { assertThat(playbackStateExtrasFromController.get()).string("key-0").isEqualTo("value-0"); } - @Test - public void sendError_toAllControllers_onPlaybackStateChangedToErrorStateAndWithCorrectErrorData() - throws Exception { - CountDownLatch latch = new CountDownLatch(3); - List playbackStates = new ArrayList<>(); - MediaControllerCompat.Callback callback = - new MediaControllerCompat.Callback() { - @Override - public void onPlaybackStateChanged(PlaybackStateCompat state) { - playbackStates.add(state); - latch.countDown(); - } - }; - controllerCompat.registerCallback(callback, handler); - Bundle sessionExtras = new Bundle(); - sessionExtras.putString("initialKey", "initialValue"); - session.setSessionExtras(sessionExtras); - PlaybackStateCompat initialPlaybackStateCompat = controllerCompat.getPlaybackState(); - Bundle errorBundle = new Bundle(); - errorBundle.putInt("errorKey", 99); - - session.sendError( - /* controllerKey= */ null, - /* errorCode= */ 1, - R.string.authentication_required, - errorBundle); - - assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(playbackStates).hasSize(3); - - // Skip the playback state from the first setSessionExtras() call. - PlaybackStateCompat errorPlaybackStateCompat = playbackStates.get(1); - assertThat(errorPlaybackStateCompat.getState()).isEqualTo(PlaybackStateCompat.STATE_ERROR); - assertThat(errorPlaybackStateCompat.getErrorCode()).isEqualTo(1); - assertThat(errorPlaybackStateCompat.getErrorMessage().toString()) - .isEqualTo(context.getString(R.string.authentication_required)); - assertThat(errorPlaybackStateCompat.getExtras()).hasSize(2); - assertThat(errorPlaybackStateCompat.getExtras()).integer("errorKey").isEqualTo(99); - assertThat(errorPlaybackStateCompat.getExtras()).string("initialKey").isEqualTo("initialValue"); - - PlaybackStateCompat resolvedPlaybackStateCompat = playbackStates.get(2); - assertThat(resolvedPlaybackStateCompat.getState()) - .isEqualTo(initialPlaybackStateCompat.getState()); - assertThat(resolvedPlaybackStateCompat.getErrorCode()) - .isEqualTo(initialPlaybackStateCompat.getErrorCode()); - assertThat(resolvedPlaybackStateCompat.getErrorMessage()).isNull(); - assertThat(resolvedPlaybackStateCompat.getActions()) - .isEqualTo(initialPlaybackStateCompat.getActions()); - assertThat(resolvedPlaybackStateCompat.getExtras()) - .hasSize(initialPlaybackStateCompat.getExtras().size()); - assertThat(resolvedPlaybackStateCompat.getExtras()) - .string("initialKey") - .isEqualTo(initialPlaybackStateCompat.getExtras().getString("initialKey")); - } - - @Test - public void - sendError_toMediaNotificationControllers_onPlaybackStateChangedToErrorStateAndWithCorrectErrorData() - throws Exception { - CountDownLatch latch = new CountDownLatch(3); - List playbackStates = new ArrayList<>(); - MediaControllerCompat.Callback callback = - new MediaControllerCompat.Callback() { - @Override - public void onPlaybackStateChanged(PlaybackStateCompat state) { - playbackStates.add(state); - latch.countDown(); - } - }; - controllerCompat.registerCallback(callback, handler); - Bundle sessionExtras = new Bundle(); - sessionExtras.putString("initialKey", "initialValue"); - session.setSessionExtras(/* controllerKey= */ NOTIFICATION_CONTROLLER_KEY, sessionExtras); - PlaybackStateCompat initialPlaybackStateCompat = controllerCompat.getPlaybackState(); - - Bundle errorBundle = new Bundle(); - errorBundle.putInt("errorKey", 99); - session.sendError( - /* controllerKey= */ NOTIFICATION_CONTROLLER_KEY, - /* errorCode= */ 1, - R.string.authentication_required, - errorBundle); - - assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(playbackStates).hasSize(3); - - // Skip the playback state from the first setSessionExtras() call. - PlaybackStateCompat errorPlaybackStateCompat = playbackStates.get(1); - assertThat(errorPlaybackStateCompat.getState()).isEqualTo(PlaybackStateCompat.STATE_ERROR); - assertThat(errorPlaybackStateCompat.getErrorCode()).isEqualTo(1); - assertThat(errorPlaybackStateCompat.getErrorMessage().toString()) - .isEqualTo(context.getString(R.string.authentication_required)); - assertThat(errorPlaybackStateCompat.getActions()).isEqualTo(0); - assertThat(errorPlaybackStateCompat.getExtras()).hasSize(2); - assertThat(errorPlaybackStateCompat.getExtras()).string("initialKey").isEqualTo("initialValue"); - assertThat(errorPlaybackStateCompat.getExtras()).integer("errorKey").isEqualTo(99); - - PlaybackStateCompat resolvedPlaybackStateCompat = playbackStates.get(2); - assertThat(resolvedPlaybackStateCompat.getState()) - .isEqualTo(initialPlaybackStateCompat.getState()); - assertThat(resolvedPlaybackStateCompat.getErrorCode()) - .isEqualTo(initialPlaybackStateCompat.getErrorCode()); - assertThat(resolvedPlaybackStateCompat.getErrorMessage()).isNull(); - assertThat(resolvedPlaybackStateCompat.getActions()) - .isEqualTo(initialPlaybackStateCompat.getActions()); - assertThat(resolvedPlaybackStateCompat.getExtras()) - .hasSize(initialPlaybackStateCompat.getExtras().size()); - assertThat(resolvedPlaybackStateCompat.getExtras()) - .string("initialKey") - .isEqualTo(initialPlaybackStateCompat.getExtras().getString("initialKey")); - } - @Test public void setSessionActivity_changedWhenReceivedWithSetter() throws Exception { Intent intent = new Intent(context, SurfaceActivity.class); @@ -1609,11 +1627,4 @@ public class MediaControllerCompatCallbackWithMediaSessionTest { assertThat(targetVolumeNotified.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); assertThat(controllerCompat.getPlaybackInfo().getCurrentVolume()).isEqualTo(targetVolume); } - - private static void assertPlaybackStateCompatErrorEquals( - PlaybackStateCompat state, PlaybackException playerError) { - assertThat(state.getState()).isEqualTo(PlaybackStateCompat.STATE_ERROR); - assertThat(state.getErrorCode()).isEqualTo(PlaybackStateCompat.ERROR_CODE_UNKNOWN_ERROR); - assertThat(state.getErrorMessage().toString()).isEqualTo(playerError.getMessage()); - } } diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerTest.java index debdf3a2dc..8ccc47d7a8 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerTest.java @@ -2638,9 +2638,7 @@ public class MediaControllerListenerTest { public void onError_sendErrorToAllAndToSingleController_correctErrorDataReported() throws Exception { CountDownLatch errorLatch = new CountDownLatch(/* count= */ 3); - List errorCodes1 = new ArrayList<>(); - List errorMessages1 = new ArrayList<>(); - List errorExtras1 = new ArrayList<>(); + List sessionErrors1 = new ArrayList<>(); Bundle connectionHints1 = new Bundle(); connectionHints1.putString(KEY_CONTROLLER, "ctrl-1"); controllerTestRule.createController( @@ -2648,17 +2646,12 @@ public class MediaControllerListenerTest { connectionHints1, new MediaController.Listener() { @Override - public void onError( - MediaController controller, int errorCode, String errorMessage, Bundle extras) { - errorCodes1.add(errorCode); - errorMessages1.add(errorMessage); - errorExtras1.add(extras); + public void onError(MediaController controller, SessionError sessionError) { + sessionErrors1.add(sessionError); errorLatch.countDown(); } }); - List errorCodes2 = new ArrayList<>(); - List errorMessages2 = new ArrayList<>(); - List errorExtras2 = new ArrayList<>(); + List sessionErrors2 = new ArrayList<>(); Bundle connectionHints2 = new Bundle(); connectionHints2.putString(KEY_CONTROLLER, "ctrl-2"); controllerTestRule.createController( @@ -2666,44 +2659,38 @@ public class MediaControllerListenerTest { connectionHints2, new MediaController.Listener() { @Override - public void onError( - MediaController controller, int errorCode, String errorMessage, Bundle extras) { - errorCodes2.add(errorCode); - errorMessages2.add(errorMessage); - errorExtras2.add(extras); + public void onError(MediaController controller, SessionError sessionError) { + sessionErrors2.add(sessionError); errorLatch.countDown(); } }); Bundle errorExtra1 = new Bundle(); errorExtra1.putInt("intKey", 1); + SessionError error1 = + new SessionError( + /* code= */ SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED, + ApplicationProvider.getApplicationContext().getString(R.string.authentication_required), + errorExtra1); Bundle errorExtra2 = new Bundle(); errorExtra2.putInt("intKey", 2); + SessionError error2 = + new SessionError( + SessionError.ERROR_SESSION_CONCURRENT_STREAM_LIMIT, + ApplicationProvider.getApplicationContext() + .getString(R.string.default_notification_channel_name), + errorExtra2); - remoteSession.sendError( - /* controllerKey= */ null, - /* errorCode= */ 1, - R.string.authentication_required, - errorExtra1); - remoteSession.sendError( - /* controllerKey= */ "ctrl-2", - /* errorCode= */ 2, - R.string.default_notification_channel_name, - errorExtra2); + remoteSession.sendError(/* controllerKey= */ null, error1); + remoteSession.sendError(/* controllerKey= */ "ctrl-2", error2); assertThat(errorLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(errorCodes1).containsExactly(1); - assertThat(errorMessages1).containsExactly(context.getString(R.string.authentication_required)); - assertThat(TestUtils.equals(errorExtras1.get(0), errorExtra1)).isTrue(); - assertThat(errorExtras1).hasSize(1); - assertThat(errorCodes2).containsExactly(1, 2).inOrder(); - assertThat(errorMessages2) - .containsExactly( - context.getString(R.string.authentication_required), - context.getString(R.string.default_notification_channel_name)) - .inOrder(); - assertThat(TestUtils.equals(errorExtras2.get(0), errorExtra1)).isTrue(); - assertThat(TestUtils.equals(errorExtras2.get(1), errorExtra2)).isTrue(); - assertThat(errorExtras2).hasSize(2); + assertThat(sessionErrors1).containsExactly(error1); + assertThat(sessionErrors1.get(0).extras.getInt("intKey")).isEqualTo(1); + assertThat(sessionErrors2).containsExactly(error1, error2).inOrder(); + assertThat(sessionErrors2.get(0).extras.getInt("intKey")).isEqualTo(1); + assertThat(sessionErrors2.get(0).extras.size()).isEqualTo(1); + assertThat(sessionErrors2.get(1).extras.getInt("intKey")).isEqualTo(2); + assertThat(sessionErrors2.get(1).extras.size()).isEqualTo(1); } @Test diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerWithMediaSessionCompatTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerWithMediaSessionCompatTest.java index 228b4aa3c2..3a9f11a64b 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerWithMediaSessionCompatTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerWithMediaSessionCompatTest.java @@ -190,6 +190,82 @@ public class MediaControllerListenerWithMediaSessionCompatTest { .inOrder(); } + @Test + public void setPlaybackState_fatalError_callsOnPlayerErrorWithCodeMessageAndExtras() + throws Exception { + MediaController controller = + controllerTestRule.createController(session.getSessionToken(), /* listener= */ null); + CountDownLatch fatalErrorLatch = new CountDownLatch(/* count= */ 1); + List fatalErrorExceptions = new ArrayList<>(); + Bundle fatalErrorExtras = new Bundle(); + fatalErrorExtras.putString("key-2", "value-2"); + controller.addListener( + new Player.Listener() { + @Override + public void onPlayerError(PlaybackException error) { + fatalErrorExceptions.add(error); + fatalErrorLatch.countDown(); + } + }); + + session.setPlaybackState( + new PlaybackStateCompat.Builder() + .setState( + PlaybackStateCompat.STATE_ERROR, /* position= */ 0L, /* playbackSpeed= */ 1.0f) + .setErrorMessage( + PlaybackStateCompat.ERROR_CODE_AUTHENTICATION_EXPIRED, + ApplicationProvider.getApplicationContext() + .getString(R.string.authentication_required)) + .setExtras(fatalErrorExtras) + .build()); + + assertThat(fatalErrorLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(fatalErrorExceptions).hasSize(1); + assertThat(fatalErrorExceptions.get(0)) + .hasMessageThat() + .isEqualTo(context.getString(R.string.authentication_required)); + assertThat(fatalErrorExceptions.get(0).errorCode) + .isEqualTo(PlaybackException.ERROR_CODE_AUTHENTICATION_EXPIRED); + assertThat(TestUtils.equals(fatalErrorExceptions.get(0).extras, fatalErrorExtras)).isTrue(); + } + + @Test + public void setPlaybackState_nonFatalError_callsOnErrorWithCodeMessageAndExtras() + throws Exception { + CountDownLatch nonFatalErrorLatch = new CountDownLatch(/* count= */ 1); + List sessionErrors = new ArrayList<>(); + Bundle nonFatalErrorExtra = new Bundle(); + nonFatalErrorExtra.putString("key-1", "value-1"); + controllerTestRule.createController( + session.getSessionToken(), + new MediaController.Listener() { + @Override + public void onError(MediaController controller, SessionError sessionError) { + sessionErrors.add(sessionError); + nonFatalErrorLatch.countDown(); + } + }); + + session.setPlaybackState( + new PlaybackStateCompat.Builder() + .setState( + PlaybackStateCompat.STATE_PLAYING, + PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, + /* playbackSpeed= */ .0f) + .setErrorMessage( + PlaybackStateCompat.ERROR_CODE_APP_ERROR, + ApplicationProvider.getApplicationContext() + .getString(R.string.default_notification_channel_name)) + .setExtras(nonFatalErrorExtra) + .build()); + + assertThat(nonFatalErrorLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(sessionErrors).hasSize(1); + assertThat(sessionErrors.get(0).message) + .isEqualTo(context.getString(R.string.default_notification_channel_name)); + assertThat(TestUtils.equals(sessionErrors.get(0).extras, nonFatalErrorExtra)).isTrue(); + } + @Test public void setSessionExtras_onExtrasChangedCalled() throws Exception { Bundle sessionExtras = new Bundle(); @@ -230,72 +306,6 @@ public class MediaControllerListenerWithMediaSessionCompatTest { .isTrue(); } - @Test - public void sendError_fatalAndNonFatalErrorCodes_callsCorrectCallbackWithErrorData() - throws Exception { - CountDownLatch nonFatalErrorLatch = new CountDownLatch(/* count= */ 1); - List nonFatalErrorCodes = new ArrayList<>(); - List nonFatalErrorMessages = new ArrayList<>(); - List nonFatalErrorExtras = new ArrayList<>(); - Bundle nonFatalErrorExtra = new Bundle(); - nonFatalErrorExtra.putString("key-1", "value-1"); - MediaController controller = - controllerTestRule.createController( - session.getSessionToken(), - new MediaController.Listener() { - @Override - public void onError( - MediaController controller, - int errorCode, - String errorMessage, - Bundle errorExtra) { - nonFatalErrorCodes.add(errorCode); - nonFatalErrorMessages.add(errorMessage); - nonFatalErrorExtras.add(errorExtra); - nonFatalErrorLatch.countDown(); - } - }); - CountDownLatch fatalErrorLatch = new CountDownLatch(/* count= */ 1); - List fatalErrorExceptions = new ArrayList<>(); - Bundle fatalErrorExtra = new Bundle(); - fatalErrorExtra.putString("key-2", "value-2"); - controller.addListener( - new Player.Listener() { - @Override - public void onPlayerError(PlaybackException error) { - fatalErrorExceptions.add(error); - fatalErrorLatch.countDown(); - } - }); - - // Send fatal errors code. - session.sendError( - /* errorCode= */ PlaybackStateCompat.ERROR_CODE_AUTHENTICATION_EXPIRED, - R.string.authentication_required, - fatalErrorExtra); - assertThat(fatalErrorLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - // Send non-fatal error code. - session.sendError( - /* errorCode= */ PlaybackStateCompat.ERROR_CODE_APP_ERROR, - R.string.default_notification_channel_name, - nonFatalErrorExtra); - - assertThat(nonFatalErrorLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(nonFatalErrorCodes).containsExactly(PlaybackStateCompat.ERROR_CODE_APP_ERROR); - assertThat(nonFatalErrorMessages) - .containsExactly(context.getString(R.string.default_notification_channel_name)); - assertThat(TestUtils.equals(nonFatalErrorExtras.get(0), nonFatalErrorExtra)).isTrue(); - assertThat(fatalErrorExceptions).hasSize(1); - assertThat(fatalErrorExceptions.get(0)) - .hasMessageThat() - .isEqualTo( - context.getString(R.string.authentication_required) - + ", code=" - + PlaybackStateCompat.ERROR_CODE_AUTHENTICATION_EXPIRED); - assertThat(fatalErrorExceptions.get(0).errorCode) - .isEqualTo(PlaybackException.ERROR_CODE_REMOTE_ERROR); - } - @Test public void onPlaylistMetadataChanged() throws Exception { MediaController controller = controllerTestRule.createController(session.getSessionToken()); diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerWithMediaSessionCompatTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerWithMediaSessionCompatTest.java index dad08d970f..e9ffd8639f 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerWithMediaSessionCompatTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerWithMediaSessionCompatTest.java @@ -24,7 +24,6 @@ import static android.support.v4.media.MediaMetadataCompat.METADATA_KEY_DISPLAY_ import static android.support.v4.media.MediaMetadataCompat.METADATA_KEY_DURATION; import static android.support.v4.media.MediaMetadataCompat.METADATA_KEY_MEDIA_ID; import static android.support.v4.media.MediaMetadataCompat.METADATA_KEY_MEDIA_URI; -import static androidx.media3.common.PlaybackException.ERROR_CODE_REMOTE_ERROR; import static androidx.media3.common.Player.MEDIA_ITEM_TRANSITION_REASON_AUTO; import static androidx.media3.common.Player.STATE_BUFFERING; import static androidx.media3.common.Player.STATE_READY; @@ -64,7 +63,6 @@ import androidx.media.VolumeProviderCompat; import androidx.media3.common.DeviceInfo; import androidx.media3.common.MediaItem; import androidx.media3.common.MediaMetadata; -import androidx.media3.common.PlaybackException; import androidx.media3.common.PlaybackParameters; import androidx.media3.common.Player; import androidx.media3.common.Player.DiscontinuityReason; @@ -1505,39 +1503,6 @@ public class MediaControllerWithMediaSessionCompatTest { assertThat(playbackParametersFromGetterRef.get().speed).isEqualTo(testSpeed); } - @Test - public void setPlaybackState_withError_notifiesOnPlayerErrorChanged() throws Exception { - String testErrorMessage = "testErrorMessage"; - int testErrorCode = PlaybackStateCompat.ERROR_CODE_UNKNOWN_ERROR; // 0 - String testConvertedErrorMessage = "testErrorMessage, code=0"; - MediaController controller = controllerTestRule.createController(session.getSessionToken()); - CountDownLatch latch = new CountDownLatch(1); - AtomicReference errorFromParamRef = new AtomicReference<>(); - AtomicReference errorFromGetterRef = new AtomicReference<>(); - Player.Listener listener = - new Player.Listener() { - @Override - public void onPlayerErrorChanged(@Nullable PlaybackException error) { - errorFromParamRef.set(error); - errorFromGetterRef.set(controller.getPlayerError()); - latch.countDown(); - } - }; - controller.addListener(listener); - - session.setPlaybackState( - new PlaybackStateCompat.Builder() - .setState(PlaybackStateCompat.STATE_ERROR, /* position= */ 0, /* playbackSpeed= */ 1.0f) - .setErrorMessage(testErrorCode, testErrorMessage) - .build()); - - assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(errorFromParamRef.get().errorCode).isEqualTo(ERROR_CODE_REMOTE_ERROR); - assertThat(errorFromParamRef.get().getMessage()).isEqualTo(testConvertedErrorMessage); - assertThat(errorFromGetterRef.get().errorCode).isEqualTo(ERROR_CODE_REMOTE_ERROR); - assertThat(errorFromGetterRef.get().getMessage()).isEqualTo(testConvertedErrorMessage); - } - @Test public void setPlaybackState_withActions_updatesAndNotifiesAvailableCommands() throws Exception { MediaController controller = controllerTestRule.createController(session.getSessionToken()); diff --git a/libraries/test_session_current/src/main/java/androidx/media3/session/MediaSessionCompatProviderService.java b/libraries/test_session_current/src/main/java/androidx/media3/session/MediaSessionCompatProviderService.java index 37ef3aed33..6638bf4b8a 100644 --- a/libraries/test_session_current/src/main/java/androidx/media3/session/MediaSessionCompatProviderService.java +++ b/libraries/test_session_current/src/main/java/androidx/media3/session/MediaSessionCompatProviderService.java @@ -15,20 +15,19 @@ */ package androidx.media3.session; -import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.test.session.common.CommonConstants.ACTION_MEDIA_SESSION_COMPAT; import static androidx.media3.test.session.common.CommonConstants.KEY_METADATA_COMPAT; import static androidx.media3.test.session.common.CommonConstants.KEY_PLAYBACK_STATE_COMPAT; import static androidx.media3.test.session.common.CommonConstants.KEY_QUEUE; import static androidx.media3.test.session.common.CommonConstants.KEY_SESSION_COMPAT_TOKEN; +import android.annotation.SuppressLint; import android.app.PendingIntent; import android.app.Service; import android.content.Intent; import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; -import android.os.SystemClock; import android.support.v4.media.MediaMetadataCompat; import android.support.v4.media.session.MediaSessionCompat; import android.support.v4.media.session.MediaSessionCompat.QueueItem; @@ -120,6 +119,7 @@ public class MediaSessionCompatProviderService extends Service { session.setPlaybackToLocal(stream); } + @SuppressLint("RestrictedApi") @Override public void setPlaybackToRemote( String sessionTag, @@ -234,24 +234,6 @@ public class MediaSessionCompatProviderService extends Service { session.setExtras(extras); } - @Override - public void sendError( - String sessionTag, int errorCode, int errorMessageResId, Bundle errorExtras) { - MediaSessionCompat session = sessionMap.get(sessionTag); - session.setPlaybackState( - new PlaybackStateCompat.Builder() - .setState( - PlaybackStateCompat.STATE_ERROR, - /* position= */ PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, - /* playbackSpeed= */ 0, - /* updateTime= */ SystemClock.elapsedRealtime()) - .setActions(0) - .setBufferedPosition(0) - .setErrorMessage(errorCode, checkNotNull(getString(errorMessageResId))) - .setExtras(checkNotNull(errorExtras)) - .build()); - } - @Override public int getCallbackMethodCount(String sessionTag, String methodName) { CallCountingCallback callCountingCallback = callbackMap.get(sessionTag); diff --git a/libraries/test_session_current/src/main/java/androidx/media3/session/MediaSessionProviderService.java b/libraries/test_session_current/src/main/java/androidx/media3/session/MediaSessionProviderService.java index 7534a7ffb2..206db15a60 100644 --- a/libraries/test_session_current/src/main/java/androidx/media3/session/MediaSessionProviderService.java +++ b/libraries/test_session_current/src/main/java/androidx/media3/session/MediaSessionProviderService.java @@ -583,19 +583,15 @@ public class MediaSessionProviderService extends Service { } @Override - public void sendError( - String sessionId, - String controllerKey, - int errorCode, - int errorMessageResId, - Bundle errorExtras) + public void sendError(String sessionId, String controllerKey, Bundle sessionError) throws RemoteException { runOnHandler( () -> { MediaSession mediaSession = checkNotNull(sessionMap.get(sessionId)); + SessionError error = SessionError.fromBundle(sessionError); if (TextUtils.isEmpty(controllerKey)) { // Broadcast to all connected Media3 controller. - mediaSession.sendError(errorCode, errorMessageResId, errorExtras); + mediaSession.sendError(error); } else { // Send to controller with the given controller key in connection hints. for (ControllerInfo controllerInfo : mediaSession.getConnectedControllers()) { @@ -603,7 +599,7 @@ public class MediaSessionProviderService extends Service { .getConnectionHints() .getString(KEY_CONTROLLER, /* defaultValue= */ "") .equals(controllerKey)) { - mediaSession.sendError(controllerInfo, errorCode, errorMessageResId, errorExtras); + mediaSession.sendError(controllerInfo, error); } } } 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 b188a97046..922511f0b0 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,11 +25,14 @@ 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_FATAL_AUTHENTICATION_ERROR; +import static androidx.media3.test.session.common.MediaBrowserServiceCompatConstants.TEST_GET_CHILDREN_NON_FATAL_AUTHENTICATION_ERROR; 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 android.content.Intent; +import android.net.Uri; import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; @@ -38,6 +41,7 @@ import android.support.v4.media.MediaBrowserCompat.MediaItem; import android.support.v4.media.MediaDescriptionCompat; import android.support.v4.media.session.MediaSessionCompat; import android.support.v4.media.session.MediaSessionCompat.Callback; +import android.support.v4.media.session.PlaybackStateCompat; import androidx.annotation.GuardedBy; import androidx.annotation.Nullable; import androidx.media.MediaBrowserServiceCompat; @@ -81,7 +85,7 @@ public class MockMediaBrowserServiceCompat extends MediaBrowserServiceCompat { sessionCompat.setActive(true); setSessionToken(sessionCompat.getSessionToken()); - testBinder = new RemoteMediaBrowserServiceCompatStub(); + testBinder = new RemoteMediaBrowserServiceCompatStub(sessionCompat); } @Override @@ -236,6 +240,13 @@ public class MockMediaBrowserServiceCompat extends MediaBrowserServiceCompat { private static class RemoteMediaBrowserServiceCompatStub extends IRemoteMediaBrowserServiceCompat.Stub { + + private final MediaSessionCompat session; + + public RemoteMediaBrowserServiceCompatStub(MediaSessionCompat sessionCompat) { + session = sessionCompat; + } + @Override public void setProxyForTest(String testName) throws RemoteException { switch (testName) { @@ -254,6 +265,12 @@ public class MockMediaBrowserServiceCompat extends MediaBrowserServiceCompat { case TEST_GET_CHILDREN_WITH_NULL_LIST: setProxyForTestOnChildrenChanged_withNullChildrenListInLegacyService_convertedToSessionError(); break; + case TEST_GET_CHILDREN_FATAL_AUTHENTICATION_ERROR: + getChildren_authenticationError_receivesPlaybackException(session, /* isFatal= */ true); + break; + case TEST_GET_CHILDREN_NON_FATAL_AUTHENTICATION_ERROR: + getChildren_authenticationError_receivesPlaybackException(session, /* isFatal= */ false); + break; default: throw new IllegalArgumentException("Unknown testName: " + testName); } @@ -319,6 +336,44 @@ public class MockMediaBrowserServiceCompat extends MediaBrowserServiceCompat { }); } + private void getChildren_authenticationError_receivesPlaybackException( + MediaSessionCompat session, boolean isFatal) { + 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( + isFatal + ? null + : ImmutableList.of( + new MediaItem( + new MediaDescriptionCompat.Builder() + .setMediaUri(Uri.parse("http://www.example.com")) + .setMediaId("mediaId") + .build(), + MediaItem.FLAG_PLAYABLE))); + session.setPlaybackState( + new PlaybackStateCompat.Builder() + .setState( + isFatal + ? PlaybackStateCompat.STATE_ERROR + : PlaybackStateCompat.STATE_PLAYING, + isFatal ? PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN : 123L, + /* playbackSpeed= */ isFatal ? 0f : 1.0f) + .setErrorMessage( + PlaybackStateCompat.ERROR_CODE_AUTHENTICATION_EXPIRED, + "authentication expired") + .build()); + } + }); + } + 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 ab0547476b..612b9dea22 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 @@ -40,6 +40,7 @@ import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_I 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_AUTH_EXPIRED_ERROR_NON_FATAL; import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_ERROR; import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_LONG_LIST; import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_NO_CHILDREN; @@ -365,13 +366,32 @@ public class MockMediaLibraryService extends MediaLibraryService { PARENT_ID_AUTH_EXPIRED_ERROR_KEY_ERROR_RESOLUTION_ACTION_LABEL); return Objects.equals(parentId, PARENT_ID_AUTH_EXPIRED_ERROR) ? Futures.immediateFuture( + // error with SessionError LibraryResult.ofError( new SessionError(ERROR_SESSION_AUTHENTICATION_EXPIRED, "error message", bundle), new LibraryParams.Builder().build())) : Futures.immediateFuture( + // deprecated error before SessionError was introduced LibraryResult.ofError( ERROR_SESSION_AUTHENTICATION_EXPIRED, new LibraryParams.Builder().setExtras(bundle).build())); + } else if (Objects.equals(parentId, PARENT_ID_AUTH_EXPIRED_ERROR_NON_FATAL)) { + Bundle bundle = new Bundle(); + Intent signInIntent = new Intent("action"); + int flags = Util.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0; + bundle.putParcelable( + EXTRAS_KEY_ERROR_RESOLUTION_ACTION_INTENT_COMPAT, + PendingIntent.getActivity( + getApplicationContext(), /* requestCode= */ 0, signInIntent, flags)); + bundle.putString( + EXTRAS_KEY_ERROR_RESOLUTION_ACTION_LABEL_COMPAT, + PARENT_ID_AUTH_EXPIRED_ERROR_KEY_ERROR_RESOLUTION_ACTION_LABEL); + session.sendError( + new SessionError(ERROR_SESSION_AUTHENTICATION_EXPIRED, "error message", bundle)); + return Futures.immediateFuture( + LibraryResult.ofError( + new SessionError(ERROR_SESSION_AUTHENTICATION_EXPIRED, "error message"), + new LibraryParams.Builder().build())); } return Futures.immediateFuture(LibraryResult.ofError(ERROR_BAD_VALUE, params)); } diff --git a/libraries/test_session_current/src/main/java/androidx/media3/session/RemoteMediaSession.java b/libraries/test_session_current/src/main/java/androidx/media3/session/RemoteMediaSession.java index d5a13e09cb..869b9eb164 100644 --- a/libraries/test_session_current/src/main/java/androidx/media3/session/RemoteMediaSession.java +++ b/libraries/test_session_current/src/main/java/androidx/media3/session/RemoteMediaSession.java @@ -213,11 +213,9 @@ public class RemoteMediaSession { binder.setSessionActivity(sessionId, sessionActivity); } - public void sendError( - @Nullable String controllerKey, int errorCode, int errorMessageResId, Bundle errorExtras) + public void sendError(@Nullable String controllerKey, SessionError sessionError) throws RemoteException { - binder.sendError( - sessionId, nullToEmpty(controllerKey), errorCode, errorMessageResId, errorExtras); + binder.sendError(sessionId, nullToEmpty(controllerKey), sessionError.toBundle()); } //////////////////////////////////////////////////////////////////////////////// diff --git a/libraries/test_session_current/src/main/java/androidx/media3/session/RemoteMediaSessionCompat.java b/libraries/test_session_current/src/main/java/androidx/media3/session/RemoteMediaSessionCompat.java index 7022ff48d6..e190bc39dc 100644 --- a/libraries/test_session_current/src/main/java/androidx/media3/session/RemoteMediaSessionCompat.java +++ b/libraries/test_session_current/src/main/java/androidx/media3/session/RemoteMediaSessionCompat.java @@ -180,11 +180,6 @@ public class RemoteMediaSessionCompat { binder.setSessionExtras(sessionTag, extras); } - public void sendError(int errorCode, int errorMessageResInt, Bundle errorExtras) - throws RemoteException { - binder.sendError(sessionTag, errorCode, errorMessageResInt, errorExtras); - } - //////////////////////////////////////////////////////////////////////////////// // Non-public methods //////////////////////////////////////////////////////////////////////////////// diff --git a/libraries/test_session_current/src/main/java/androidx/media3/session/TestMediaBrowserListener.java b/libraries/test_session_current/src/main/java/androidx/media3/session/TestMediaBrowserListener.java index d8907c160b..288079d84a 100644 --- a/libraries/test_session_current/src/main/java/androidx/media3/session/TestMediaBrowserListener.java +++ b/libraries/test_session_current/src/main/java/androidx/media3/session/TestMediaBrowserListener.java @@ -68,9 +68,8 @@ public final class TestMediaBrowserListener implements MediaBrowser.Listener { } @Override - public void onError( - MediaController controller, int errorCode, String errorMessage, Bundle errorExtras) { - delegate.onError(controller, errorCode, errorMessage, errorExtras); + public void onError(MediaController controller, SessionError sessionError) { + delegate.onError(controller, sessionError); } @Override