From 84c0b6bcb136ab45c411a6e0355c18b6af395dda Mon Sep 17 00:00:00 2001 From: bachinger Date: Tue, 14 May 2024 09:27:41 -0700 Subject: [PATCH] Add MediaSession.sendError to send non-fatal error data to controllers This allows to set custom error message for instance on Android Auto/Automotive OS. Issue: androidx/media#543 PiperOrigin-RevId: 633610089 --- RELEASENOTES.md | 6 ++ .../media3/session/IMediaController.aidl | 3 +- .../media3/session/LegacyConversions.java | 32 +++++- .../media3/session/MediaController.java | 49 ++++++++++ .../session/MediaControllerImplBase.java | 9 ++ .../session/MediaControllerImplLegacy.java | 98 +++++++++++++++---- .../media3/session/MediaControllerStub.java | 9 +- .../androidx/media3/session/MediaSession.java | 43 ++++++++ .../media3/session/MediaSessionImpl.java | 28 ++++++ .../session/MediaSessionLegacyStub.java | 8 ++ .../media3/session/MediaSessionStub.java | 6 ++ .../session/src/main/res/values/strings.xml | 2 + .../session/common/IRemoteMediaSession.aidl | 1 + .../common/IRemoteMediaSessionCompat.aidl | 1 + ...lerCompatCallbackWithMediaSessionTest.java | 67 +++++++++++++ .../session/MediaControllerListenerTest.java | 72 ++++++++++++++ ...lerListenerWithMediaSessionCompatTest.java | 67 +++++++++++++ .../MediaSessionCompatProviderService.java | 20 ++++ .../session/MediaSessionProviderService.java | 38 +++++++ .../media3/session/RemoteMediaSession.java | 8 ++ .../session/RemoteMediaSessionCompat.java | 5 + .../session/TestMediaBrowserListener.java | 6 ++ 22 files changed, 554 insertions(+), 24 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index a738f70528..fa567b9389 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -108,6 +108,12 @@ * Align conversion of `MediaMetadata` to `MediaDescriptionCompat`, to use the same preferred order and logic when selecting metadata properties as in media1. + * Add `MediaSession.sendError()` that allows sending non-fatal errors to + Media3 controller. When using the notification controller (see + `MediaSession.getMediaNotificationControllerInfo()`), the custom error + is used to update the `PlaybackState` of the platform session to an + error state with the given error information + ([#543](https://github.com/androidx/media/issues/543)). * UI: * Downloads: * OkHttp Extension: 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 5aa925044c..97e6fcb6c3 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,8 @@ oneway interface IMediaController { void onRenderedFirstFrame(int seq) = 3010; void onExtrasChanged(int seq, in Bundle extras) = 3011; void onSessionActivityChanged(int seq, in PendingIntent pendingIntent) = 3013; - // Next Id for MediaController: 3014 + void onError(int seq, int errorCode, String errorMessage, in Bundle errorExtras) = 3014; + // Next Id for MediaController: 3015 void onChildrenChanged( int seq, String parentId, int itemCount, in @nullable Bundle libraryParams) = 4000; 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 948a741975..be273558f8 100644 --- a/libraries/session/src/main/java/androidx/media3/session/LegacyConversions.java +++ b/libraries/session/src/main/java/androidx/media3/session/LegacyConversions.java @@ -76,6 +76,7 @@ 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; @@ -153,12 +154,23 @@ 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) { + || playbackStateCompat.getState() != PlaybackStateCompat.STATE_ERROR + || !FATAL_LEGACY_ERROR_CODES.contains(playbackStateCompat.getErrorCode())) { return null; } StringBuilder stringBuilder = new StringBuilder(); @@ -171,6 +183,24 @@ import java.util.concurrent.TimeoutException; errorMessage, /* cause= */ null, PlaybackException.ERROR_CODE_REMOTE_ERROR); } + /** Converts {@link PlaybackStateCompat} to {@link NonFatalErrorInfo}. */ + @Nullable + public static NonFatalErrorInfo convertToNonFatalErrorInfo( + @Nullable PlaybackStateCompat playbackStateCompat, String errorMessageFallback) { + if (playbackStateCompat == null + || playbackStateCompat.getState() != PlaybackStateCompat.STATE_ERROR + || FATAL_LEGACY_ERROR_CODES.contains(playbackStateCompat.getErrorCode())) { + return null; + } + @Nullable Bundle playbackStateCompatExtras = playbackStateCompat.getExtras(); + return new NonFatalErrorInfo( + playbackStateCompat.getErrorCode(), + !TextUtils.isEmpty(playbackStateCompat.getErrorMessage()) + ? playbackStateCompat.getErrorMessage().toString() + : errorMessageFallback, + playbackStateCompatExtras != null ? playbackStateCompatExtras : Bundle.EMPTY); + } + 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/MediaController.java b/libraries/session/src/main/java/androidx/media3/session/MediaController.java index db63c231ec..37e512987d 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaController.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaController.java @@ -24,6 +24,7 @@ import static androidx.media3.common.util.Util.postOrRun; import android.app.PendingIntent; import android.content.Context; +import android.media.session.PlaybackState; import android.os.Bundle; import android.os.Handler; import android.os.Looper; @@ -444,6 +445,54 @@ public class MediaController implements Player { @UnstableApi default void onSessionActivityChanged( MediaController controller, PendingIntent sessionActivity) {} + + /** + * Called when an non-fatal error {@linkplain + * MediaSession#sendError(MediaSession.ControllerInfo, int, int, Bundle) sent by the session} is + * received. + * + *

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

Non-fatal legacy error codes: + * + *

+ * + *

Fatal legacy error codes of the {@link PlaybackState} in state {@link + * PlaybackState#STATE_ERROR} are converted to a player error. See {@link + * Player.Listener#onPlayerError(PlaybackException)} and {@link + * Player.Listener#onPlayerErrorChanged(PlaybackException)}. + * + *

Fatal legacy error codes: + * + *

+ * + * @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. + */ + @UnstableApi + default void onError( + MediaController controller, int errorCode, String errorMessage, Bundle errorExtras) {} } /* 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 9a24401c59..d23837b84c 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java @@ -2889,6 +2889,15 @@ import org.checkerframework.checker.nullness.qual.NonNull; listener -> listener.onSessionActivityChanged(getInstance(), sessionActivity)); } + public void onError(int seq, int errorCode, String errorMessage, Bundle errorExtras) { + if (!isConnected()) { + return; + } + getInstance() + .notifyControllerListener( + listener -> listener.onError(getInstance(), errorCode, errorMessage, errorExtras)); + } + public void onRenderedFirstFrame() { listeners.sendEvent( /* eventFlag= */ Player.EVENT_RENDERED_FIRST_FRAME, Listener::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 d4cc1fe019..cdc4cf316d 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java @@ -192,7 +192,8 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; controllerInfo.availableSessionCommands, controllerInfo.availablePlayerCommands, controllerInfo.customLayout, - controllerInfo.sessionExtras); + controllerInfo.sessionExtras, + /* errorInfo= */ null); updateStateMaskedControllerInfo( maskedControllerInfo, /* discontinuityReason= */ null, @@ -257,7 +258,8 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; controllerInfo.availableSessionCommands, controllerInfo.availablePlayerCommands, controllerInfo.customLayout, - controllerInfo.sessionExtras); + controllerInfo.sessionExtras, + /* errorInfo= */ null); updateStateMaskedControllerInfo( maskedControllerInfo, /* discontinuityReason= */ null, @@ -379,7 +381,8 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; controllerInfo.availableSessionCommands, controllerInfo.availablePlayerCommands, controllerInfo.customLayout, - controllerInfo.sessionExtras); + controllerInfo.sessionExtras, + /* errorInfo= */ null); updateStateMaskedControllerInfo( maskedControllerInfo, discontinuityReason, mediaItemTransitionReason); } @@ -538,7 +541,8 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; controllerInfo.availableSessionCommands, controllerInfo.availablePlayerCommands, controllerInfo.customLayout, - controllerInfo.sessionExtras); + controllerInfo.sessionExtras, + /* errorInfo= */ null); updateStateMaskedControllerInfo( maskedControllerInfo, /* discontinuityReason= */ null, @@ -558,7 +562,8 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; controllerInfo.availableSessionCommands, controllerInfo.availablePlayerCommands, controllerInfo.customLayout, - controllerInfo.sessionExtras); + controllerInfo.sessionExtras, + /* errorInfo= */ null); updateStateMaskedControllerInfo( maskedControllerInfo, /* discontinuityReason= */ null, @@ -651,7 +656,8 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; controllerInfo.availableSessionCommands, controllerInfo.availablePlayerCommands, controllerInfo.customLayout, - controllerInfo.sessionExtras); + controllerInfo.sessionExtras, + /* errorInfo= */ null); updateStateMaskedControllerInfo( maskedControllerInfo, /* discontinuityReason= */ null, @@ -715,7 +721,8 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; controllerInfo.availableSessionCommands, controllerInfo.availablePlayerCommands, controllerInfo.customLayout, - controllerInfo.sessionExtras); + controllerInfo.sessionExtras, + /* errorInfo= */ null); updateStateMaskedControllerInfo( maskedControllerInfo, /* discontinuityReason= */ null, @@ -768,7 +775,8 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; controllerInfo.availableSessionCommands, controllerInfo.availablePlayerCommands, controllerInfo.customLayout, - controllerInfo.sessionExtras); + controllerInfo.sessionExtras, + /* errorInfo= */ null); updateStateMaskedControllerInfo( maskedControllerInfo, /* discontinuityReason= */ null, @@ -835,7 +843,8 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; controllerInfo.availableSessionCommands, controllerInfo.availablePlayerCommands, controllerInfo.customLayout, - controllerInfo.sessionExtras); + controllerInfo.sessionExtras, + /* errorInfo= */ null); updateStateMaskedControllerInfo( maskedControllerInfo, /* discontinuityReason= */ null, @@ -948,7 +957,8 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; controllerInfo.availableSessionCommands, controllerInfo.availablePlayerCommands, controllerInfo.customLayout, - controllerInfo.sessionExtras); + controllerInfo.sessionExtras, + /* errorInfo= */ null); updateStateMaskedControllerInfo( maskedControllerInfo, /* discontinuityReason= */ null, @@ -975,7 +985,8 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; controllerInfo.availableSessionCommands, controllerInfo.availablePlayerCommands, controllerInfo.customLayout, - controllerInfo.sessionExtras); + controllerInfo.sessionExtras, + /* errorInfo= */ null); updateStateMaskedControllerInfo( maskedControllerInfo, /* discontinuityReason= */ null, @@ -1111,7 +1122,8 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; controllerInfo.availableSessionCommands, controllerInfo.availablePlayerCommands, controllerInfo.customLayout, - controllerInfo.sessionExtras); + controllerInfo.sessionExtras, + /* errorInfo= */ null); updateStateMaskedControllerInfo( maskedControllerInfo, /* discontinuityReason= */ null, @@ -1143,7 +1155,8 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; controllerInfo.availableSessionCommands, controllerInfo.availablePlayerCommands, controllerInfo.customLayout, - controllerInfo.sessionExtras); + controllerInfo.sessionExtras, + /* errorInfo= */ null); updateStateMaskedControllerInfo( maskedControllerInfo, /* discontinuityReason= */ null, @@ -1174,7 +1187,8 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; controllerInfo.availableSessionCommands, controllerInfo.availablePlayerCommands, controllerInfo.customLayout, - controllerInfo.sessionExtras); + controllerInfo.sessionExtras, + /* errorInfo= */ null); updateStateMaskedControllerInfo( maskedControllerInfo, /* discontinuityReason= */ null, @@ -1208,7 +1222,8 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; controllerInfo.availableSessionCommands, controllerInfo.availablePlayerCommands, controllerInfo.customLayout, - controllerInfo.sessionExtras); + controllerInfo.sessionExtras, + /* errorInfo= */ null); updateStateMaskedControllerInfo( maskedControllerInfo, /* discontinuityReason= */ null, @@ -1246,7 +1261,8 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; controllerInfo.availableSessionCommands, controllerInfo.availablePlayerCommands, controllerInfo.customLayout, - controllerInfo.sessionExtras); + controllerInfo.sessionExtras, + /* errorInfo= */ null); updateStateMaskedControllerInfo( maskedControllerInfo, /* discontinuityReason= */ null, @@ -1539,7 +1555,8 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; controllerCompat.isSessionReady(), controllerCompat.getRatingType(), getInstance().getTimeDiffMs(), - getRoutingControllerId(controllerCompat)); + getRoutingControllerId(controllerCompat), + context); Pair<@NullableType Integer, @NullableType Integer> reasons = calculateDiscontinuityAndTransitionReason( legacyPlayerInfo, @@ -1731,6 +1748,16 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; listener.onCustomLayoutChanged(getInstance(), newControllerInfo.customLayout); }); } + if (newControllerInfo.nonFatalErrorInfo != null) { + getInstance() + .notifyControllerListener( + listener -> + listener.onError( + getInstance(), + newControllerInfo.nonFatalErrorInfo.errorCode, + newControllerInfo.nonFatalErrorInfo.errorMessage, + newControllerInfo.nonFatalErrorInfo.errorExtras)); + } listeners.flushEvents(); } @@ -1754,6 +1781,18 @@ 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 @@ -1873,7 +1912,8 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; controllerInfo.availableSessionCommands, controllerInfo.availablePlayerCommands, controllerInfo.customLayout, - extras); + extras, + /* errorInfo= */ null); getInstance() .notifyControllerListener(listener -> listener.onExtrasChanged(getInstance(), extras)); } @@ -1936,7 +1976,8 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; boolean isSessionReady, @RatingCompat.Style int ratingType, long timeDiffMs, - @Nullable String routingControllerId) { + @Nullable String routingControllerId, + Context context) { QueueTimeline currentTimeline; MediaMetadata mediaMetadata; int currentMediaItemIndex; @@ -2053,6 +2094,10 @@ 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)); long currentPositionMs = LegacyConversions.convertToCurrentPositionMs( @@ -2121,6 +2166,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; customLayout, newLegacyPlayerInfo.sessionExtras, playerError, + nonFatalErrorInfo, durationMs, currentPositionMs, bufferedPositionMs, @@ -2289,6 +2335,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; ImmutableList customLayout, Bundle sessionExtras, @Nullable PlaybackException playerError, + @Nullable NonFatalErrorInfo nonFatalErrorInfo, long durationMs, long currentPositionMs, long bufferedPositionMs, @@ -2358,7 +2405,12 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; /* parameters= */ TrackSelectionParameters.DEFAULT_WITHOUT_CONTEXT); return new ControllerInfo( - playerInfo, availableSessionCommands, availablePlayerCommands, customLayout, sessionExtras); + playerInfo, + availableSessionCommands, + availablePlayerCommands, + customLayout, + sessionExtras, + /* errorInfo= */ nonFatalErrorInfo); } private static PositionInfo createPositionInfo( @@ -2569,6 +2621,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; public ControllerInfo() { playerInfo = PlayerInfo.DEFAULT.copyWithTimeline(QueueTimeline.DEFAULT); @@ -2576,6 +2629,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; availablePlayerCommands = Commands.EMPTY; customLayout = ImmutableList.of(); sessionExtras = Bundle.EMPTY; + nonFatalErrorInfo = null; } public ControllerInfo( @@ -2583,12 +2637,14 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; SessionCommands availableSessionCommands, Commands availablePlayerCommands, ImmutableList customLayout, - @Nullable Bundle sessionExtras) { + @Nullable Bundle sessionExtras, + @Nullable NonFatalErrorInfo nonFatalErrorInfo) { this.playerInfo = playerInfo; this.availableSessionCommands = availableSessionCommands; this.availablePlayerCommands = availablePlayerCommands; this.customLayout = customLayout; this.sessionExtras = sessionExtras == null ? Bundle.EMPTY : sessionExtras; + this.nonFatalErrorInfo = nonFatalErrorInfo; } } } 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 d1245c4ffc..c8211f9a2a 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 = 3; + public static final int VERSION_INT = 4; private final WeakReference controller; @@ -264,6 +264,13 @@ import org.checkerframework.checker.nullness.qual.NonNull; dispatchControllerTaskOnHandler(controller -> controller.onExtrasChanged(extras)); } + @Override + public void onError(int seq, int errorCode, String errorMessage, Bundle errorExtras) + throws RemoteException { + dispatchControllerTaskOnHandler( + controller -> controller.onError(seq, errorCode, errorMessage, errorExtras)); + } + @Override public void onRenderedFirstFrame(int seq) { dispatchControllerTaskOnHandler(MediaControllerImplBase::onRenderedFirstFrame); 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 a8edd4e66d..e52639d50f 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java @@ -1139,6 +1139,46 @@ public class MediaSession { return impl.sendCustomCommand(controller, command, args); } + /** + * Sends a non-fatal error to the given controller. + * + *

Use {@linkplain MediaSession#getMediaNotificationControllerInfo()} to set the error of the + * {@linkplain android.media.session.PlaybackState playback state} of the legacy platform session. + * + *

Only Media3 controllers are supported. If an error is attempted to be sent to a controller + * with {@link ControllerInfo#getControllerVersion() a controller version} of value {@link + * 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. + * @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) { + checkArgument( + controllerInfo.getControllerVersion() != ControllerInfo.LEGACY_CONTROLLER_VERSION); + impl.sendError(controllerInfo, errorCode, errorMessageResId, errorExtras); + } + + /** + * Sends a non-fatal error to all connected Media3 controllers. + * + *

See {@link #sendError(ControllerInfo, int, int, Bundle)} 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. + */ + @UnstableApi + public final void sendError(int errorCode, int errorMessageResourceId, Bundle errorExtras) { + impl.sendError(errorCode, errorMessageResourceId, errorExtras); + } + /* package */ final MediaSessionCompat getSessionCompat() { return impl.getSessionCompat(); } @@ -1944,6 +1984,9 @@ public class MediaSession { throws RemoteException {} default void onRenderedFirstFrame(int seq) throws RemoteException {} + + default void onError(int seq, int errorCode, String errorMessage, Bundle errorExtras) + 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 1de512a824..7b52053fca 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java @@ -602,6 +602,34 @@ 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) { + 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)); + if (isMediaNotificationController(controllerInfo)) { + dispatchRemoteControllerTaskToLegacyStub( + (callback, seq) -> callback.onError(seq, errorCode, errorMessage, errorExtras)); + } + } + + public void sendError(int errorCode, int errorMessageResourceId, Bundle errorExtras) { + // 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); + } + } + public MediaSession.ConnectionResult onConnectOnHandler(ControllerInfo controller) { if (isMediaNotificationControllerConnected && isSystemUiController(controller)) { // Hide System UI and provide the connection result from the `PlayerWrapper` state. 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 449aef9c60..7761bcf09d 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java @@ -1076,6 +1076,14 @@ import org.checkerframework.checker.initialization.qual.Initialized; sessionCompat.setExtras(sessionExtras); } + @Override + public void onError(int seq, int errorCode, String errorMessage, Bundle errorExtras) { + PlayerWrapper playerWrapper = sessionImpl.getPlayerWrapper(); + playerWrapper.setLegacyErrorStatus(errorCode, errorMessage, errorExtras); + sessionCompat.setPlaybackState(playerWrapper.createPlaybackStateCompat()); + playerWrapper.clearLegacyErrorStatus(); + } + @Override public void sendCustomCommand(int seq, SessionCommand command, Bundle args) { sessionCompat.sendSessionEvent(command.customAction, args); 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 98744faf06..db0f71f13e 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java @@ -2137,6 +2137,12 @@ import java.util.concurrent.ExecutionException; iController.onExtrasChanged(sequenceNumber, sessionExtras); } + @Override + public void onError(int sequenceNumber, int errorCode, String errorMessage, Bundle errorExtras) + throws RemoteException { + iController.onError(sequenceNumber, errorCode, errorMessage, errorExtras); + } + @Override public int hashCode() { return ObjectsCompat.hash(getCallbackBinder()); diff --git a/libraries/session/src/main/res/values/strings.xml b/libraries/session/src/main/res/values/strings.xml index 06eef42afe..7c2955e0bd 100644 --- a/libraries/session/src/main/res/values/strings.xml +++ b/libraries/session/src/main/res/values/strings.xml @@ -29,4 +29,6 @@ Seek forward Authentication required + + No error message provided 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 77959d4959..9caf8212a2 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,6 +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 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 b51384d961..d9f1ce736d 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,5 +42,6 @@ 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_current/src/androidTest/java/androidx/media3/session/MediaControllerCompatCallbackWithMediaSessionTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerCompatCallbackWithMediaSessionTest.java index fba6045961..1bf5c95093 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 @@ -1021,6 +1021,73 @@ public class MediaControllerCompatCallbackWithMediaSessionTest { assertThat(TestUtils.equals(receivedSessionExtras.get(1), sessionExtras)).isTrue(); } + @Test + public void sendError_toAllControllers_onPlaybackStateChangedToErrorStateAndWithCorrectErrorData() + throws Exception { + CountDownLatch latch = new CountDownLatch(1); + 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 errorBundle = new Bundle(); + errorBundle.putInt("intKey", 99); + + session.sendError( + /* controllerKey= */ null, + /* errorCode= */ 1, + R.string.authentication_required, + errorBundle); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(playbackStates).hasSize(1); + PlaybackStateCompat playbackStateCompat = playbackStates.get(0); + assertThat(playbackStateCompat.getState()).isEqualTo(PlaybackStateCompat.STATE_ERROR); + assertThat(playbackStateCompat.getErrorCode()).isEqualTo(1); + assertThat(playbackStateCompat.getErrorMessage().toString()) + .isEqualTo(context.getString(R.string.authentication_required)); + assertThat(TestUtils.equals(playbackStateCompat.getExtras(), errorBundle)).isTrue(); + } + + @Test + public void + sendError_toMediaNotificationControllers_onPlaybackStateChangedToErrorStateAndWithCorrectErrorData() + throws Exception { + CountDownLatch latch = new CountDownLatch(1); + 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 errorBundle = new Bundle(); + errorBundle.putInt("intKey", 99); + + session.sendError( + /* controllerKey= */ MediaController.KEY_MEDIA_NOTIFICATION_CONTROLLER_FLAG, + /* errorCode= */ 1, + R.string.authentication_required, + errorBundle); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(playbackStates).hasSize(1); + PlaybackStateCompat playbackStateCompat = playbackStates.get(0); + assertThat(playbackStateCompat.getState()).isEqualTo(PlaybackStateCompat.STATE_ERROR); + assertThat(playbackStateCompat.getErrorCode()).isEqualTo(1); + assertThat(playbackStateCompat.getErrorMessage().toString()) + .isEqualTo(context.getString(R.string.authentication_required)); + assertThat(TestUtils.equals(playbackStateCompat.getExtras(), errorBundle)).isTrue(); + } + @Test public void setSessionActivity_changedWhenReceivedWithSetter() throws Exception { Intent intent = new Intent(context, SurfaceActivity.class); 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 bae4f67ff4..debdf3a2dc 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 @@ -2634,6 +2634,78 @@ public class MediaControllerListenerTest { .containsExactly(Player.EVENT_AUDIO_ATTRIBUTES_CHANGED); } + @Test + 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<>(); + Bundle connectionHints1 = new Bundle(); + connectionHints1.putString(KEY_CONTROLLER, "ctrl-1"); + controllerTestRule.createController( + remoteSession.getToken(), + 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); + errorLatch.countDown(); + } + }); + List errorCodes2 = new ArrayList<>(); + List errorMessages2 = new ArrayList<>(); + List errorExtras2 = new ArrayList<>(); + Bundle connectionHints2 = new Bundle(); + connectionHints2.putString(KEY_CONTROLLER, "ctrl-2"); + controllerTestRule.createController( + remoteSession.getToken(), + 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); + errorLatch.countDown(); + } + }); + Bundle errorExtra1 = new Bundle(); + errorExtra1.putInt("intKey", 1); + Bundle errorExtra2 = new Bundle(); + errorExtra2.putInt("intKey", 2); + + 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); + + 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); + } + @Test public void getCurrentCues_afterConnected() throws Exception { Cue testCue1 = new Cue.Builder().setText(SpannedString.valueOf("cue1")).build(); 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 b4748270c6..228b4aa3c2 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 @@ -34,6 +34,7 @@ import androidx.media3.common.C; import androidx.media3.common.DeviceInfo; import androidx.media3.common.FlagSet; import androidx.media3.common.MediaMetadata; +import androidx.media3.common.PlaybackException; import androidx.media3.common.Player; import androidx.media3.common.util.ConditionVariable; import androidx.media3.common.util.Util; @@ -229,6 +230,72 @@ 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/main/java/androidx/media3/session/MediaSessionCompatProviderService.java b/libraries/test_session_current/src/main/java/androidx/media3/session/MediaSessionCompatProviderService.java index be08fe3b9a..37ef3aed33 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,6 +15,7 @@ */ 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; @@ -27,6 +28,7 @@ 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; @@ -232,6 +234,24 @@ 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 172343b52d..416ad9bf40 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 @@ -16,6 +16,7 @@ package androidx.media3.session; import static androidx.media3.common.Player.COMMAND_GET_TRACKS; +import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.session.MediaSession.ConnectionResult.accept; import static androidx.media3.test.session.common.CommonConstants.ACTION_MEDIA3_SESSION; import static androidx.media3.test.session.common.CommonConstants.KEY_AUDIO_ATTRIBUTES; @@ -76,6 +77,7 @@ import android.content.Intent; import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; +import android.text.TextUtils; import androidx.annotation.Nullable; import androidx.media3.common.AudioAttributes; import androidx.media3.common.C; @@ -579,6 +581,42 @@ public class MediaSessionProviderService extends Service { }); } + @Override + public void sendError( + String sessionId, + String controllerKey, + int errorCode, + int errorMessageResId, + Bundle errorExtras) + throws RemoteException { + runOnHandler( + () -> { + MediaSession mediaSession = checkNotNull(sessionMap.get(sessionId)); + if (TextUtils.isEmpty(controllerKey)) { + // Broadcast to all connected Media3 controller. + mediaSession.sendError(errorCode, errorMessageResId, errorExtras); + } else if (controllerKey.equals( + MediaController.KEY_MEDIA_NOTIFICATION_CONTROLLER_FLAG)) { + // Send to media notification controller. + mediaSession.sendError( + checkNotNull(mediaSession.getMediaNotificationControllerInfo()), + errorCode, + errorMessageResId, + errorExtras); + } else { + // Send to controller with the given controller key in connection hints. + for (ControllerInfo controllerInfo : mediaSession.getConnectedControllers()) { + if (controllerInfo + .getConnectionHints() + .getString(KEY_CONTROLLER, /* defaultValue= */ "") + .equals(controllerKey)) { + mediaSession.sendError(controllerInfo, errorCode, errorMessageResId, errorExtras); + } + } + } + }); + } + @Override public void setSessionActivity(String sessionId, PendingIntent sessionActivity) throws RemoteException { 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 f1d5007e10..d5a13e09cb 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 @@ -56,6 +56,7 @@ import static androidx.media3.test.session.common.CommonConstants.KEY_VIDEO_SIZE import static androidx.media3.test.session.common.CommonConstants.KEY_VOLUME; import static androidx.media3.test.session.common.CommonConstants.MEDIA3_SESSION_PROVIDER_SERVICE; import static androidx.media3.test.session.common.TestUtils.SERVICE_CONNECTION_TIMEOUT_MS; +import static com.google.common.base.Strings.nullToEmpty; import static com.google.common.truth.Truth.assertWithMessage; import static java.util.concurrent.TimeUnit.MILLISECONDS; @@ -212,6 +213,13 @@ public class RemoteMediaSession { binder.setSessionActivity(sessionId, sessionActivity); } + public void sendError( + @Nullable String controllerKey, int errorCode, int errorMessageResId, Bundle errorExtras) + throws RemoteException { + binder.sendError( + sessionId, nullToEmpty(controllerKey), errorCode, errorMessageResId, errorExtras); + } + //////////////////////////////////////////////////////////////////////////////// // RemoteMockPlayer methods //////////////////////////////////////////////////////////////////////////////// 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 e190bc39dc..7022ff48d6 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,6 +180,11 @@ 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 d95a37f995..d8907c160b 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 @@ -67,6 +67,12 @@ public final class TestMediaBrowserListener implements MediaBrowser.Listener { delegate.onExtrasChanged(controller, extras); } + @Override + public void onError( + MediaController controller, int errorCode, String errorMessage, Bundle errorExtras) { + delegate.onError(controller, errorCode, errorMessage, errorExtras); + } + @Override public void onSessionActivityChanged(MediaController controller, PendingIntent sessionActivity) { delegate.onSessionActivityChanged(controller, sessionActivity);