diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 91bbfcbe6c..b137660096 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -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 diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaLibraryService.java b/libraries/session/src/main/java/androidx/media3/session/MediaLibraryService.java index 2712383a92..e8c7d50280 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaLibraryService.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaLibraryService.java @@ -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 { + 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. + * + *

The default is {@link #LIBRARY_ERROR_REPLICATION_MODE_FATAL}. + * + *

{@link MediaLibrarySession.Callback#onGetLibraryRoot} is exempted from replication, + * because this method is part of the connection process of a legacy browser. + * + *

The following error codes are replicated: + * + *

+ * + *

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}. + * + *

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. + * + *

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(); + } } /** diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaLibrarySessionImpl.java b/libraries/session/src/main/java/androidx/media3/session/MediaLibrarySessionImpl.java index c0f99e3135..6d288636f5 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaLibrarySessionImpl.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaLibrarySessionImpl.java @@ -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 parentIdToSubscribedControllers; private final HashMultimap 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> 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> future = - callback.onGetLibraryRoot(instance, resolveControllerInfoForCallback(browser), params); - future.addListener( - () -> { - @Nullable LibraryResult result = tryGetFutureResult(future); - if (result != null) { - maybeUpdateLegacyErrorState(result); - } - }, - this::postOrRunOnApplicationHandler); - return future; + return callback.onGetLibraryRoot(instance, resolveControllerInfoForCallback(browser), params); } public ListenableFuture>> onGetChildrenOnHandler( @@ -185,7 +186,7 @@ import java.util.concurrent.Future; () -> { @Nullable LibraryResult> 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 result = tryGetFutureResult(future); if (result != null) { - maybeUpdateLegacyErrorState(result); + maybeUpdateLegacyErrorState(browser, result); } }, this::postOrRunOnApplicationHandler); @@ -291,7 +292,7 @@ import java.util.concurrent.Future; () -> { @Nullable LibraryResult result = tryGetFutureResult(future); if (result != null) { - maybeUpdateLegacyErrorState(result); + maybeUpdateLegacyErrorState(browser, result); } }, this::postOrRunOnApplicationHandler); @@ -311,7 +312,7 @@ import java.util.concurrent.Future; () -> { @Nullable LibraryResult> 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 tryGetFutureResult(Future future) { checkState(future.isDone()); diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java index 5aa4ba1934..ac350eed60 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java @@ -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 { *

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

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

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. + *

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

See {@link #sendError(ControllerInfo, SessionError)} for sending an error to a specific * controller only. diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java index a6e4c12ecb..e67512598a 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java @@ -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 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) { diff --git a/libraries/test_session_common/src/main/java/androidx/media3/test/session/common/CommonConstants.java b/libraries/test_session_common/src/main/java/androidx/media3/test/session/common/CommonConstants.java index 6d710da272..46265af7a9 100644 --- a/libraries/test_session_common/src/main/java/androidx/media3/test/session/common/CommonConstants.java +++ b/libraries/test_session_common/src/main/java/androidx/media3/test/session/common/CommonConstants.java @@ -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( diff --git a/libraries/test_session_common/src/main/java/androidx/media3/test/session/common/MediaBrowserConstants.java b/libraries/test_session_common/src/main/java/androidx/media3/test/session/common/MediaBrowserConstants.java index e1f3d19711..409aa5238a 100644 --- a/libraries/test_session_common/src/main/java/androidx/media3/test/session/common/MediaBrowserConstants.java +++ b/libraries/test_session_common/src/main/java/androidx/media3/test/session/common/MediaBrowserConstants.java @@ -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(); diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserCompatWithMediaLibraryServiceTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserCompatWithMediaLibraryServiceTest.java index 9d6b320183..02e895e7a4 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserCompatWithMediaLibraryServiceTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserCompatWithMediaLibraryServiceTest.java @@ -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 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 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; diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserCompatWithMediaSessionServiceTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserCompatWithMediaSessionServiceTest.java index 8bef4ca9f5..d779984220 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserCompatWithMediaSessionServiceTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserCompatWithMediaSessionServiceTest.java @@ -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 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. + * + *

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 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(); } }; diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserListenerTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserListenerTest.java index e70b120904..f79ac58f0e 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserListenerTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserListenerTest.java @@ -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(); } diff --git a/libraries/test_session_current/src/main/java/androidx/media3/session/MockMediaLibraryService.java b/libraries/test_session_current/src/main/java/androidx/media3/session/MockMediaLibraryService.java index 612b9dea22..ac918345e6 100644 --- a/libraries/test_session_current/src/main/java/androidx/media3/session/MockMediaLibraryService.java +++ b/libraries/test_session_current/src/main/java/androidx/media3/session/MockMediaLibraryService.java @@ -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"); diff --git a/libraries/test_session_current/src/main/java/androidx/media3/session/MockMediaSessionService.java b/libraries/test_session_current/src/main/java/androidx/media3/session/MockMediaSessionService.java index 3b31210e96..22f4801549 100644 --- a/libraries/test_session_current/src/main/java/androidx/media3/session/MockMediaSessionService.java +++ b/libraries/test_session_current/src/main/java/androidx/media3/session/MockMediaSessionService.java @@ -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();