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:
bachinger 2024-07-01 09:44:44 -07:00 committed by Copybara-Service
parent 6bf2461f80
commit 70c063905c
12 changed files with 384 additions and 93 deletions

View File

@ -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

View File

@ -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();
}
}
/**

View File

@ -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());

View File

@ -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.

View File

@ -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) {

View File

@ -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(

View File

@ -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();

View File

@ -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;

View File

@ -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();
}
};

View File

@ -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();
}

View File

@ -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");

View File

@ -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();