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
This commit is contained in:
parent
2175c432d7
commit
84c0b6bcb1
@ -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:
|
||||
|
@ -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;
|
||||
|
@ -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<Integer> 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);
|
||||
|
@ -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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>Non-fatal legacy error codes:
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@code PlaybackStateCompat.ERROR_CODE_ACTION_ABORTED}
|
||||
* <li>{@code PlaybackStateCompat.ERROR_CODE_APP_ERROR}
|
||||
* <li>{@code PlaybackStateCompat.ERROR_CODE_CONTENT_ALREADY_PLAYING}
|
||||
* <li>{@code PlaybackStateCompat.ERROR_CODE_END_OF_QUEUE}
|
||||
* <li>{@code PlaybackStateCompat.ERROR_CODE_NOT_SUPPORTED}
|
||||
* <li>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.
|
||||
* </ul>
|
||||
*
|
||||
* <p>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)}.
|
||||
*
|
||||
* <p>Fatal legacy error codes:
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@code PlaybackStateCompat.ERROR_CODE_UNKNOWN_ERROR}
|
||||
* <li>{@code PlaybackStateCompat.ERROR_CODE_AUTHENTICATION_EXPIRED}
|
||||
* <li>{@code PlaybackStateCompat.ERROR_CODE_PREMIUM_ACCOUNT_REQUIRED}
|
||||
* <li>{@code PlaybackStateCompat.ERROR_CODE_CONCURRENT_STREAM_LIMIT}
|
||||
* <li>{@code PlaybackStateCompat.ERROR_CODE_PARENTAL_CONTROL_RESTRICTED}
|
||||
* <li>{@code PlaybackStateCompat.ERROR_CODE_NOT_AVAILABLE_IN_REGION}
|
||||
* <li>{@code PlaybackStateCompat.ERROR_CODE_SKIP_LIMIT_REACHED}
|
||||
* </ul>
|
||||
*
|
||||
* @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 {
|
||||
|
@ -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);
|
||||
|
@ -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<CommandButton> 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<CommandButton> 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<CommandButton> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<MediaControllerImplBase> 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);
|
||||
|
@ -1139,6 +1139,46 @@ public class MediaSession {
|
||||
return impl.sendCustomCommand(controller, command, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a non-fatal error to the given controller.
|
||||
*
|
||||
* <p>Use {@linkplain MediaSession#getMediaNotificationControllerInfo()} to set the error of the
|
||||
* {@linkplain android.media.session.PlaybackState playback state} of the legacy platform session.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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 {}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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<ControllerInfo> 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.
|
||||
|
@ -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);
|
||||
|
@ -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());
|
||||
|
@ -29,4 +29,6 @@
|
||||
<!-- Accessibility description for a 'seek forward' or 'fast-forward' button on a media notification. [CHAR LIMIT=NONE] -->
|
||||
<string name="media3_controls_seek_forward_description">Seek forward</string>
|
||||
<string name="authentication_required">Authentication required</string>
|
||||
<!-- Fallback error message if a PlaybackStateCompat has an error code without an error message. [CHAR LIMIT=NONE] -->
|
||||
<string name="legacy_error_message_fallback">No error message provided</string>
|
||||
</resources>
|
||||
|
@ -34,6 +34,7 @@ interface IRemoteMediaSession {
|
||||
void setCustomLayout(String sessionId, in List<Bundle> 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
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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<PlaybackStateCompat> 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<PlaybackStateCompat> 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);
|
||||
|
@ -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<Integer> errorCodes1 = new ArrayList<>();
|
||||
List<String> errorMessages1 = new ArrayList<>();
|
||||
List<Bundle> 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<Integer> errorCodes2 = new ArrayList<>();
|
||||
List<String> errorMessages2 = new ArrayList<>();
|
||||
List<Bundle> 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();
|
||||
|
@ -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<Integer> nonFatalErrorCodes = new ArrayList<>();
|
||||
List<String> nonFatalErrorMessages = new ArrayList<>();
|
||||
List<Bundle> 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<PlaybackException> 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());
|
||||
|
@ -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);
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -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
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -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);
|
||||
|
Loading…
x
Reference in New Issue
Block a user