Improve automatic error replication for legacy browsers
This change extends the error replication to a given set of error codes (not only authentication error), but only replicates an error if the caller of the service `Callback` is a legacy controller. It also makes error replication configurable so that apps can opt-out and report errors manually instead, or define the error codes for which replication is enabled. The change also removes the restriction of `sendError` only being available for Media3 controllers. Instead, sending an error to a legacy controller updates the platform playback state in the same way as sending the error to the media notification controller. #cherrypick PiperOrigin-RevId: 648399237
This commit is contained in:
parent
6bf2461f80
commit
70c063905c
@ -37,6 +37,11 @@
|
||||
`AcceptedResultBuilder.setSessionActivivty(PendingIntent)`. Once
|
||||
connected, the session activity can be updated with
|
||||
`MediaSession.setSessionActivity(ControllerInfo, PendingIntent)`.
|
||||
* Improve error replication of calls to `MediaLibrarySession.Callback`.
|
||||
Error replication can now be configured by using
|
||||
`MediaLibrarySession.Builder.setLibraryErrorReplicationMode()` for
|
||||
chosing the error type or opt-ing out of error replication which is on
|
||||
by default.
|
||||
* UI:
|
||||
* Work around a platform bug causing stretched/cropped video when using
|
||||
`SurfaceView` inside a Compose `AndroidView` on API 34
|
||||
|
@ -22,6 +22,11 @@ import static androidx.media3.session.LibraryResult.RESULT_SUCCESS;
|
||||
import static androidx.media3.session.LibraryResult.ofVoid;
|
||||
import static androidx.media3.session.SessionError.ERROR_BAD_VALUE;
|
||||
import static androidx.media3.session.SessionError.ERROR_NOT_SUPPORTED;
|
||||
import static java.lang.annotation.ElementType.FIELD;
|
||||
import static java.lang.annotation.ElementType.LOCAL_VARIABLE;
|
||||
import static java.lang.annotation.ElementType.METHOD;
|
||||
import static java.lang.annotation.ElementType.PARAMETER;
|
||||
import static java.lang.annotation.ElementType.TYPE_USE;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
@ -29,6 +34,7 @@ import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.IntRange;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.MediaItem;
|
||||
@ -43,6 +49,10 @@ import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@ -118,6 +128,37 @@ public abstract class MediaLibraryService extends MediaSessionService {
|
||||
*/
|
||||
public static final class MediaLibrarySession extends MediaSession {
|
||||
|
||||
/**
|
||||
* Library error replication mode. One of {@link #LIBRARY_ERROR_REPLICATION_MODE_NONE}, {@link
|
||||
* #LIBRARY_ERROR_REPLICATION_MODE_FATAL} or {@link #LIBRARY_ERROR_REPLICATION_MODE_NON_FATAL}.
|
||||
*/
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@Target({FIELD, METHOD, PARAMETER, LOCAL_VARIABLE, TYPE_USE})
|
||||
@IntDef({
|
||||
LIBRARY_ERROR_REPLICATION_MODE_NONE,
|
||||
LIBRARY_ERROR_REPLICATION_MODE_FATAL,
|
||||
LIBRARY_ERROR_REPLICATION_MODE_NON_FATAL
|
||||
})
|
||||
@UnstableApi
|
||||
@interface LibraryErrorReplicationMode {}
|
||||
|
||||
/** No library service errors are replicated. */
|
||||
@UnstableApi public static final int LIBRARY_ERROR_REPLICATION_MODE_NONE = 0;
|
||||
|
||||
/**
|
||||
* {@linkplain SessionError Session errors} returned by the {@link MediaLibrarySession.Callback}
|
||||
* as part of a {@link LibraryResult} are replicated to the platform session as a fatal error.
|
||||
*/
|
||||
@UnstableApi public static final int LIBRARY_ERROR_REPLICATION_MODE_FATAL = 1;
|
||||
|
||||
/**
|
||||
* {@linkplain SessionError Session errors} returned by the {@link MediaLibrarySession.Callback}
|
||||
* as part of a {@link LibraryResult} are replicated to the platform session as a non-fatal
|
||||
* error.
|
||||
*/
|
||||
@UnstableApi public static final int LIBRARY_ERROR_REPLICATION_MODE_NON_FATAL = 2;
|
||||
|
||||
/**
|
||||
* An extended {@link MediaSession.Callback} for the {@link MediaLibrarySession}.
|
||||
*
|
||||
@ -393,6 +434,8 @@ public abstract class MediaLibraryService extends MediaSessionService {
|
||||
// constructor.
|
||||
public static final class Builder extends BuilderBase<MediaLibrarySession, Builder, Callback> {
|
||||
|
||||
private @LibraryErrorReplicationMode int libraryErrorReplicationMode;
|
||||
|
||||
/**
|
||||
* Creates a builder for {@link MediaLibrarySession}.
|
||||
*
|
||||
@ -407,7 +450,7 @@ public abstract class MediaLibraryService extends MediaSessionService {
|
||||
// Ideally it's better to make it inner class of service to enforce, but it violates API
|
||||
// guideline that Builders should be the inner class of the building target.
|
||||
public Builder(MediaLibraryService service, Player player, Callback callback) {
|
||||
super(service, player, callback);
|
||||
this((Context) service, player, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -421,6 +464,7 @@ public abstract class MediaLibraryService extends MediaSessionService {
|
||||
@UnstableApi
|
||||
public Builder(Context context, Player player, Callback callback) {
|
||||
super(context, player, callback);
|
||||
libraryErrorReplicationMode = LIBRARY_ERROR_REPLICATION_MODE_FATAL;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -560,6 +604,37 @@ public abstract class MediaLibraryService extends MediaSessionService {
|
||||
return super.setPeriodicPositionUpdateEnabled(isEnabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether library result errors should be replicated to the platform media session's
|
||||
* {@link android.media.session.PlaybackState} as a fatal error, a non-fatal error or not
|
||||
* replicated at all. Replication is only executed for calling legacy browsers ({@link
|
||||
* android.support.v4.media.MediaBrowserCompat} and {@link android.media.browse.MediaBrowser})
|
||||
* to which no error codes can be transmitted as a result of the service call.
|
||||
*
|
||||
* <p>The default is {@link #LIBRARY_ERROR_REPLICATION_MODE_FATAL}.
|
||||
*
|
||||
* <p>{@link MediaLibrarySession.Callback#onGetLibraryRoot} is exempted from replication,
|
||||
* because this method is part of the connection process of a legacy browser.
|
||||
*
|
||||
* <p>The following error codes are replicated:
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@link SessionError#ERROR_SESSION_AUTHENTICATION_EXPIRED}
|
||||
* <li>{@link SessionError#ERROR_SESSION_PREMIUM_ACCOUNT_REQUIRED}
|
||||
* </ul>
|
||||
*
|
||||
* <p>See {@link MediaLibrarySession#clearReplicatedLibraryError} also.
|
||||
*
|
||||
* @param libraryErrorReplicationMode The mode to use.
|
||||
*/
|
||||
@UnstableApi
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setLibraryErrorReplicationMode(
|
||||
@LibraryErrorReplicationMode int libraryErrorReplicationMode) {
|
||||
this.libraryErrorReplicationMode = libraryErrorReplicationMode;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a {@link MediaLibrarySession}.
|
||||
*
|
||||
@ -583,7 +658,8 @@ public abstract class MediaLibraryService extends MediaSessionService {
|
||||
sessionExtras,
|
||||
checkNotNull(bitmapLoader),
|
||||
playIfSuppressed,
|
||||
isPeriodicPositionUpdateEnabled);
|
||||
isPeriodicPositionUpdateEnabled,
|
||||
libraryErrorReplicationMode);
|
||||
}
|
||||
}
|
||||
|
||||
@ -598,7 +674,8 @@ public abstract class MediaLibraryService extends MediaSessionService {
|
||||
Bundle sessionExtras,
|
||||
BitmapLoader bitmapLoader,
|
||||
boolean playIfSuppressed,
|
||||
boolean isPeriodicPositionUpdateEnabled) {
|
||||
boolean isPeriodicPositionUpdateEnabled,
|
||||
@LibraryErrorReplicationMode int libraryErrorReplicationMode) {
|
||||
super(
|
||||
context,
|
||||
id,
|
||||
@ -610,7 +687,8 @@ public abstract class MediaLibraryService extends MediaSessionService {
|
||||
sessionExtras,
|
||||
bitmapLoader,
|
||||
playIfSuppressed,
|
||||
isPeriodicPositionUpdateEnabled);
|
||||
isPeriodicPositionUpdateEnabled,
|
||||
libraryErrorReplicationMode);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -625,7 +703,8 @@ public abstract class MediaLibraryService extends MediaSessionService {
|
||||
Bundle sessionExtras,
|
||||
BitmapLoader bitmapLoader,
|
||||
boolean playIfSuppressed,
|
||||
boolean isPeriodicPositionUpdateEnabled) {
|
||||
boolean isPeriodicPositionUpdateEnabled,
|
||||
@LibraryErrorReplicationMode int libraryErrorReplicationMode) {
|
||||
return new MediaLibrarySessionImpl(
|
||||
this,
|
||||
context,
|
||||
@ -638,7 +717,8 @@ public abstract class MediaLibraryService extends MediaSessionService {
|
||||
sessionExtras,
|
||||
bitmapLoader,
|
||||
playIfSuppressed,
|
||||
isPeriodicPositionUpdateEnabled);
|
||||
isPeriodicPositionUpdateEnabled,
|
||||
libraryErrorReplicationMode);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -718,6 +798,25 @@ public abstract class MediaLibraryService extends MediaSessionService {
|
||||
.notifySearchResultChanged(
|
||||
checkNotNull(browser), checkNotEmpty(query), itemCount, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the replicated library error in the platform session that was set when a {@link
|
||||
* LibraryResult} with an error result code was returned by the {@link
|
||||
* MediaLibrarySession.Callback} that is replicated and if {@linkplain
|
||||
* MediaLibrarySession.Builder#setLibraryErrorReplicationMode(int) legacy session error
|
||||
* replication is not turned off}.
|
||||
*
|
||||
* <p>Note: If a {@link LibraryResult#RESULT_SUCCESS} was returned by a method of {@link
|
||||
* MediaLibrarySession.Callback} that is considered for replication, the error is cleared
|
||||
* automatically by the library.
|
||||
*
|
||||
* <p>Calling this method updates the platform session playback state in case there was a
|
||||
* replicated error set. If no error was set, calling this method is a no-op.
|
||||
*/
|
||||
@UnstableApi
|
||||
public void clearReplicatedLibraryError() {
|
||||
getImpl().clearReplicatedLibraryError();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -18,11 +18,9 @@ package androidx.media3.session;
|
||||
import static androidx.media3.common.util.Assertions.checkNotNull;
|
||||
import static androidx.media3.common.util.Assertions.checkState;
|
||||
import static androidx.media3.session.LibraryResult.RESULT_SUCCESS;
|
||||
import static androidx.media3.session.MediaConstants.ERROR_CODE_AUTHENTICATION_EXPIRED_COMPAT;
|
||||
import static androidx.media3.session.MediaConstants.EXTRAS_KEY_ERROR_RESOLUTION_ACTION_INTENT_COMPAT;
|
||||
import static androidx.media3.session.SessionError.ERROR_INVALID_STATE;
|
||||
import static androidx.media3.session.SessionError.ERROR_NOT_SUPPORTED;
|
||||
import static androidx.media3.session.SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED;
|
||||
import static androidx.media3.session.SessionError.ERROR_UNKNOWN;
|
||||
import static java.lang.Math.max;
|
||||
import static java.lang.Math.min;
|
||||
@ -43,6 +41,7 @@ import androidx.media3.session.MediaLibraryService.MediaLibrarySession;
|
||||
import androidx.media3.session.MediaSession.ControllerCb;
|
||||
import androidx.media3.session.MediaSession.ControllerInfo;
|
||||
import androidx.media3.session.legacy.MediaSessionCompat;
|
||||
import androidx.media3.session.legacy.PlaybackStateCompat;
|
||||
import com.google.common.collect.HashMultimap;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
@ -66,6 +65,8 @@ import java.util.concurrent.Future;
|
||||
private final HashMultimap<String, ControllerInfo> parentIdToSubscribedControllers;
|
||||
private final HashMultimap<ControllerCb, String> controllerToSubscribedParentIds;
|
||||
|
||||
private final @MediaLibrarySession.LibraryErrorReplicationMode int libraryErrorReplicationMode;
|
||||
|
||||
/** Creates an instance. */
|
||||
public MediaLibrarySessionImpl(
|
||||
MediaLibrarySession instance,
|
||||
@ -79,7 +80,8 @@ import java.util.concurrent.Future;
|
||||
Bundle sessionExtras,
|
||||
BitmapLoader bitmapLoader,
|
||||
boolean playIfSuppressed,
|
||||
boolean isPeriodicPositionUpdateEnabled) {
|
||||
boolean isPeriodicPositionUpdateEnabled,
|
||||
@MediaLibrarySession.LibraryErrorReplicationMode int libraryErrorReplicationMode) {
|
||||
super(
|
||||
instance,
|
||||
context,
|
||||
@ -95,6 +97,7 @@ import java.util.concurrent.Future;
|
||||
isPeriodicPositionUpdateEnabled);
|
||||
this.instance = instance;
|
||||
this.callback = callback;
|
||||
this.libraryErrorReplicationMode = libraryErrorReplicationMode;
|
||||
parentIdToSubscribedControllers = HashMultimap.create();
|
||||
controllerToSubscribedParentIds = HashMultimap.create();
|
||||
}
|
||||
@ -119,6 +122,14 @@ import java.util.concurrent.Future;
|
||||
&& legacyStub.getConnectedControllersManager().isConnected(controller);
|
||||
}
|
||||
|
||||
public void clearReplicatedLibraryError() {
|
||||
PlayerWrapper playerWrapper = getPlayerWrapper();
|
||||
if (playerWrapper.getLegacyError() != null) {
|
||||
playerWrapper.clearLegacyErrorStatus();
|
||||
getSessionCompat().setPlaybackState(playerWrapper.createPlaybackStateCompat());
|
||||
}
|
||||
}
|
||||
|
||||
public ListenableFuture<LibraryResult<MediaItem>> onGetLibraryRootOnHandler(
|
||||
ControllerInfo browser, @Nullable LibraryParams params) {
|
||||
if (params != null && params.isRecent && isSystemUiController(browser)) {
|
||||
@ -137,17 +148,7 @@ import java.util.concurrent.Future;
|
||||
.build(),
|
||||
params));
|
||||
}
|
||||
ListenableFuture<LibraryResult<MediaItem>> future =
|
||||
callback.onGetLibraryRoot(instance, resolveControllerInfoForCallback(browser), params);
|
||||
future.addListener(
|
||||
() -> {
|
||||
@Nullable LibraryResult<MediaItem> result = tryGetFutureResult(future);
|
||||
if (result != null) {
|
||||
maybeUpdateLegacyErrorState(result);
|
||||
}
|
||||
},
|
||||
this::postOrRunOnApplicationHandler);
|
||||
return future;
|
||||
return callback.onGetLibraryRoot(instance, resolveControllerInfoForCallback(browser), params);
|
||||
}
|
||||
|
||||
public ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> onGetChildrenOnHandler(
|
||||
@ -185,7 +186,7 @@ import java.util.concurrent.Future;
|
||||
() -> {
|
||||
@Nullable LibraryResult<ImmutableList<MediaItem>> result = tryGetFutureResult(future);
|
||||
if (result != null) {
|
||||
maybeUpdateLegacyErrorState(result);
|
||||
maybeUpdateLegacyErrorState(browser, result);
|
||||
verifyResultItems(result, pageSize);
|
||||
}
|
||||
},
|
||||
@ -201,7 +202,7 @@ import java.util.concurrent.Future;
|
||||
() -> {
|
||||
@Nullable LibraryResult<MediaItem> result = tryGetFutureResult(future);
|
||||
if (result != null) {
|
||||
maybeUpdateLegacyErrorState(result);
|
||||
maybeUpdateLegacyErrorState(browser, result);
|
||||
}
|
||||
},
|
||||
this::postOrRunOnApplicationHandler);
|
||||
@ -291,7 +292,7 @@ import java.util.concurrent.Future;
|
||||
() -> {
|
||||
@Nullable LibraryResult<Void> result = tryGetFutureResult(future);
|
||||
if (result != null) {
|
||||
maybeUpdateLegacyErrorState(result);
|
||||
maybeUpdateLegacyErrorState(browser, result);
|
||||
}
|
||||
},
|
||||
this::postOrRunOnApplicationHandler);
|
||||
@ -311,7 +312,7 @@ import java.util.concurrent.Future;
|
||||
() -> {
|
||||
@Nullable LibraryResult<ImmutableList<MediaItem>> result = tryGetFutureResult(future);
|
||||
if (result != null) {
|
||||
maybeUpdateLegacyErrorState(result);
|
||||
maybeUpdateLegacyErrorState(browser, result);
|
||||
verifyResultItems(result, pageSize);
|
||||
}
|
||||
},
|
||||
@ -369,44 +370,59 @@ import java.util.concurrent.Future;
|
||||
}
|
||||
}
|
||||
|
||||
private void maybeUpdateLegacyErrorState(LibraryResult<?> result) {
|
||||
private void maybeUpdateLegacyErrorState(ControllerInfo browser, LibraryResult<?> result) {
|
||||
if (libraryErrorReplicationMode == MediaLibrarySession.LIBRARY_ERROR_REPLICATION_MODE_NONE
|
||||
|| browser.getControllerVersion() != ControllerInfo.LEGACY_CONTROLLER_VERSION) {
|
||||
return;
|
||||
}
|
||||
PlayerWrapper playerWrapper = getPlayerWrapper();
|
||||
if (setLegacyAuthenticationExpiredErrorState(result)) {
|
||||
if (setLegacyErrorState(result)) {
|
||||
// Sync playback state if legacy error state changed.
|
||||
getSessionCompat().setPlaybackState(playerWrapper.createPlaybackStateCompat());
|
||||
} else if (playerWrapper.getLegacyError() != null && result.resultCode == RESULT_SUCCESS) {
|
||||
playerWrapper.clearLegacyErrorStatus();
|
||||
getSessionCompat().setPlaybackState(playerWrapper.createPlaybackStateCompat());
|
||||
} else if (result.resultCode == RESULT_SUCCESS) {
|
||||
clearReplicatedLibraryError();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean setLegacyAuthenticationExpiredErrorState(LibraryResult<?> result) {
|
||||
private boolean setLegacyErrorState(LibraryResult<?> result) {
|
||||
PlayerWrapper playerWrapper = getPlayerWrapper();
|
||||
PlayerWrapper.LegacyError legacyError = playerWrapper.getLegacyError();
|
||||
if (result.resultCode == ERROR_SESSION_AUTHENTICATION_EXPIRED
|
||||
&& (legacyError == null || legacyError.code != ERROR_SESSION_AUTHENTICATION_EXPIRED)) {
|
||||
// Mapping this error to the legacy error state provides backwards compatibility for the
|
||||
// Automotive OS sign-in.
|
||||
Bundle bundle = Bundle.EMPTY;
|
||||
if (result.params != null
|
||||
&& result.params.extras.containsKey(EXTRAS_KEY_ERROR_RESOLUTION_ACTION_INTENT_COMPAT)) {
|
||||
// Backwards compatibility for Callbacks before SessionError was introduced.
|
||||
bundle = result.params.extras;
|
||||
} else if (result.sessionError != null
|
||||
&& result.sessionError.extras.containsKey(
|
||||
EXTRAS_KEY_ERROR_RESOLUTION_ACTION_INTENT_COMPAT)) {
|
||||
bundle = result.sessionError.extras;
|
||||
if (isReplicationErrorCode(result.resultCode)) {
|
||||
@PlaybackStateCompat.ErrorCode
|
||||
int legacyErrorCode = LegacyConversions.convertToLegacyErrorCode(result.resultCode);
|
||||
@Nullable PlayerWrapper.LegacyError legacyError = playerWrapper.getLegacyError();
|
||||
if (legacyError == null || legacyError.code != legacyErrorCode) {
|
||||
// Mapping this error to the legacy error state provides backwards compatibility for the
|
||||
// documented AAOS error flow:
|
||||
// https://developer.android.com/training/cars/media/automotive-os#-error-handling
|
||||
String errorMessage =
|
||||
result.sessionError != null
|
||||
? result.sessionError.message
|
||||
: SessionError.DEFAULT_ERROR_MESSAGE;
|
||||
Bundle bundle = Bundle.EMPTY;
|
||||
if (result.params != null
|
||||
&& result.params.extras.containsKey(EXTRAS_KEY_ERROR_RESOLUTION_ACTION_INTENT_COMPAT)) {
|
||||
// Backwards compatibility for Callbacks before SessionError was introduced.
|
||||
bundle = result.params.extras;
|
||||
} else if (result.sessionError != null) {
|
||||
bundle = result.sessionError.extras;
|
||||
}
|
||||
playerWrapper.setLegacyError(
|
||||
/* isFatal= */ libraryErrorReplicationMode
|
||||
== MediaLibrarySession.LIBRARY_ERROR_REPLICATION_MODE_FATAL,
|
||||
legacyErrorCode,
|
||||
errorMessage,
|
||||
bundle);
|
||||
return true;
|
||||
}
|
||||
playerWrapper.setLegacyError(
|
||||
/* isFatal= */ true,
|
||||
ERROR_CODE_AUTHENTICATION_EXPIRED_COMPAT,
|
||||
getContext().getString(R.string.error_message_authentication_expired),
|
||||
bundle);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isReplicationErrorCode(@LibraryResult.Code int resultCode) {
|
||||
return resultCode == LibraryResult.RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED
|
||||
|| resultCode == LibraryResult.RESULT_ERROR_SESSION_PARENTAL_CONTROL_RESTRICTED;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static <T> T tryGetFutureResult(Future<T> future) {
|
||||
checkState(future.isDone());
|
||||
|
@ -450,7 +450,8 @@ public class MediaSession {
|
||||
sessionExtras,
|
||||
checkNotNull(bitmapLoader),
|
||||
playIfSuppressed,
|
||||
isPeriodicPositionUpdateEnabled);
|
||||
isPeriodicPositionUpdateEnabled,
|
||||
MediaLibrarySession.LIBRARY_ERROR_REPLICATION_MODE_NONE);
|
||||
}
|
||||
}
|
||||
|
||||
@ -678,7 +679,8 @@ public class MediaSession {
|
||||
Bundle sessionExtras,
|
||||
BitmapLoader bitmapLoader,
|
||||
boolean playIfSuppressed,
|
||||
boolean isPeriodicPositionUpdateEnabled) {
|
||||
boolean isPeriodicPositionUpdateEnabled,
|
||||
@MediaLibrarySession.LibraryErrorReplicationMode int libraryErrorReplicationMode) {
|
||||
synchronized (STATIC_LOCK) {
|
||||
if (SESSION_ID_TO_SESSION_MAP.containsKey(id)) {
|
||||
throw new IllegalStateException("Session ID must be unique. ID=" + id);
|
||||
@ -697,7 +699,8 @@ public class MediaSession {
|
||||
sessionExtras,
|
||||
bitmapLoader,
|
||||
playIfSuppressed,
|
||||
isPeriodicPositionUpdateEnabled);
|
||||
isPeriodicPositionUpdateEnabled,
|
||||
libraryErrorReplicationMode);
|
||||
}
|
||||
|
||||
/* package */ MediaSessionImpl createImpl(
|
||||
@ -711,7 +714,8 @@ public class MediaSession {
|
||||
Bundle sessionExtras,
|
||||
BitmapLoader bitmapLoader,
|
||||
boolean playIfSuppressed,
|
||||
boolean isPeriodicPositionUpdateEnabled) {
|
||||
boolean isPeriodicPositionUpdateEnabled,
|
||||
@MediaLibrarySession.LibraryErrorReplicationMode int libraryErrorReplicationMode) {
|
||||
return new MediaSessionImpl(
|
||||
this,
|
||||
context,
|
||||
@ -1167,12 +1171,9 @@ public class MediaSession {
|
||||
* <p>This will call {@link MediaController.Listener#onError(MediaController, SessionError)} of
|
||||
* the given connected 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.
|
||||
* <p>When an error is sent to {@linkplain MediaSession#getMediaNotificationControllerInfo()} or a
|
||||
* legacy controller, the error of the {@linkplain android.media.session.PlaybackState playback
|
||||
* state} of the platform session is updated accordingly.
|
||||
*
|
||||
* @param controllerInfo The controller to send the error to.
|
||||
* @param sessionError The session error.
|
||||
@ -1181,13 +1182,11 @@ public class MediaSession {
|
||||
*/
|
||||
@UnstableApi
|
||||
public final void sendError(ControllerInfo controllerInfo, SessionError sessionError) {
|
||||
checkArgument(
|
||||
controllerInfo.getControllerVersion() != ControllerInfo.LEGACY_CONTROLLER_VERSION);
|
||||
impl.sendError(controllerInfo, sessionError);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a non-fatal error to all connected Media3 controllers.
|
||||
* Sends a non-fatal error to all connected controllers.
|
||||
*
|
||||
* <p>See {@link #sendError(ControllerInfo, SessionError)} for sending an error to a specific
|
||||
* controller only.
|
||||
|
@ -626,25 +626,38 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
}
|
||||
|
||||
public void sendError(ControllerInfo controllerInfo, SessionError sessionError) {
|
||||
if (controllerInfo.getInterfaceVersion() < 4) {
|
||||
if (controllerInfo.getControllerVersion() != ControllerInfo.LEGACY_CONTROLLER_VERSION
|
||||
&& controllerInfo.getInterfaceVersion() < 4) {
|
||||
// IMediaController.onError introduced with interface version 4.
|
||||
return;
|
||||
}
|
||||
dispatchRemoteControllerTaskWithoutReturn(
|
||||
controllerInfo, (callback, seq) -> callback.onError(seq, sessionError));
|
||||
if (isMediaNotificationController(controllerInfo)) {
|
||||
if (isMediaNotificationController(controllerInfo)
|
||||
|| controllerInfo.getControllerVersion() == ControllerInfo.LEGACY_CONTROLLER_VERSION) {
|
||||
// Media notification controller or legacy controllers update the platform session.
|
||||
dispatchRemoteControllerTaskToLegacyStub(
|
||||
(callback, seq) -> callback.onError(seq, sessionError));
|
||||
} else {
|
||||
// Media3 controller are notified individually.
|
||||
dispatchRemoteControllerTaskWithoutReturn(
|
||||
controllerInfo, (callback, seq) -> callback.onError(seq, sessionError));
|
||||
}
|
||||
}
|
||||
|
||||
public void sendError(SessionError sessionError) {
|
||||
// Send error messages only to Media3 controllers.
|
||||
// Send error messages to Media3 controllers.
|
||||
ImmutableList<ControllerInfo> connectedControllers =
|
||||
sessionStub.getConnectedControllersManager().getConnectedControllers();
|
||||
for (int i = 0; i < connectedControllers.size(); i++) {
|
||||
sendError(connectedControllers.get(i), sessionError);
|
||||
ControllerInfo controllerInfo = connectedControllers.get(i);
|
||||
if (!isMediaNotificationController(controllerInfo)) {
|
||||
// Omit sending to the media notification controller. Instead the error will be dispatched
|
||||
// through the legacy stub below to avoid updating the legacy session multiple times.
|
||||
sendError(controllerInfo, sessionError);
|
||||
}
|
||||
}
|
||||
// Send error messages to legacy controllers.
|
||||
dispatchRemoteControllerTaskToLegacyStub(
|
||||
(callback, seq) -> callback.onError(seq, sessionError));
|
||||
}
|
||||
|
||||
public MediaSession.ConnectionResult onConnectOnHandler(ControllerInfo controller) {
|
||||
|
@ -23,6 +23,8 @@ import android.os.Bundle;
|
||||
public class CommonConstants {
|
||||
|
||||
public static final String SUPPORT_APP_PACKAGE_NAME = "androidx.media3.test.session";
|
||||
public static final String MEDIA_CONTROLLER_PACKAGE_NAME_API_21 =
|
||||
"android.media.session.MediaController";
|
||||
|
||||
public static final ComponentName MEDIA3_SESSION_PROVIDER_SERVICE =
|
||||
new ComponentName(
|
||||
|
@ -38,6 +38,7 @@ public class MediaBrowserConstants {
|
||||
public static final String PARENT_ID_NO_CHILDREN = "parent_id_no_children";
|
||||
public static final String PARENT_ID_ERROR = "parent_id_error";
|
||||
public static final String PARENT_ID_AUTH_EXPIRED_ERROR = "parent_auth_expired_error";
|
||||
public static final String PARENT_ID_SKIP_LIMIT_REACHED_ERROR = "parent_skip_limit_reached_error";
|
||||
public static final String PARENT_ID_AUTH_EXPIRED_ERROR_DEPRECATED =
|
||||
"parent_auth_expired_error_deprecated";
|
||||
public static final String PARENT_ID_AUTH_EXPIRED_ERROR_NON_FATAL =
|
||||
@ -70,6 +71,8 @@ public class MediaBrowserConstants {
|
||||
"notify_children_changed_delay";
|
||||
public static final String EXTRAS_KEY_NOTIFY_CHILDREN_CHANGED_BROADCAST =
|
||||
"notify_children_changed_broadcast";
|
||||
public static final String CONNECTION_HINTS_KEY_LIBRARY_ERROR_REPLICATION_MODE =
|
||||
"error_replication_mode";
|
||||
|
||||
public static final String CUSTOM_ACTION = "customAction";
|
||||
public static final Bundle CUSTOM_ACTION_EXTRAS = new Bundle();
|
||||
|
@ -18,6 +18,8 @@ package androidx.media3.session;
|
||||
import static androidx.media.utils.MediaConstants.BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED;
|
||||
import static androidx.media3.session.MediaConstants.EXTRAS_KEY_COMPLETION_STATUS;
|
||||
import static androidx.media3.session.MediaConstants.EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED;
|
||||
import static androidx.media3.session.MediaLibraryService.MediaLibrarySession.LIBRARY_ERROR_REPLICATION_MODE_NONE;
|
||||
import static androidx.media3.session.MediaLibraryService.MediaLibrarySession.LIBRARY_ERROR_REPLICATION_MODE_NON_FATAL;
|
||||
import static androidx.media3.session.MockMediaLibraryService.CONNECTION_HINTS_CUSTOM_LIBRARY_ROOT;
|
||||
import static androidx.media3.session.MockMediaLibraryService.createNotifyChildrenChangedBundle;
|
||||
import static androidx.media3.test.session.common.CommonConstants.METADATA_ALBUM_TITLE;
|
||||
@ -29,6 +31,7 @@ import static androidx.media3.test.session.common.CommonConstants.METADATA_MEDIA
|
||||
import static androidx.media3.test.session.common.CommonConstants.METADATA_TITLE;
|
||||
import static androidx.media3.test.session.common.CommonConstants.MOCK_MEDIA3_LIBRARY_SERVICE;
|
||||
import static androidx.media3.test.session.common.MediaBrowserConstants.CHILDREN_COUNT;
|
||||
import static androidx.media3.test.session.common.MediaBrowserConstants.CONNECTION_HINTS_KEY_LIBRARY_ERROR_REPLICATION_MODE;
|
||||
import static androidx.media3.test.session.common.MediaBrowserConstants.CUSTOM_ACTION;
|
||||
import static androidx.media3.test.session.common.MediaBrowserConstants.CUSTOM_ACTION_EXTRAS;
|
||||
import static androidx.media3.test.session.common.MediaBrowserConstants.GET_CHILDREN_RESULT;
|
||||
@ -43,6 +46,7 @@ import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_I
|
||||
import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_ERROR;
|
||||
import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_LONG_LIST;
|
||||
import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_NO_CHILDREN;
|
||||
import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_SKIP_LIMIT_REACHED_ERROR;
|
||||
import static androidx.media3.test.session.common.MediaBrowserConstants.ROOT_EXTRAS;
|
||||
import static androidx.media3.test.session.common.MediaBrowserConstants.ROOT_EXTRAS_KEY;
|
||||
import static androidx.media3.test.session.common.MediaBrowserConstants.ROOT_EXTRAS_VALUE;
|
||||
@ -78,6 +82,7 @@ import androidx.media3.test.session.common.TestUtils;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.ext.truth.os.BundleSubject;
|
||||
import androidx.test.filters.LargeTest;
|
||||
import com.google.common.collect.Iterables;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
@ -382,21 +387,44 @@ public class MediaBrowserCompatWithMediaLibraryServiceTest
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getChildren_authErrorResult_correctPlaybackStateCompatUpdates() throws Exception {
|
||||
assertGetChildrenAuthenticationRequired(PARENT_ID_AUTH_EXPIRED_ERROR);
|
||||
public void getChildren_errorResultWithDefaultErrorReplication_legacyPlaybackStateWithFatalError()
|
||||
throws Exception {
|
||||
connectAndWait(/* rootHints= */ Bundle.EMPTY);
|
||||
subscribeAndAssertServiceCallbackErrorWithAuthErrorReplicated(
|
||||
PARENT_ID_AUTH_EXPIRED_ERROR, /* assertFatalError= */ true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getChildren_authErrorResultDeprecated_correctPlaybackStateCompatUpdates()
|
||||
throws Exception {
|
||||
public void
|
||||
getChildren_deprecatedErrorResultWithDefaultErrorReplication_legacyPlaybackStateWithFatalError()
|
||||
throws Exception {
|
||||
connectAndWait(/* rootHints= */ Bundle.EMPTY);
|
||||
// Tests the deprecated approach where apps were expected to pass the error extras back as the
|
||||
// extras of the LibraryParams of the LibraryResult because the SessionError type didn't then
|
||||
// exist as part of the LibraryResult.
|
||||
assertGetChildrenAuthenticationRequired(PARENT_ID_AUTH_EXPIRED_ERROR_DEPRECATED);
|
||||
subscribeAndAssertServiceCallbackErrorWithAuthErrorReplicated(
|
||||
PARENT_ID_AUTH_EXPIRED_ERROR_DEPRECATED, /* assertFatalError= */ true);
|
||||
}
|
||||
|
||||
public void assertGetChildrenAuthenticationRequired(String authExpiredParentId) throws Exception {
|
||||
@Test
|
||||
public void
|
||||
getChildren_errorResultWithNonFatalErrorReplication_legacyPlaybackStateWithNonFatalError()
|
||||
throws Exception {
|
||||
Bundle connectionHints = new Bundle();
|
||||
connectionHints.putInt(
|
||||
CONNECTION_HINTS_KEY_LIBRARY_ERROR_REPLICATION_MODE,
|
||||
LIBRARY_ERROR_REPLICATION_MODE_NON_FATAL);
|
||||
connectForServiceStartWithConnectionHints(connectionHints);
|
||||
connectAndWait(/* rootHints= */ Bundle.EMPTY);
|
||||
// Tests the deprecated approach where apps were expected to pass the error extras back as the
|
||||
// extras of the LibraryParams of the LibraryResult because the SessionError type didn't then
|
||||
// exist as part of the LibraryResult.
|
||||
subscribeAndAssertServiceCallbackErrorWithAuthErrorReplicated(
|
||||
PARENT_ID_AUTH_EXPIRED_ERROR, /* assertFatalError= */ false);
|
||||
}
|
||||
|
||||
private void subscribeAndAssertServiceCallbackErrorWithAuthErrorReplicated(
|
||||
String authExpiredParentId, boolean assertFatalError) throws Exception {
|
||||
CountDownLatch errorLatch = new CountDownLatch(1);
|
||||
AtomicReference<String> parentIdRefOnError = new AtomicReference<>();
|
||||
|
||||
@ -413,10 +441,15 @@ public class MediaBrowserCompatWithMediaLibraryServiceTest
|
||||
assertThat(errorLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
|
||||
assertThat(parentIdRefOnError.get()).isEqualTo(authExpiredParentId);
|
||||
assertThat(firstPlaybackStateCompatReported.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
|
||||
assertThat(lastReportedPlaybackStateCompat.getState())
|
||||
.isEqualTo(PlaybackStateCompat.STATE_ERROR);
|
||||
if (assertFatalError) {
|
||||
assertThat(Iterables.getLast(reportedPlaybackStatesCompat).getState())
|
||||
.isEqualTo(PlaybackStateCompat.STATE_ERROR);
|
||||
} else {
|
||||
assertThat(Iterables.getLast(reportedPlaybackStatesCompat).getState())
|
||||
.isNotEqualTo(PlaybackStateCompat.STATE_ERROR);
|
||||
}
|
||||
assertThat(
|
||||
lastReportedPlaybackStateCompat
|
||||
Iterables.getLast(reportedPlaybackStatesCompat)
|
||||
.getExtras()
|
||||
.getString(MediaConstants.EXTRAS_KEY_ERROR_RESOLUTION_ACTION_LABEL_COMPAT))
|
||||
.isEqualTo(PARENT_ID_AUTH_EXPIRED_ERROR_KEY_ERROR_RESOLUTION_ACTION_LABEL);
|
||||
@ -437,15 +470,65 @@ public class MediaBrowserCompatWithMediaLibraryServiceTest
|
||||
assertThat(successLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
|
||||
assertThat(parentIdRefOnChildrenLoaded.get()).isEqualTo(PARENT_ID);
|
||||
// Any successful calls remove the error state,
|
||||
assertThat(lastReportedPlaybackStateCompat.getState())
|
||||
assertThat(Iterables.getLast(reportedPlaybackStatesCompat).getState())
|
||||
.isNotEqualTo(PlaybackStateCompat.STATE_ERROR);
|
||||
assertThat(
|
||||
lastReportedPlaybackStateCompat
|
||||
Iterables.getLast(reportedPlaybackStatesCompat)
|
||||
.getExtras()
|
||||
.getString(MediaConstants.EXTRAS_KEY_ERROR_RESOLUTION_ACTION_LABEL_COMPAT))
|
||||
.isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getChildren_errorResultWithErrorReplicationDisabled_errorNotReplicated()
|
||||
throws Exception {
|
||||
Bundle connectionHints = new Bundle();
|
||||
connectionHints.putInt(
|
||||
CONNECTION_HINTS_KEY_LIBRARY_ERROR_REPLICATION_MODE, LIBRARY_ERROR_REPLICATION_MODE_NONE);
|
||||
connectForServiceStartWithConnectionHints(connectionHints);
|
||||
connectAndWait(/* rootHints= */ Bundle.EMPTY);
|
||||
|
||||
subscribeAndAssertServiceCallbackErrorWithoutErrorReplication(PARENT_ID_AUTH_EXPIRED_ERROR);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getChildren_skipLimitReachedErrorResultDefaultErrorCodeSet_errorNotReplicated()
|
||||
throws Exception {
|
||||
connectAndWait(/* rootHints= */ Bundle.EMPTY);
|
||||
|
||||
subscribeAndAssertServiceCallbackErrorWithoutErrorReplication(
|
||||
PARENT_ID_SKIP_LIMIT_REACHED_ERROR);
|
||||
}
|
||||
|
||||
private void subscribeAndAssertServiceCallbackErrorWithoutErrorReplication(String parentId)
|
||||
throws InterruptedException {
|
||||
CountDownLatch errorLatch = new CountDownLatch(1);
|
||||
AtomicReference<String> parentIdRefOnError = new AtomicReference<>();
|
||||
browserCompat.subscribe(
|
||||
parentId,
|
||||
new SubscriptionCallback() {
|
||||
@Override
|
||||
public void onError(String parentId) {
|
||||
parentIdRefOnError.set(parentId);
|
||||
errorLatch.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
assertThat(errorLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
|
||||
assertThat(parentIdRefOnError.get()).isEqualTo(parentId);
|
||||
assertThat(reportedPlaybackStatesCompat).isEmpty();
|
||||
assertThat(controllerCompat.getPlaybackState().getErrorCode()).isEqualTo(0);
|
||||
assertThat(controllerCompat.getPlaybackState().getErrorMessage()).isNull();
|
||||
|
||||
// Trigger a playback state update to assert we never get a playback state with error reported.
|
||||
controllerCompat.getTransportControls().play();
|
||||
|
||||
assertThat(firstPlaybackStateCompatReported.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
|
||||
assertThat(reportedPlaybackStatesCompat).isNotEmpty();
|
||||
assertThat(controllerCompat.getPlaybackState().getErrorCode()).isEqualTo(0);
|
||||
assertThat(controllerCompat.getPlaybackState().getErrorMessage()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getChildren_emptyResult() throws Exception {
|
||||
String testParentId = PARENT_ID_NO_CHILDREN;
|
||||
|
@ -28,12 +28,18 @@ import android.support.v4.media.MediaBrowserCompat;
|
||||
import android.support.v4.media.session.MediaControllerCompat;
|
||||
import android.support.v4.media.session.PlaybackStateCompat;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.session.MediaLibraryService.MediaLibrarySession;
|
||||
import androidx.media3.test.session.common.HandlerThreadTestRule;
|
||||
import androidx.media3.test.session.common.TestHandler;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.LargeTest;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Ignore;
|
||||
@ -59,9 +65,10 @@ public class MediaBrowserCompatWithMediaSessionServiceTest {
|
||||
Context context;
|
||||
TestHandler handler;
|
||||
@Nullable MediaBrowserCompat browserCompat;
|
||||
@Nullable MediaController serviceStartController;
|
||||
@Nullable MediaControllerCompat controllerCompat;
|
||||
TestConnectionCallback connectionCallback;
|
||||
@Nullable PlaybackStateCompat lastReportedPlaybackStateCompat;
|
||||
List<PlaybackStateCompat> reportedPlaybackStatesCompat;
|
||||
@Nullable CountDownLatch firstPlaybackStateCompatReported;
|
||||
|
||||
@Before
|
||||
@ -70,20 +77,61 @@ public class MediaBrowserCompatWithMediaSessionServiceTest {
|
||||
handler = threadTestRule.getHandler();
|
||||
connectionCallback = new TestConnectionCallback();
|
||||
firstPlaybackStateCompatReported = new CountDownLatch(1);
|
||||
reportedPlaybackStatesCompat = new ArrayList<>();
|
||||
}
|
||||
|
||||
@After
|
||||
public void cleanUp() {
|
||||
public void cleanUp() throws Exception {
|
||||
if (browserCompat != null) {
|
||||
browserCompat.disconnect();
|
||||
browserCompat = null;
|
||||
}
|
||||
if (serviceStartController != null) {
|
||||
handler.postAndSync(() -> serviceStartController.release());
|
||||
}
|
||||
}
|
||||
|
||||
ComponentName getServiceComponent() {
|
||||
return MOCK_MEDIA3_SESSION_SERVICE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the service by connecting a Media3 controller and passing connection hints. The service
|
||||
* can then use the connection hints to build the session for instance with specific settings.
|
||||
*
|
||||
* <p>Note that a media1 {@link MediaBrowserCompat} can't send connection hints. The root hints of
|
||||
* the legacy browser end up in the {@link MediaLibraryService.LibraryParams} passed to {@link
|
||||
* MediaLibrarySession.Callback#onGetLibraryRoot(MediaLibraryService.MediaLibrarySession,
|
||||
* MediaSession.ControllerInfo, MediaLibraryService.LibraryParams)} as they aren't available
|
||||
* earlier.
|
||||
*/
|
||||
void connectForServiceStartWithConnectionHints(Bundle connectionHints) throws Exception {
|
||||
CountDownLatch latch = new CountDownLatch(/* count= */ 1);
|
||||
handler.postAndSync(
|
||||
() -> {
|
||||
ListenableFuture<MediaController> future =
|
||||
new MediaController.Builder(
|
||||
ApplicationProvider.getApplicationContext(),
|
||||
new SessionToken(
|
||||
ApplicationProvider.getApplicationContext(), getServiceComponent()))
|
||||
.setConnectionHints(connectionHints)
|
||||
.buildAsync();
|
||||
future.addListener(
|
||||
() -> {
|
||||
try {
|
||||
if (future.isDone()) {
|
||||
latch.countDown();
|
||||
}
|
||||
serviceStartController = future.get();
|
||||
} catch (ExecutionException | InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
},
|
||||
MoreExecutors.directExecutor());
|
||||
});
|
||||
assertThat(latch.await(SERVICE_CONNECTION_TIMEOUT_MS, MILLISECONDS)).isTrue();
|
||||
}
|
||||
|
||||
void connectAndWait(Bundle rootHints) throws Exception {
|
||||
handler.postAndSync(
|
||||
() -> {
|
||||
@ -138,7 +186,7 @@ public class MediaBrowserCompatWithMediaSessionServiceTest {
|
||||
new MediaControllerCompat.Callback() {
|
||||
@Override
|
||||
public void onPlaybackStateChanged(PlaybackStateCompat state) {
|
||||
lastReportedPlaybackStateCompat = state;
|
||||
reportedPlaybackStatesCompat.add(state);
|
||||
firstPlaybackStateCompatReported.countDown();
|
||||
}
|
||||
};
|
||||
|
@ -19,7 +19,7 @@ import static androidx.media3.session.LibraryResult.RESULT_SUCCESS;
|
||||
import static androidx.media3.session.MediaConstants.EXTRAS_KEY_COMPLETION_STATUS;
|
||||
import static androidx.media3.session.MediaConstants.EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED;
|
||||
import static androidx.media3.session.MockMediaLibraryService.createNotifyChildrenChangedBundle;
|
||||
import static androidx.media3.session.SessionError.ERROR_BAD_VALUE;
|
||||
import static androidx.media3.session.SessionError.ERROR_SESSION_SKIP_LIMIT_REACHED;
|
||||
import static androidx.media3.test.session.common.CommonConstants.MOCK_MEDIA3_LIBRARY_SERVICE;
|
||||
import static androidx.media3.test.session.common.MediaBrowserConstants.CUSTOM_ACTION_ASSERT_PARAMS;
|
||||
import static androidx.media3.test.session.common.MediaBrowserConstants.LONG_LIST_COUNT;
|
||||
@ -180,7 +180,7 @@ public class MediaBrowserListenerTest extends MediaControllerListenerTest {
|
||||
.getHandler()
|
||||
.postAndSync(() -> browser.getItem(mediaId))
|
||||
.get(TIMEOUT_MS, MILLISECONDS);
|
||||
assertThat(result.resultCode).isEqualTo(ERROR_BAD_VALUE);
|
||||
assertThat(result.resultCode).isEqualTo(ERROR_SESSION_SKIP_LIMIT_REACHED);
|
||||
assertThat(result.value).isNull();
|
||||
}
|
||||
|
||||
|
@ -21,9 +21,13 @@ import static androidx.media3.session.MediaConstants.EXTRAS_KEY_ERROR_RESOLUTION
|
||||
import static androidx.media3.session.MediaConstants.EXTRAS_KEY_ERROR_RESOLUTION_ACTION_LABEL_COMPAT;
|
||||
import static androidx.media3.session.MediaConstants.EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED;
|
||||
import static androidx.media3.session.MediaConstants.EXTRA_KEY_ROOT_CHILDREN_BROWSABLE_ONLY;
|
||||
import static androidx.media3.session.MediaLibraryService.MediaLibrarySession.LIBRARY_ERROR_REPLICATION_MODE_FATAL;
|
||||
import static androidx.media3.session.SessionError.ERROR_BAD_VALUE;
|
||||
import static androidx.media3.session.SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED;
|
||||
import static androidx.media3.session.SessionError.ERROR_SESSION_SKIP_LIMIT_REACHED;
|
||||
import static androidx.media3.test.session.common.CommonConstants.MEDIA_CONTROLLER_PACKAGE_NAME_API_21;
|
||||
import static androidx.media3.test.session.common.CommonConstants.SUPPORT_APP_PACKAGE_NAME;
|
||||
import static androidx.media3.test.session.common.MediaBrowserConstants.CONNECTION_HINTS_KEY_LIBRARY_ERROR_REPLICATION_MODE;
|
||||
import static androidx.media3.test.session.common.MediaBrowserConstants.CUSTOM_ACTION;
|
||||
import static androidx.media3.test.session.common.MediaBrowserConstants.CUSTOM_ACTION_ASSERT_PARAMS;
|
||||
import static androidx.media3.test.session.common.MediaBrowserConstants.CUSTOM_ACTION_EXTRAS;
|
||||
@ -44,6 +48,7 @@ import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_I
|
||||
import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_ERROR;
|
||||
import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_LONG_LIST;
|
||||
import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_NO_CHILDREN;
|
||||
import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_SKIP_LIMIT_REACHED_ERROR;
|
||||
import static androidx.media3.test.session.common.MediaBrowserConstants.ROOT_EXTRAS;
|
||||
import static androidx.media3.test.session.common.MediaBrowserConstants.ROOT_ID;
|
||||
import static androidx.media3.test.session.common.MediaBrowserConstants.ROOT_ID_SUPPORTS_BROWSABLE_CHILDREN_ONLY;
|
||||
@ -202,15 +207,25 @@ public class MockMediaLibraryService extends MediaLibraryService {
|
||||
|
||||
if (session == null) {
|
||||
MockPlayer player =
|
||||
new MockPlayer.Builder().setApplicationLooper(handlerThread.getLooper()).build();
|
||||
new MockPlayer.Builder()
|
||||
.setChangePlayerStateWithTransportControl(true)
|
||||
.setApplicationLooper(handlerThread.getLooper())
|
||||
.build();
|
||||
|
||||
MediaLibrarySession.Callback callback = registry.getSessionCallback();
|
||||
int libraryErrorReplicationMode =
|
||||
controllerInfo
|
||||
.getConnectionHints()
|
||||
.getInt(
|
||||
CONNECTION_HINTS_KEY_LIBRARY_ERROR_REPLICATION_MODE,
|
||||
LIBRARY_ERROR_REPLICATION_MODE_FATAL);
|
||||
session =
|
||||
new MediaLibrarySession.Builder(
|
||||
MockMediaLibraryService.this,
|
||||
player,
|
||||
callback != null ? callback : new TestLibrarySessionCallback())
|
||||
.setId(ID)
|
||||
.setLibraryErrorReplicationMode(libraryErrorReplicationMode)
|
||||
.build();
|
||||
}
|
||||
return session;
|
||||
@ -247,7 +262,8 @@ public class MockMediaLibraryService extends MediaLibraryService {
|
||||
@Override
|
||||
public MediaSession.ConnectionResult onConnect(
|
||||
MediaSession session, ControllerInfo controller) {
|
||||
if (!SUPPORT_APP_PACKAGE_NAME.equals(controller.getPackageName())) {
|
||||
if (!SUPPORT_APP_PACKAGE_NAME.equals(controller.getPackageName())
|
||||
&& !MEDIA_CONTROLLER_PACKAGE_NAME_API_21.equals(controller.getPackageName())) {
|
||||
return MediaSession.ConnectionResult.reject();
|
||||
}
|
||||
MediaSession.ConnectionResult connectionResult =
|
||||
@ -322,7 +338,7 @@ public class MockMediaLibraryService extends MediaLibraryService {
|
||||
LibraryResult.ofItem(createMediaItemWithMetadata(mediaId), /* params= */ null));
|
||||
default: // fall out
|
||||
}
|
||||
return Futures.immediateFuture(LibraryResult.ofError(ERROR_BAD_VALUE));
|
||||
return Futures.immediateFuture(LibraryResult.ofError(ERROR_SESSION_SKIP_LIMIT_REACHED));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -353,7 +369,8 @@ public class MockMediaLibraryService extends MediaLibraryService {
|
||||
return Futures.immediateFuture(
|
||||
LibraryResult.ofError(new SessionError(ERROR_BAD_VALUE, "error message", errorBundle)));
|
||||
} else if (Objects.equals(parentId, PARENT_ID_AUTH_EXPIRED_ERROR)
|
||||
|| Objects.equals(parentId, PARENT_ID_AUTH_EXPIRED_ERROR_DEPRECATED)) {
|
||||
|| Objects.equals(parentId, PARENT_ID_AUTH_EXPIRED_ERROR_DEPRECATED)
|
||||
|| Objects.equals(parentId, PARENT_ID_SKIP_LIMIT_REACHED_ERROR)) {
|
||||
Bundle bundle = new Bundle();
|
||||
Intent signInIntent = new Intent("action");
|
||||
int flags = Util.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0;
|
||||
@ -364,17 +381,21 @@ public class MockMediaLibraryService extends MediaLibraryService {
|
||||
bundle.putString(
|
||||
EXTRAS_KEY_ERROR_RESOLUTION_ACTION_LABEL_COMPAT,
|
||||
PARENT_ID_AUTH_EXPIRED_ERROR_KEY_ERROR_RESOLUTION_ACTION_LABEL);
|
||||
@SessionError.Code
|
||||
int errorCode =
|
||||
Objects.equals(parentId, PARENT_ID_SKIP_LIMIT_REACHED_ERROR)
|
||||
? ERROR_SESSION_SKIP_LIMIT_REACHED
|
||||
: ERROR_SESSION_AUTHENTICATION_EXPIRED;
|
||||
return Objects.equals(parentId, PARENT_ID_AUTH_EXPIRED_ERROR)
|
||||
? Futures.immediateFuture(
|
||||
// error with SessionError
|
||||
LibraryResult.ofError(
|
||||
new SessionError(ERROR_SESSION_AUTHENTICATION_EXPIRED, "error message", bundle),
|
||||
new SessionError(errorCode, "error message", bundle),
|
||||
new LibraryParams.Builder().build()))
|
||||
: Futures.immediateFuture(
|
||||
// deprecated error before SessionError was introduced
|
||||
LibraryResult.ofError(
|
||||
ERROR_SESSION_AUTHENTICATION_EXPIRED,
|
||||
new LibraryParams.Builder().setExtras(bundle).build()));
|
||||
errorCode, new LibraryParams.Builder().setExtras(bundle).build()));
|
||||
} else if (Objects.equals(parentId, PARENT_ID_AUTH_EXPIRED_ERROR_NON_FATAL)) {
|
||||
Bundle bundle = new Bundle();
|
||||
Intent signInIntent = new Intent("action");
|
||||
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
package androidx.media3.session;
|
||||
|
||||
import static androidx.media3.test.session.common.CommonConstants.MEDIA_CONTROLLER_PACKAGE_NAME_API_21;
|
||||
import static androidx.media3.test.session.common.CommonConstants.SUPPORT_APP_PACKAGE_NAME;
|
||||
|
||||
import android.content.Intent;
|
||||
@ -119,7 +120,8 @@ public class MockMediaSessionService extends MediaSessionService {
|
||||
@Override
|
||||
public MediaSession.ConnectionResult onConnect(
|
||||
MediaSession session, ControllerInfo controller) {
|
||||
if (TextUtils.equals(SUPPORT_APP_PACKAGE_NAME, controller.getPackageName())) {
|
||||
if (TextUtils.equals(SUPPORT_APP_PACKAGE_NAME, controller.getPackageName())
|
||||
|| TextUtils.equals(MEDIA_CONTROLLER_PACKAGE_NAME_API_21, controller.getPackageName())) {
|
||||
return MediaSession.Callback.super.onConnect(session, controller);
|
||||
}
|
||||
return MediaSession.ConnectionResult.reject();
|
||||
|
Loading…
x
Reference in New Issue
Block a user