Add SessionError and use it in service results

This change adds `SessionError` and uses it in `SessionResult`
and `LibraryResult` to report errors to callers.

Constructors and factory method that used a simple `errorCode` to
construct error variants of `SessionResult` and `LibraryResult`
have been overloaded with a variant that uses a `SessionError`
instead. While these methods and constructors are supposed to be
deprecated, they aren't yet deprecated until the newly added
alternative is stabilized.

PiperOrigin-RevId: 642254336
This commit is contained in:
bachinger 2024-06-11 06:51:13 -07:00 committed by Copybara-Service
parent 06e95ad2fb
commit efff1ee2f1
32 changed files with 847 additions and 231 deletions

View File

@ -21,6 +21,9 @@
* Add `MediaSession.Callback.onPlayerInteractionFinished` to inform
sessions when a series of player interactions from a specific controller
finished.
* Add `SessionError` and use it in `SessionResult` and `LibraryResult`
instead of the error code to provide more information about the error
and how to resolve the error if possible.
* UI:
* Add customisation of various icons in `PlayerControlView` through xml
attributes to allow different drawables per `PlayerView` instance,

View File

@ -1511,7 +1511,7 @@ package androidx.media3.session {
field @Nullable public final V value;
}
@IntDef({androidx.media3.session.LibraryResult.RESULT_SUCCESS, androidx.media3.session.LibraryResult.RESULT_ERROR_UNKNOWN, androidx.media3.session.LibraryResult.RESULT_ERROR_INVALID_STATE, androidx.media3.session.LibraryResult.RESULT_ERROR_BAD_VALUE, androidx.media3.session.LibraryResult.RESULT_ERROR_PERMISSION_DENIED, androidx.media3.session.LibraryResult.RESULT_ERROR_IO, androidx.media3.session.LibraryResult.RESULT_INFO_SKIPPED, androidx.media3.session.LibraryResult.RESULT_ERROR_SESSION_DISCONNECTED, androidx.media3.session.LibraryResult.RESULT_ERROR_NOT_SUPPORTED, androidx.media3.session.LibraryResult.RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED, androidx.media3.session.LibraryResult.RESULT_ERROR_SESSION_PREMIUM_ACCOUNT_REQUIRED, androidx.media3.session.LibraryResult.RESULT_ERROR_SESSION_CONCURRENT_STREAM_LIMIT, androidx.media3.session.LibraryResult.RESULT_ERROR_SESSION_PARENTAL_CONTROL_RESTRICTED, androidx.media3.session.LibraryResult.RESULT_ERROR_SESSION_NOT_AVAILABLE_IN_REGION, androidx.media3.session.LibraryResult.RESULT_ERROR_SESSION_SKIP_LIMIT_REACHED, androidx.media3.session.LibraryResult.RESULT_ERROR_SESSION_SETUP_REQUIRED}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target(java.lang.annotation.ElementType.TYPE_USE) public static @interface LibraryResult.Code {
@IntDef({androidx.media3.session.LibraryResult.RESULT_SUCCESS, androidx.media3.session.SessionError.INFO_SKIPPED, androidx.media3.session.SessionError.ERROR_UNKNOWN, androidx.media3.session.SessionError.ERROR_INVALID_STATE, androidx.media3.session.SessionError.ERROR_BAD_VALUE, androidx.media3.session.SessionError.ERROR_PERMISSION_DENIED, androidx.media3.session.SessionError.ERROR_IO, androidx.media3.session.SessionError.ERROR_SESSION_DISCONNECTED, androidx.media3.session.SessionError.ERROR_NOT_SUPPORTED, androidx.media3.session.SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED, androidx.media3.session.SessionError.ERROR_SESSION_PREMIUM_ACCOUNT_REQUIRED, androidx.media3.session.SessionError.ERROR_SESSION_CONCURRENT_STREAM_LIMIT, androidx.media3.session.SessionError.ERROR_SESSION_PARENTAL_CONTROL_RESTRICTED, androidx.media3.session.SessionError.ERROR_SESSION_NOT_AVAILABLE_IN_REGION, androidx.media3.session.SessionError.ERROR_SESSION_SKIP_LIMIT_REACHED, androidx.media3.session.SessionError.ERROR_SESSION_SETUP_REQUIRED}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target(java.lang.annotation.ElementType.TYPE_USE) public static @interface LibraryResult.Code {
}
public final class MediaBrowser extends androidx.media3.session.MediaController {
@ -1866,7 +1866,7 @@ package androidx.media3.session {
field @androidx.media3.session.SessionResult.Code public final int resultCode;
}
@IntDef({androidx.media3.session.SessionResult.RESULT_SUCCESS, androidx.media3.session.SessionResult.RESULT_ERROR_UNKNOWN, androidx.media3.session.SessionResult.RESULT_ERROR_INVALID_STATE, androidx.media3.session.SessionResult.RESULT_ERROR_BAD_VALUE, androidx.media3.session.SessionResult.RESULT_ERROR_PERMISSION_DENIED, androidx.media3.session.SessionResult.RESULT_ERROR_IO, androidx.media3.session.SessionResult.RESULT_INFO_SKIPPED, androidx.media3.session.SessionResult.RESULT_ERROR_SESSION_DISCONNECTED, androidx.media3.session.SessionResult.RESULT_ERROR_NOT_SUPPORTED, androidx.media3.session.SessionResult.RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED, androidx.media3.session.SessionResult.RESULT_ERROR_SESSION_PREMIUM_ACCOUNT_REQUIRED, androidx.media3.session.SessionResult.RESULT_ERROR_SESSION_CONCURRENT_STREAM_LIMIT, androidx.media3.session.SessionResult.RESULT_ERROR_SESSION_PARENTAL_CONTROL_RESTRICTED, androidx.media3.session.SessionResult.RESULT_ERROR_SESSION_NOT_AVAILABLE_IN_REGION, androidx.media3.session.SessionResult.RESULT_ERROR_SESSION_SKIP_LIMIT_REACHED, androidx.media3.session.SessionResult.RESULT_ERROR_SESSION_SETUP_REQUIRED}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target(java.lang.annotation.ElementType.TYPE_USE) public static @interface SessionResult.Code {
@IntDef({androidx.media3.session.SessionResult.RESULT_SUCCESS, androidx.media3.session.SessionError.INFO_SKIPPED, androidx.media3.session.SessionError.ERROR_UNKNOWN, androidx.media3.session.SessionError.ERROR_INVALID_STATE, androidx.media3.session.SessionError.ERROR_BAD_VALUE, androidx.media3.session.SessionError.ERROR_PERMISSION_DENIED, androidx.media3.session.SessionError.ERROR_IO, androidx.media3.session.SessionError.ERROR_SESSION_DISCONNECTED, androidx.media3.session.SessionError.ERROR_NOT_SUPPORTED, androidx.media3.session.SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED, androidx.media3.session.SessionError.ERROR_SESSION_PREMIUM_ACCOUNT_REQUIRED, androidx.media3.session.SessionError.ERROR_SESSION_CONCURRENT_STREAM_LIMIT, androidx.media3.session.SessionError.ERROR_SESSION_PARENTAL_CONTROL_RESTRICTED, androidx.media3.session.SessionError.ERROR_SESSION_NOT_AVAILABLE_IN_REGION, androidx.media3.session.SessionError.ERROR_SESSION_SKIP_LIMIT_REACHED, androidx.media3.session.SessionError.ERROR_SESSION_SETUP_REQUIRED}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target(java.lang.annotation.ElementType.TYPE_USE) public static @interface SessionResult.Code {
}
public final class SessionToken {

View File

@ -27,6 +27,7 @@ import androidx.media3.session.MediaLibraryService
import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSession.MediaItemsWithStartPosition
import androidx.media3.session.SessionCommand
import androidx.media3.session.SessionError
import androidx.media3.session.SessionResult
import com.google.common.collect.ImmutableList
import com.google.common.util.concurrent.Futures
@ -132,7 +133,7 @@ open class DemoMediaLibrarySessionCallback(context: Context) :
MediaItemTree.getItem(mediaId)?.let {
return Futures.immediateFuture(LibraryResult.ofItem(it, /* params= */ null))
}
return Futures.immediateFuture(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE))
return Futures.immediateFuture(LibraryResult.ofError(SessionError.ERROR_BAD_VALUE))
}
override fun onGetChildren(
@ -147,7 +148,7 @@ open class DemoMediaLibrarySessionCallback(context: Context) :
if (children.isNotEmpty()) {
return Futures.immediateFuture(LibraryResult.ofItemList(children, params))
}
return Futures.immediateFuture(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE))
return Futures.immediateFuture(LibraryResult.ofError(SessionError.ERROR_BAD_VALUE))
}
override fun onAddMediaItems(

View File

@ -54,21 +54,21 @@ public final class LibraryResult<V> implements Bundleable {
@Target(TYPE_USE)
@IntDef({
RESULT_SUCCESS,
RESULT_ERROR_UNKNOWN,
RESULT_ERROR_INVALID_STATE,
RESULT_ERROR_BAD_VALUE,
RESULT_ERROR_PERMISSION_DENIED,
RESULT_ERROR_IO,
RESULT_INFO_SKIPPED,
RESULT_ERROR_SESSION_DISCONNECTED,
RESULT_ERROR_NOT_SUPPORTED,
RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED,
RESULT_ERROR_SESSION_PREMIUM_ACCOUNT_REQUIRED,
RESULT_ERROR_SESSION_CONCURRENT_STREAM_LIMIT,
RESULT_ERROR_SESSION_PARENTAL_CONTROL_RESTRICTED,
RESULT_ERROR_SESSION_NOT_AVAILABLE_IN_REGION,
RESULT_ERROR_SESSION_SKIP_LIMIT_REACHED,
RESULT_ERROR_SESSION_SETUP_REQUIRED
SessionError.INFO_SKIPPED,
SessionError.ERROR_UNKNOWN,
SessionError.ERROR_INVALID_STATE,
SessionError.ERROR_BAD_VALUE,
SessionError.ERROR_PERMISSION_DENIED,
SessionError.ERROR_IO,
SessionError.ERROR_SESSION_DISCONNECTED,
SessionError.ERROR_NOT_SUPPORTED,
SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED,
SessionError.ERROR_SESSION_PREMIUM_ACCOUNT_REQUIRED,
SessionError.ERROR_SESSION_CONCURRENT_STREAM_LIMIT,
SessionError.ERROR_SESSION_PARENTAL_CONTROL_RESTRICTED,
SessionError.ERROR_SESSION_NOT_AVAILABLE_IN_REGION,
SessionError.ERROR_SESSION_SKIP_LIMIT_REACHED,
SessionError.ERROR_SESSION_SETUP_REQUIRED
})
public @interface Code {}
@ -82,56 +82,64 @@ public final class LibraryResult<V> implements Bundleable {
*/
public static final int RESULT_SUCCESS = 0;
/** Result code representing that the command is skipped. */
public static final int RESULT_INFO_SKIPPED = SessionError.INFO_SKIPPED;
/** Result code representing that the command is ended with an unknown error. */
public static final int RESULT_ERROR_UNKNOWN = -1;
public static final int RESULT_ERROR_UNKNOWN = SessionError.ERROR_UNKNOWN;
/**
* Result code representing that the command cannot be completed because the current state is not
* valid for the command.
*/
public static final int RESULT_ERROR_INVALID_STATE = -2;
public static final int RESULT_ERROR_INVALID_STATE = SessionError.ERROR_INVALID_STATE;
/** Result code representing that an argument is illegal. */
public static final int RESULT_ERROR_BAD_VALUE = -3;
public static final int RESULT_ERROR_BAD_VALUE = SessionError.ERROR_BAD_VALUE;
/** Result code representing that the command is not allowed. */
public static final int RESULT_ERROR_PERMISSION_DENIED = -4;
public static final int RESULT_ERROR_PERMISSION_DENIED = SessionError.ERROR_PERMISSION_DENIED;
/** Result code representing that a file or network related error happened. */
public static final int RESULT_ERROR_IO = -5;
public static final int RESULT_ERROR_IO = SessionError.ERROR_IO;
/** Result code representing that the command is not supported. */
public static final int RESULT_ERROR_NOT_SUPPORTED = -6;
/** Result code representing that the command is skipped. */
public static final int RESULT_INFO_SKIPPED = 1;
public static final int RESULT_ERROR_NOT_SUPPORTED = SessionError.ERROR_NOT_SUPPORTED;
/** Result code representing that the session and controller were disconnected. */
public static final int RESULT_ERROR_SESSION_DISCONNECTED = -100;
public static final int RESULT_ERROR_SESSION_DISCONNECTED =
SessionError.ERROR_SESSION_DISCONNECTED;
/** Result code representing that the authentication has expired. */
public static final int RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED = -102;
public static final int RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED =
SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED;
/** Result code representing that a premium account is required. */
public static final int RESULT_ERROR_SESSION_PREMIUM_ACCOUNT_REQUIRED = -103;
public static final int RESULT_ERROR_SESSION_PREMIUM_ACCOUNT_REQUIRED =
SessionError.ERROR_SESSION_PREMIUM_ACCOUNT_REQUIRED;
/** Result code representing that too many concurrent streams are detected. */
public static final int RESULT_ERROR_SESSION_CONCURRENT_STREAM_LIMIT = -104;
public static final int RESULT_ERROR_SESSION_CONCURRENT_STREAM_LIMIT =
SessionError.ERROR_SESSION_CONCURRENT_STREAM_LIMIT;
/** Result code representing that the content is blocked due to parental controls. */
public static final int RESULT_ERROR_SESSION_PARENTAL_CONTROL_RESTRICTED = -105;
public static final int RESULT_ERROR_SESSION_PARENTAL_CONTROL_RESTRICTED =
SessionError.ERROR_SESSION_PARENTAL_CONTROL_RESTRICTED;
/** Result code representing that the content is blocked due to being regionally unavailable. */
public static final int RESULT_ERROR_SESSION_NOT_AVAILABLE_IN_REGION = -106;
public static final int RESULT_ERROR_SESSION_NOT_AVAILABLE_IN_REGION =
SessionError.ERROR_SESSION_NOT_AVAILABLE_IN_REGION;
/**
* Result code representing that the application cannot skip any more because the skip limit is
* reached.
*/
public static final int RESULT_ERROR_SESSION_SKIP_LIMIT_REACHED = -107;
public static final int RESULT_ERROR_SESSION_SKIP_LIMIT_REACHED =
SessionError.ERROR_SESSION_SKIP_LIMIT_REACHED;
/** Result code representing that the session needs user's manual intervention. */
public static final int RESULT_ERROR_SESSION_SETUP_REQUIRED = -108;
public static final int RESULT_ERROR_SESSION_SETUP_REQUIRED =
SessionError.ERROR_SESSION_SETUP_REQUIRED;
/** The {@link Code} of this result. */
public final @Code int resultCode;
@ -153,12 +161,16 @@ public final class LibraryResult<V> implements Bundleable {
/** The optional parameters. */
@Nullable public final MediaLibraryService.LibraryParams params;
/** The optional session error. */
@UnstableApi @Nullable public final SessionError sessionError;
/** Creates an instance with {@link #resultCode}{@code ==}{@link #RESULT_SUCCESS}. */
public static LibraryResult<Void> ofVoid() {
return new LibraryResult<>(
RESULT_SUCCESS,
SystemClock.elapsedRealtime(),
/* params= */ null,
/* sessionError= */ null,
/* value= */ null,
VALUE_TYPE_VOID);
}
@ -169,7 +181,12 @@ public final class LibraryResult<V> implements Bundleable {
*/
public static LibraryResult<Void> ofVoid(@Nullable LibraryParams params) {
return new LibraryResult<>(
RESULT_SUCCESS, SystemClock.elapsedRealtime(), params, /* value= */ null, VALUE_TYPE_VOID);
RESULT_SUCCESS,
SystemClock.elapsedRealtime(),
params,
/* sessionError= */ null,
/* value= */ null,
VALUE_TYPE_VOID);
}
/**
@ -184,7 +201,12 @@ public final class LibraryResult<V> implements Bundleable {
public static LibraryResult<MediaItem> ofItem(MediaItem item, @Nullable LibraryParams params) {
verifyMediaItem(item);
return new LibraryResult<>(
RESULT_SUCCESS, SystemClock.elapsedRealtime(), params, item, VALUE_TYPE_ITEM);
RESULT_SUCCESS,
SystemClock.elapsedRealtime(),
params,
/* sessionError= */ null,
item,
VALUE_TYPE_ITEM);
}
/**
@ -206,6 +228,7 @@ public final class LibraryResult<V> implements Bundleable {
RESULT_SUCCESS,
SystemClock.elapsedRealtime(),
params,
/* sessionError= */ null,
ImmutableList.copyOf(items),
VALUE_TYPE_ITEM_LIST);
}
@ -215,10 +238,13 @@ public final class LibraryResult<V> implements Bundleable {
*
* <p>{@code errorCode} must not be {@link #RESULT_SUCCESS}.
*
* <p>Note: This method will be deprecated when {@link #ofError(SessionError)} is promoted to
* stable API status.
*
* @param errorCode The error code.
*/
public static <V> LibraryResult<V> ofError(@Code int errorCode) {
return ofError(errorCode, /* params= */ null);
return ofError(new SessionError(errorCode, SessionError.DEFAULT_ERROR_MESSAGE, Bundle.EMPTY));
}
/**
@ -227,15 +253,54 @@ public final class LibraryResult<V> implements Bundleable {
*
* <p>{@code errorCode} must not be {@link #RESULT_SUCCESS}.
*
* <p>Note: This method will be deprecated when {@link #ofError(SessionError, LibraryParams)} is
* promoted to stable API status.
*
* @param errorCode The error code.
* @param params The optional parameters to describe the error.
*/
public static <V> LibraryResult<V> ofError(@Code int errorCode, @Nullable LibraryParams params) {
checkArgument(errorCode != RESULT_SUCCESS);
return new LibraryResult<>(
/* resultCode= */ errorCode,
SystemClock.elapsedRealtime(),
/* params= */ params,
new SessionError(errorCode, SessionError.DEFAULT_ERROR_MESSAGE, Bundle.EMPTY),
/* value= */ null,
VALUE_TYPE_ERROR);
}
/**
* Creates an instance with a {@link SessionError} to describe the error. The {@link #resultCode}
* is taken from {@link SessionError#code}.
*
* @param sessionError The {@link SessionError}.
*/
@UnstableApi
public static <V> LibraryResult<V> ofError(SessionError sessionError) {
return new LibraryResult<>(
/* resultCode= */ sessionError.code,
SystemClock.elapsedRealtime(),
/* params= */ null,
sessionError,
/* value= */ null,
VALUE_TYPE_ERROR);
}
/**
* Creates an instance with a {@link SessionError} to describe the error, and the {@linkplain
* LibraryParams parameters sent by the browser}. The {@link #resultCode} is taken from {@link
* SessionError#code}.
*
* @param sessionError The {@link SessionError}.
* @param params The {@link LibraryParams} sent by the browser.
*/
@UnstableApi
public static <V> LibraryResult<V> ofError(SessionError sessionError, LibraryParams params) {
return new LibraryResult<>(
/* resultCode= */ sessionError.code,
SystemClock.elapsedRealtime(),
/* params= */ params,
sessionError,
/* value= */ null,
VALUE_TYPE_ERROR);
}
@ -244,11 +309,13 @@ public final class LibraryResult<V> implements Bundleable {
@Code int resultCode,
long completionTimeMs,
@Nullable LibraryParams params,
@Nullable SessionError sessionError,
@Nullable V value,
@ValueType int valueType) {
this.resultCode = resultCode;
this.completionTimeMs = completionTimeMs;
this.params = params;
this.sessionError = sessionError;
this.value = value;
this.valueType = valueType;
}
@ -266,6 +333,7 @@ public final class LibraryResult<V> implements Bundleable {
private static final String FIELD_PARAMS = Util.intToStringMaxRadix(2);
private static final String FIELD_VALUE = Util.intToStringMaxRadix(3);
private static final String FIELD_VALUE_TYPE = Util.intToStringMaxRadix(4);
private static final String FIELD_SESSION_ERROR = Util.intToStringMaxRadix(5);
// Casting V to ImmutableList<MediaItem> is safe if valueType == VALUE_TYPE_ITEM_LIST.
@SuppressWarnings("unchecked")
@ -278,6 +346,9 @@ public final class LibraryResult<V> implements Bundleable {
if (params != null) {
bundle.putBundle(FIELD_PARAMS, params.toBundle());
}
if (sessionError != null) {
bundle.putBundle(FIELD_SESSION_ERROR, sessionError.toBundle());
}
bundle.putInt(FIELD_VALUE_TYPE, valueType);
if (value == null) {
@ -391,6 +462,14 @@ public final class LibraryResult<V> implements Bundleable {
@Nullable
MediaLibraryService.LibraryParams params =
paramsBundle == null ? null : LibraryParams.fromBundle(paramsBundle);
@Nullable SessionError sessionError = null;
@Nullable Bundle sessionErrorBundle = bundle.getBundle(FIELD_SESSION_ERROR);
if (sessionErrorBundle != null) {
sessionError = SessionError.fromBundle(sessionErrorBundle);
} else if (resultCode != RESULT_SUCCESS) {
// Result from a session with a library version that doesn't have the SessionError.
sessionError = new SessionError(resultCode, SessionError.DEFAULT_ERROR_MESSAGE);
}
@ValueType int valueType = bundle.getInt(FIELD_VALUE_TYPE);
@Nullable Object value;
switch (valueType) {
@ -416,7 +495,8 @@ public final class LibraryResult<V> implements Bundleable {
throw new IllegalStateException();
}
return new LibraryResult<>(resultCode, completionTimeMs, params, value, valueType);
return new LibraryResult<>(
resultCode, completionTimeMs, params, sessionError, value, valueType);
}
@Documented

View File

@ -20,7 +20,7 @@ import static androidx.media3.common.util.Assertions.checkNotEmpty;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.common.util.Util.postOrRun;
import static androidx.media3.session.LibraryResult.RESULT_ERROR_SESSION_DISCONNECTED;
import static androidx.media3.session.SessionError.ERROR_SESSION_DISCONNECTED;
import android.content.Context;
import android.os.Bundle;
@ -422,7 +422,7 @@ public final class MediaBrowser extends MediaController {
}
private static <V> ListenableFuture<LibraryResult<V>> createDisconnectedFuture() {
return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_SESSION_DISCONNECTED));
return Futures.immediateFuture(LibraryResult.ofError(ERROR_SESSION_DISCONNECTED));
}
private void verifyApplicationThread() {

View File

@ -15,9 +15,6 @@
*/
package androidx.media3.session;
import static androidx.media3.session.LibraryResult.RESULT_ERROR_PERMISSION_DENIED;
import static androidx.media3.session.LibraryResult.RESULT_ERROR_SESSION_DISCONNECTED;
import static androidx.media3.session.LibraryResult.RESULT_INFO_SKIPPED;
import static androidx.media3.session.SessionCommand.COMMAND_CODE_LIBRARY_GET_CHILDREN;
import static androidx.media3.session.SessionCommand.COMMAND_CODE_LIBRARY_GET_ITEM;
import static androidx.media3.session.SessionCommand.COMMAND_CODE_LIBRARY_GET_LIBRARY_ROOT;
@ -25,6 +22,9 @@ import static androidx.media3.session.SessionCommand.COMMAND_CODE_LIBRARY_GET_SE
import static androidx.media3.session.SessionCommand.COMMAND_CODE_LIBRARY_SEARCH;
import static androidx.media3.session.SessionCommand.COMMAND_CODE_LIBRARY_SUBSCRIBE;
import static androidx.media3.session.SessionCommand.COMMAND_CODE_LIBRARY_UNSUBSCRIBE;
import static androidx.media3.session.SessionError.ERROR_PERMISSION_DENIED;
import static androidx.media3.session.SessionError.ERROR_SESSION_DISCONNECTED;
import static androidx.media3.session.SessionError.INFO_SKIPPED;
import android.content.Context;
import android.os.Bundle;
@ -189,20 +189,20 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
IMediaSession iSession = getSessionInterfaceWithSessionCommandIfAble(commandCode);
if (iSession != null) {
SequencedFuture<LibraryResult<V>> result =
sequencedFutureManager.createSequencedFuture(LibraryResult.ofError(RESULT_INFO_SKIPPED));
sequencedFutureManager.createSequencedFuture(LibraryResult.ofError(INFO_SKIPPED));
try {
task.run(iSession, result.getSequenceNumber());
} catch (RemoteException e) {
Log.w(TAG, "Cannot connect to the service or the session is gone", e);
sequencedFutureManager.setFutureResult(
result.getSequenceNumber(), LibraryResult.ofError(RESULT_ERROR_SESSION_DISCONNECTED));
result.getSequenceNumber(), LibraryResult.ofError(ERROR_SESSION_DISCONNECTED));
}
return result;
} else {
// Don't create Future with SequencedFutureManager.
// Otherwise session would receive discontinued sequence number, and it would make
// future work item 'keeping call sequence when session execute commands' impossible.
return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_PERMISSION_DENIED));
return Futures.immediateFuture(LibraryResult.ofError(ERROR_PERMISSION_DENIED));
}
}

View File

@ -15,10 +15,10 @@
*/
package androidx.media3.session;
import static androidx.media3.session.LibraryResult.RESULT_ERROR_BAD_VALUE;
import static androidx.media3.session.LibraryResult.RESULT_ERROR_PERMISSION_DENIED;
import static androidx.media3.session.LibraryResult.RESULT_ERROR_SESSION_DISCONNECTED;
import static androidx.media3.session.LibraryResult.RESULT_ERROR_UNKNOWN;
import static androidx.media3.session.SessionError.ERROR_BAD_VALUE;
import static androidx.media3.session.SessionError.ERROR_PERMISSION_DENIED;
import static androidx.media3.session.SessionError.ERROR_SESSION_DISCONNECTED;
import static androidx.media3.session.SessionError.ERROR_UNKNOWN;
import android.content.Context;
import android.os.Bundle;
@ -92,7 +92,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
public ListenableFuture<LibraryResult<MediaItem>> getLibraryRoot(@Nullable LibraryParams params) {
if (!getInstance()
.isSessionCommandAvailable(SessionCommand.COMMAND_CODE_LIBRARY_GET_LIBRARY_ROOT)) {
return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_PERMISSION_DENIED));
return Futures.immediateFuture(LibraryResult.ofError(ERROR_PERMISSION_DENIED));
}
SettableFuture<LibraryResult<MediaItem>> result = SettableFuture.create();
MediaBrowserCompat browserCompat = getBrowserCompat(params);
@ -117,11 +117,11 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
public ListenableFuture<LibraryResult<Void>> subscribe(
String parentId, @Nullable LibraryParams params) {
if (!getInstance().isSessionCommandAvailable(SessionCommand.COMMAND_CODE_LIBRARY_SUBSCRIBE)) {
return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_PERMISSION_DENIED));
return Futures.immediateFuture(LibraryResult.ofError(ERROR_PERMISSION_DENIED));
}
MediaBrowserCompat browserCompat = getBrowserCompat();
if (browserCompat == null) {
return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_SESSION_DISCONNECTED));
return Futures.immediateFuture(LibraryResult.ofError(ERROR_SESSION_DISCONNECTED));
}
SettableFuture<LibraryResult<Void>> future = SettableFuture.create();
SubscribeCallback callback = new SubscribeCallback(future);
@ -138,17 +138,17 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
@Override
public ListenableFuture<LibraryResult<Void>> unsubscribe(String parentId) {
if (!getInstance().isSessionCommandAvailable(SessionCommand.COMMAND_CODE_LIBRARY_UNSUBSCRIBE)) {
return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_PERMISSION_DENIED));
return Futures.immediateFuture(LibraryResult.ofError(ERROR_PERMISSION_DENIED));
}
MediaBrowserCompat browserCompat = getBrowserCompat();
if (browserCompat == null) {
return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_SESSION_DISCONNECTED));
return Futures.immediateFuture(LibraryResult.ofError(ERROR_SESSION_DISCONNECTED));
}
// Note: don't use MediaBrowserCompat#unsubscribe(String) here, to keep the subscription
// callback for getChildren.
List<SubscribeCallback> list = subscribeCallbacks.get(parentId);
if (list == null) {
return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_BAD_VALUE));
return Futures.immediateFuture(LibraryResult.ofError(ERROR_BAD_VALUE));
}
for (int i = 0; i < list.size(); i++) {
browserCompat.unsubscribe(parentId, list.get(i));
@ -163,11 +163,11 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
String parentId, int page, int pageSize, @Nullable LibraryParams params) {
if (!getInstance()
.isSessionCommandAvailable(SessionCommand.COMMAND_CODE_LIBRARY_GET_CHILDREN)) {
return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_PERMISSION_DENIED));
return Futures.immediateFuture(LibraryResult.ofError(ERROR_PERMISSION_DENIED));
}
MediaBrowserCompat browserCompat = getBrowserCompat();
if (browserCompat == null) {
return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_SESSION_DISCONNECTED));
return Futures.immediateFuture(LibraryResult.ofError(ERROR_SESSION_DISCONNECTED));
}
SettableFuture<LibraryResult<ImmutableList<MediaItem>>> future = SettableFuture.create();
@ -179,11 +179,11 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
@Override
public ListenableFuture<LibraryResult<MediaItem>> getItem(String mediaId) {
if (!getInstance().isSessionCommandAvailable(SessionCommand.COMMAND_CODE_LIBRARY_GET_ITEM)) {
return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_PERMISSION_DENIED));
return Futures.immediateFuture(LibraryResult.ofError(ERROR_PERMISSION_DENIED));
}
MediaBrowserCompat browserCompat = getBrowserCompat();
if (browserCompat == null) {
return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_SESSION_DISCONNECTED));
return Futures.immediateFuture(LibraryResult.ofError(ERROR_SESSION_DISCONNECTED));
}
SettableFuture<LibraryResult<MediaItem>> result = SettableFuture.create();
browserCompat.getItem(
@ -196,13 +196,13 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
LibraryResult.ofItem(
LegacyConversions.convertToMediaItem(item), /* params= */ null));
} else {
result.set(LibraryResult.ofError(RESULT_ERROR_BAD_VALUE));
result.set(LibraryResult.ofError(ERROR_BAD_VALUE));
}
}
@Override
public void onError(String itemId) {
result.set(LibraryResult.ofError(RESULT_ERROR_UNKNOWN));
result.set(LibraryResult.ofError(ERROR_UNKNOWN));
}
});
return result;
@ -212,11 +212,11 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
public ListenableFuture<LibraryResult<Void>> search(
String query, @Nullable LibraryParams params) {
if (!getInstance().isSessionCommandAvailable(SessionCommand.COMMAND_CODE_LIBRARY_SEARCH)) {
return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_PERMISSION_DENIED));
return Futures.immediateFuture(LibraryResult.ofError(ERROR_PERMISSION_DENIED));
}
MediaBrowserCompat browserCompat = getBrowserCompat();
if (browserCompat == null) {
return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_SESSION_DISCONNECTED));
return Futures.immediateFuture(LibraryResult.ofError(ERROR_SESSION_DISCONNECTED));
}
browserCompat.search(
query,
@ -259,11 +259,11 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
String query, int page, int pageSize, @Nullable LibraryParams params) {
if (!getInstance()
.isSessionCommandAvailable(SessionCommand.COMMAND_CODE_LIBRARY_GET_SEARCH_RESULT)) {
return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_PERMISSION_DENIED));
return Futures.immediateFuture(LibraryResult.ofError(ERROR_PERMISSION_DENIED));
}
MediaBrowserCompat browserCompat = getBrowserCompat();
if (browserCompat == null) {
return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_SESSION_DISCONNECTED));
return Futures.immediateFuture(LibraryResult.ofError(ERROR_SESSION_DISCONNECTED));
}
SettableFuture<LibraryResult<ImmutableList<MediaItem>>> future = SettableFuture.create();
@ -285,7 +285,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
@Override
public void onError(String query, @Nullable Bundle extrasSent) {
future.set(LibraryResult.ofError(RESULT_ERROR_UNKNOWN));
future.set(LibraryResult.ofError(ERROR_UNKNOWN));
}
});
return future;
@ -339,7 +339,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
MediaBrowserCompat browserCompat = browserCompats.get(params);
if (browserCompat == null) {
// Shouldn't be happen. Internal error?
result.set(LibraryResult.ofError(RESULT_ERROR_UNKNOWN));
result.set(LibraryResult.ofError(ERROR_UNKNOWN));
} else {
result.set(
LibraryResult.ofItem(
@ -356,7 +356,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
@Override
public void onConnectionFailed() {
// Unknown extra field.
result.set(LibraryResult.ofError(RESULT_ERROR_BAD_VALUE));
result.set(LibraryResult.ofError(ERROR_BAD_VALUE));
release();
}
}
@ -396,7 +396,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
private void onErrorInternal() {
// Don't need to unsubscribe here, because MediaBrowserServiceCompat can notify children
// changed after the initial failure and MediaBrowserCompat could receive the changes.
future.set(LibraryResult.ofError(RESULT_ERROR_UNKNOWN));
future.set(LibraryResult.ofError(ERROR_UNKNOWN));
}
private void onChildrenLoadedInternal(
@ -468,7 +468,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
}
private void onErrorInternal() {
future.set(LibraryResult.ofError(RESULT_ERROR_UNKNOWN));
future.set(LibraryResult.ofError(ERROR_UNKNOWN));
}
private void onChildrenLoadedInternal(
@ -479,14 +479,14 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
}
MediaBrowserCompat browserCompat = getBrowserCompat();
if (browserCompat == null) {
future.set(LibraryResult.ofError(RESULT_ERROR_SESSION_DISCONNECTED));
future.set(LibraryResult.ofError(ERROR_SESSION_DISCONNECTED));
return;
}
browserCompat.unsubscribe(this.parentId, GetChildrenCallback.this);
if (children == null) {
// list are non-Null, so it must be internal error.
future.set(LibraryResult.ofError(RESULT_ERROR_UNKNOWN));
future.set(LibraryResult.ofError(ERROR_UNKNOWN));
} else {
// Don't set extra here, because 'extra' have different meanings between old
// API and new API as follows.

View File

@ -21,6 +21,8 @@ import static androidx.media3.common.util.Assertions.checkNotEmpty;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.common.util.Util.postOrRun;
import static androidx.media3.session.SessionError.ERROR_NOT_SUPPORTED;
import static androidx.media3.session.SessionError.ERROR_SESSION_DISCONNECTED;
import android.app.PendingIntent;
import android.content.Context;
@ -377,7 +379,7 @@ public class MediaController implements Player {
*/
default ListenableFuture<SessionResult> onSetCustomLayout(
MediaController controller, List<CommandButton> layout) {
return Futures.immediateFuture(new SessionResult(SessionResult.RESULT_ERROR_NOT_SUPPORTED));
return Futures.immediateFuture(new SessionResult(ERROR_NOT_SUPPORTED));
}
/**
@ -416,7 +418,7 @@ public class MediaController implements Player {
* Futures#immediateFuture(Object)}.
*
* <p>The default implementation returns {@link ListenableFuture} of {@link
* SessionResult#RESULT_ERROR_NOT_SUPPORTED}.
* SessionError#ERROR_NOT_SUPPORTED}.
*
* @param controller The controller.
* @param command The custom command.
@ -425,7 +427,7 @@ public class MediaController implements Player {
*/
default ListenableFuture<SessionResult> onCustomCommand(
MediaController controller, SessionCommand command, Bundle args) {
return Futures.immediateFuture(new SessionResult(SessionResult.RESULT_ERROR_NOT_SUPPORTED));
return Futures.immediateFuture(new SessionResult(SessionError.ERROR_NOT_SUPPORTED));
}
/**
@ -2029,8 +2031,7 @@ public class MediaController implements Player {
}
private static ListenableFuture<SessionResult> createDisconnectedFuture() {
return Futures.immediateFuture(
new SessionResult(SessionResult.RESULT_ERROR_SESSION_DISCONNECTED));
return Futures.immediateFuture(new SessionResult(ERROR_SESSION_DISCONNECTED));
}
/* package */ final void runOnApplicationLooper(Runnable runnable) {

View File

@ -23,6 +23,9 @@ import static androidx.media3.common.util.Assertions.checkStateNotNull;
import static androidx.media3.common.util.Util.usToMs;
import static androidx.media3.session.MediaUtils.calculateBufferedPercentage;
import static androidx.media3.session.MediaUtils.mergePlayerInfo;
import static androidx.media3.session.SessionError.ERROR_PERMISSION_DENIED;
import static androidx.media3.session.SessionError.ERROR_SESSION_DISCONNECTED;
import static androidx.media3.session.SessionError.ERROR_UNKNOWN;
import static java.lang.Math.max;
import static java.lang.Math.min;
@ -327,8 +330,7 @@ import org.checkerframework.checker.nullness.qual.NonNull;
int sequenceNumber =
((SequencedFutureManager.SequencedFuture<SessionResult>) future).getSequenceNumber();
pendingMaskingSequencedFutureNumbers.remove(sequenceNumber);
sequencedFutureManager.setFutureResult(
sequenceNumber, new SessionResult(SessionResult.RESULT_ERROR_UNKNOWN));
sequencedFutureManager.setFutureResult(sequenceNumber, new SessionResult(ERROR_UNKNOWN));
}
Log.w(TAG, "Synchronous command takes too long on the session side.", e);
// TODO(b/188888693): Let developers know the failure in their code.
@ -377,15 +379,14 @@ import org.checkerframework.checker.nullness.qual.NonNull;
Log.w(TAG, "Cannot connect to the service or the session is gone", e);
pendingMaskingSequencedFutureNumbers.remove(sequenceNumber);
sequencedFutureManager.setFutureResult(
sequenceNumber, new SessionResult(SessionResult.RESULT_ERROR_SESSION_DISCONNECTED));
sequenceNumber, new SessionResult(ERROR_SESSION_DISCONNECTED));
}
return result;
} else {
// Don't create Future with SequencedFutureManager.
// Otherwise session would receive discontinued sequence number, and it would make
// future work item 'keeping call sequence when session execute commands' impossible.
return Futures.immediateFuture(
new SessionResult(SessionResult.RESULT_ERROR_PERMISSION_DENIED));
return Futures.immediateFuture(new SessionResult(ERROR_PERMISSION_DENIED));
}
}
@ -2662,7 +2663,7 @@ import org.checkerframework.checker.nullness.qual.NonNull;
result = new SessionResult(SessionResult.RESULT_INFO_SKIPPED);
} catch (ExecutionException | InterruptedException e) {
Log.w(TAG, "Session operation failed", e);
result = new SessionResult(SessionResult.RESULT_ERROR_UNKNOWN);
result = new SessionResult(ERROR_UNKNOWN);
}
sendControllerResult(seq, result);
},

View File

@ -18,9 +18,10 @@ package androidx.media3.session;
import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Assertions.checkNotEmpty;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.session.LibraryResult.RESULT_ERROR_NOT_SUPPORTED;
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 android.app.PendingIntent;
import android.content.Context;
@ -158,7 +159,7 @@ public abstract class MediaLibraryService extends MediaSessionService {
*/
default ListenableFuture<LibraryResult<MediaItem>> onGetLibraryRoot(
MediaLibrarySession session, ControllerInfo browser, @Nullable LibraryParams params) {
return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_NOT_SUPPORTED));
return Futures.immediateFuture(LibraryResult.ofError(ERROR_NOT_SUPPORTED));
}
/**
@ -181,7 +182,7 @@ public abstract class MediaLibraryService extends MediaSessionService {
*/
default ListenableFuture<LibraryResult<MediaItem>> onGetItem(
MediaLibrarySession session, ControllerInfo browser, String mediaId) {
return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_NOT_SUPPORTED));
return Futures.immediateFuture(LibraryResult.ofError(ERROR_NOT_SUPPORTED));
}
/**
@ -215,7 +216,7 @@ public abstract class MediaLibraryService extends MediaSessionService {
@IntRange(from = 0) int page,
@IntRange(from = 1) int pageSize,
@Nullable LibraryParams params) {
return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_NOT_SUPPORTED));
return Futures.immediateFuture(LibraryResult.ofError(ERROR_NOT_SUPPORTED));
}
/**
@ -271,9 +272,7 @@ public abstract class MediaLibraryService extends MediaSessionService {
// Reject subscription if no browsable item for the parent media ID is returned.
return Futures.immediateFuture(
LibraryResult.ofError(
result.resultCode != RESULT_SUCCESS
? result.resultCode
: LibraryResult.RESULT_ERROR_BAD_VALUE));
result.resultCode != RESULT_SUCCESS ? result.resultCode : ERROR_BAD_VALUE));
}
if (browser.getControllerVersion() != ControllerInfo.LEGACY_CONTROLLER_VERSION) {
// For legacy browsers, android.service.media.MediaBrowserService already calls
@ -343,7 +342,7 @@ public abstract class MediaLibraryService extends MediaSessionService {
ControllerInfo browser,
String query,
@Nullable LibraryParams params) {
return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_NOT_SUPPORTED));
return Futures.immediateFuture(LibraryResult.ofError(ERROR_NOT_SUPPORTED));
}
/**
@ -381,7 +380,7 @@ public abstract class MediaLibraryService extends MediaSessionService {
@IntRange(from = 0) int page,
@IntRange(from = 1) int pageSize,
@Nullable LibraryParams params) {
return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_NOT_SUPPORTED));
return Futures.immediateFuture(LibraryResult.ofError(ERROR_NOT_SUPPORTED));
}
}

View File

@ -17,11 +17,14 @@ 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_ERROR_NOT_SUPPORTED;
import static androidx.media3.session.LibraryResult.RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED;
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.PlayerWrapper.STATUS_CODE_SUCCESS_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;
@ -122,7 +125,7 @@ import java.util.concurrent.Future;
if (params != null && params.isRecent && isSystemUiController(browser)) {
// Advertise support for playback resumption, if enabled.
return !canResumePlaybackOnStart()
? Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_NOT_SUPPORTED))
? Futures.immediateFuture(LibraryResult.ofError(ERROR_NOT_SUPPORTED))
: Futures.immediateFuture(
LibraryResult.ofItem(
new MediaItem.Builder()
@ -156,7 +159,7 @@ import java.util.concurrent.Future;
@Nullable LibraryParams params) {
if (Objects.equals(parentId, RECENT_LIBRARY_ROOT_MEDIA_ID)) {
if (!canResumePlaybackOnStart()) {
return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_NOT_SUPPORTED));
return Futures.immediateFuture(LibraryResult.ofError(ERROR_NOT_SUPPORTED));
}
// Advertise support for playback resumption. If STATE_IDLE, the request arrives at boot time
// to get the full item data to build a notification. If not STATE_IDLE we don't need to
@ -369,25 +372,40 @@ import java.util.concurrent.Future;
private void maybeUpdateLegacyErrorState(LibraryResult<?> result) {
PlayerWrapper playerWrapper = getPlayerWrapper();
if (result.resultCode == RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED
&& result.params != null
&& result.params.extras.containsKey(EXTRAS_KEY_ERROR_RESOLUTION_ACTION_INTENT_COMPAT)) {
// Mapping this error to the legacy error state provides backwards compatibility for the
// Automotive OS sign-in.
MediaSessionCompat mediaSessionCompat = getSessionCompat();
if (playerWrapper.getLegacyStatusCode() != RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED) {
playerWrapper.setLegacyErrorStatus(
ERROR_CODE_AUTHENTICATION_EXPIRED_COMPAT,
getContext().getString(R.string.authentication_required),
result.params.extras);
mediaSessionCompat.setPlaybackState(playerWrapper.createPlaybackStateCompat());
}
} else if (playerWrapper.getLegacyStatusCode() != RESULT_SUCCESS) {
if (setLegacyErrorState(result)) {
// Sync playback state if legacy error state changed.
getSessionCompat().setPlaybackState(playerWrapper.createPlaybackStateCompat());
} else if (playerWrapper.getLegacyStatusCode() != STATUS_CODE_SUCCESS_COMPAT) {
playerWrapper.clearLegacyErrorStatus();
getSessionCompat().setPlaybackState(playerWrapper.createPlaybackStateCompat());
}
}
private boolean setLegacyErrorState(LibraryResult<?> result) {
if (result.resultCode == ERROR_SESSION_AUTHENTICATION_EXPIRED
&& getPlayerWrapper().getLegacyStatusCode() != 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;
}
getPlayerWrapper()
.setLegacyErrorStatus(
ERROR_CODE_AUTHENTICATION_EXPIRED_COMPAT,
getContext().getString(R.string.authentication_required),
bundle);
return true;
}
return false;
}
@Nullable
private static <T> T tryGetFutureResult(Future<T> future) {
checkState(future.isDone());
@ -436,8 +454,7 @@ import java.util.concurrent.Future;
@Override
public void onSuccess(MediaSession.MediaItemsWithStartPosition playlist) {
if (playlist.mediaItems.isEmpty()) {
settableFuture.set(
LibraryResult.ofError(LibraryResult.RESULT_ERROR_INVALID_STATE, params));
settableFuture.set(LibraryResult.ofError(ERROR_INVALID_STATE, params));
return;
}
int sanitizedStartIndex =
@ -449,7 +466,7 @@ import java.util.concurrent.Future;
@Override
public void onFailure(Throwable t) {
settableFuture.set(LibraryResult.ofError(LibraryResult.RESULT_ERROR_UNKNOWN, params));
settableFuture.set(LibraryResult.ofError(ERROR_UNKNOWN, params));
Log.e(TAG, "Failed fetching recent media item at boot time: " + t.getMessage(), t);
}
},

View File

@ -19,7 +19,7 @@ import static androidx.annotation.VisibleForTesting.PRIVATE;
import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.session.SessionResult.RESULT_ERROR_NOT_SUPPORTED;
import static androidx.media3.session.SessionError.ERROR_NOT_SUPPORTED;
import static androidx.media3.session.SessionResult.RESULT_SUCCESS;
import android.app.PendingIntent;
@ -1363,7 +1363,7 @@ public class MediaSession {
*/
default ListenableFuture<SessionResult> onSetRating(
MediaSession session, ControllerInfo controller, String mediaId, Rating rating) {
return Futures.immediateFuture(new SessionResult(RESULT_ERROR_NOT_SUPPORTED));
return Futures.immediateFuture(new SessionResult(ERROR_NOT_SUPPORTED));
}
/**
@ -1385,7 +1385,7 @@ public class MediaSession {
*/
default ListenableFuture<SessionResult> onSetRating(
MediaSession session, ControllerInfo controller, Rating rating) {
return Futures.immediateFuture(new SessionResult(RESULT_ERROR_NOT_SUPPORTED));
return Futures.immediateFuture(new SessionResult(ERROR_NOT_SUPPORTED));
}
/**
@ -1418,7 +1418,7 @@ public class MediaSession {
ControllerInfo controller,
SessionCommand customCommand,
Bundle args) {
return Futures.immediateFuture(new SessionResult(RESULT_ERROR_NOT_SUPPORTED));
return Futures.immediateFuture(new SessionResult(ERROR_NOT_SUPPORTED));
}
/**

View File

@ -31,9 +31,9 @@ import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkStateNotNull;
import static androidx.media3.common.util.Util.postOrRun;
import static androidx.media3.session.MediaSessionStub.UNKNOWN_SEQUENCE_NUMBER;
import static androidx.media3.session.SessionResult.RESULT_ERROR_SESSION_DISCONNECTED;
import static androidx.media3.session.SessionResult.RESULT_ERROR_UNKNOWN;
import static androidx.media3.session.SessionResult.RESULT_INFO_SKIPPED;
import static androidx.media3.session.SessionError.ERROR_SESSION_DISCONNECTED;
import static androidx.media3.session.SessionError.ERROR_UNKNOWN;
import static androidx.media3.session.SessionError.INFO_SKIPPED;
import android.app.PendingIntent;
import android.content.ComponentName;
@ -113,7 +113,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
public static final String TAG = "MediaSessionImpl";
private static final SessionResult RESULT_WHEN_CLOSED = new SessionResult(RESULT_INFO_SKIPPED);
private static final SessionResult RESULT_WHEN_CLOSED = new SessionResult(INFO_SKIPPED);
private final Object lock = new Object();
@ -1090,7 +1090,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
seq = ((SequencedFuture<SessionResult>) future).getSequenceNumber();
} else {
if (!isConnected(controller)) {
return Futures.immediateFuture(new SessionResult(RESULT_ERROR_SESSION_DISCONNECTED));
return Futures.immediateFuture(new SessionResult(ERROR_SESSION_DISCONNECTED));
}
// 0 is OK for legacy controllers, because they didn't have sequence numbers.
seq = 0;
@ -1104,7 +1104,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
return future;
} catch (DeadObjectException e) {
onDeadObjectException(controller);
return Futures.immediateFuture(new SessionResult(RESULT_ERROR_SESSION_DISCONNECTED));
return Futures.immediateFuture(new SessionResult(ERROR_SESSION_DISCONNECTED));
} catch (RemoteException e) {
// Currently it's TransactionTooLargeException or DeadSystemException.
// We'd better to leave log for those cases because
@ -1113,7 +1113,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
// - DeadSystemException means that errors around it can be ignored.
Log.w(TAG, "Exception in " + controller.toString(), e);
}
return Futures.immediateFuture(new SessionResult(RESULT_ERROR_UNKNOWN));
return Futures.immediateFuture(new SessionResult(ERROR_UNKNOWN));
}
/** Removes controller. Call this when DeadObjectException is happened with binder call. */

View File

@ -37,7 +37,7 @@ import static androidx.media3.common.util.Util.castNonNull;
import static androidx.media3.common.util.Util.postOrRun;
import static androidx.media3.session.MediaUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES;
import static androidx.media3.session.SessionCommand.COMMAND_CODE_CUSTOM;
import static androidx.media3.session.SessionResult.RESULT_ERROR_UNKNOWN;
import static androidx.media3.session.SessionError.ERROR_UNKNOWN;
import static androidx.media3.session.SessionResult.RESULT_INFO_SKIPPED;
import static androidx.media3.session.SessionResult.RESULT_SUCCESS;
@ -936,7 +936,7 @@ import org.checkerframework.checker.initialization.qual.Initialized;
result = new SessionResult(RESULT_INFO_SKIPPED);
} catch (ExecutionException | InterruptedException e) {
Log.w(TAG, "Custom command failed", e);
result = new SessionResult(RESULT_ERROR_UNKNOWN);
result = new SessionResult(ERROR_UNKNOWN);
}
receiver.send(result.resultCode, result.extras);
},

View File

@ -53,6 +53,11 @@ import static androidx.media3.session.SessionCommand.COMMAND_CODE_LIBRARY_SEARCH
import static androidx.media3.session.SessionCommand.COMMAND_CODE_LIBRARY_SUBSCRIBE;
import static androidx.media3.session.SessionCommand.COMMAND_CODE_LIBRARY_UNSUBSCRIBE;
import static androidx.media3.session.SessionCommand.COMMAND_CODE_SESSION_SET_RATING;
import static androidx.media3.session.SessionError.ERROR_NOT_SUPPORTED;
import static androidx.media3.session.SessionError.ERROR_PERMISSION_DENIED;
import static androidx.media3.session.SessionError.ERROR_SESSION_DISCONNECTED;
import static androidx.media3.session.SessionError.ERROR_UNKNOWN;
import static androidx.media3.session.SessionError.INFO_SKIPPED;
import android.app.PendingIntent;
import android.os.Binder;
@ -192,8 +197,8 @@ import java.util.concurrent.ExecutionException;
result =
new SessionResult(
exception.getCause() instanceof UnsupportedOperationException
? SessionResult.RESULT_ERROR_NOT_SUPPORTED
: SessionResult.RESULT_ERROR_UNKNOWN);
? ERROR_NOT_SUPPORTED
: ERROR_UNKNOWN);
}
sendSessionResult(controller, sequenceNumber, result);
});
@ -205,8 +210,7 @@ import java.util.concurrent.ExecutionException;
MediaItemPlayerTask mediaItemPlayerTask) {
return (sessionImpl, controller, sequenceNumber) -> {
if (sessionImpl.isReleased()) {
return Futures.immediateFuture(
new SessionResult(SessionResult.RESULT_ERROR_SESSION_DISCONNECTED));
return Futures.immediateFuture(new SessionResult(ERROR_SESSION_DISCONNECTED));
}
return transformFutureAsync(
mediaItemsTask.run(sessionImpl, controller, sequenceNumber),
@ -231,8 +235,7 @@ import java.util.concurrent.ExecutionException;
MediaItemsWithStartPositionPlayerTask mediaItemPlayerTask) {
return (sessionImpl, controller, sequenceNumber) -> {
if (sessionImpl.isReleased()) {
return Futures.immediateFuture(
new SessionResult(SessionResult.RESULT_ERROR_SESSION_DISCONNECTED));
return Futures.immediateFuture(new SessionResult(ERROR_SESSION_DISCONNECTED));
}
return transformFutureAsync(
mediaItemsTask.run(sessionImpl, controller, sequenceNumber),
@ -275,10 +278,10 @@ import java.util.concurrent.ExecutionException;
result = checkNotNull(future.get(), "LibraryResult must not be null");
} catch (CancellationException e) {
Log.w(TAG, "Library operation cancelled", e);
result = LibraryResult.ofError(LibraryResult.RESULT_INFO_SKIPPED);
result = LibraryResult.ofError(INFO_SKIPPED);
} catch (ExecutionException | InterruptedException e) {
Log.w(TAG, "Library operation failed", e);
result = LibraryResult.ofError(LibraryResult.RESULT_ERROR_UNKNOWN);
result = LibraryResult.ofError(ERROR_UNKNOWN);
}
sendLibraryResult(controller, sequenceNumber, result);
});
@ -314,9 +317,7 @@ import java.util.concurrent.ExecutionException;
() -> {
if (!connectedControllersManager.isPlayerCommandAvailable(controller, command)) {
sendSessionResult(
controller,
sequenceNumber,
new SessionResult(SessionResult.RESULT_ERROR_PERMISSION_DENIED));
controller, sequenceNumber, new SessionResult(ERROR_PERMISSION_DENIED));
return;
}
@SessionResult.Code
@ -393,17 +394,13 @@ import java.util.concurrent.ExecutionException;
if (!connectedControllersManager.isSessionCommandAvailable(
controller, sessionCommand)) {
sendSessionResult(
controller,
sequenceNumber,
new SessionResult(SessionResult.RESULT_ERROR_PERMISSION_DENIED));
controller, sequenceNumber, new SessionResult(ERROR_PERMISSION_DENIED));
return;
}
} else {
if (!connectedControllersManager.isSessionCommandAvailable(controller, commandCode)) {
sendSessionResult(
controller,
sequenceNumber,
new SessionResult(SessionResult.RESULT_ERROR_PERMISSION_DENIED));
controller, sequenceNumber, new SessionResult(ERROR_PERMISSION_DENIED));
return;
}
}

View File

@ -63,7 +63,7 @@ import java.util.List;
*/
/* package */ final class PlayerWrapper extends ForwardingPlayer {
private static final int STATUS_CODE_SUCCESS_COMPAT = -1;
/* package */ static final int STATUS_CODE_SUCCESS_COMPAT = -1;
private final boolean playIfSuppressed;

View File

@ -0,0 +1,194 @@
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.session;
import static java.lang.annotation.ElementType.TYPE_USE;
import android.os.Bundle;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.media3.common.PlaybackException;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Objects;
/** Provides information about a session error. */
@UnstableApi
public final class SessionError {
/**
* Info and error result codes.
*
* <ul>
* <li>Info code: Positive integer
* <li>Error code: Negative integer
* </ul>
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef({
INFO_SKIPPED,
ERROR_UNKNOWN,
ERROR_INVALID_STATE,
ERROR_BAD_VALUE,
ERROR_PERMISSION_DENIED,
ERROR_IO,
ERROR_SESSION_DISCONNECTED,
ERROR_NOT_SUPPORTED,
ERROR_SESSION_AUTHENTICATION_EXPIRED,
ERROR_SESSION_PREMIUM_ACCOUNT_REQUIRED,
ERROR_SESSION_CONCURRENT_STREAM_LIMIT,
ERROR_SESSION_PARENTAL_CONTROL_RESTRICTED,
ERROR_SESSION_NOT_AVAILABLE_IN_REGION,
ERROR_SESSION_SKIP_LIMIT_REACHED,
ERROR_SESSION_SETUP_REQUIRED
})
public @interface Code {}
/** Info code representing that the command is skipped. */
public static final int INFO_SKIPPED = 1;
/** Error code representing that the command is ended with an unknown error. */
public static final int ERROR_UNKNOWN = -1;
/**
* Error code representing that the command cannot be completed because the current state is not
* valid for the command.
*/
public static final int ERROR_INVALID_STATE = -2;
/** Error code representing that an argument is illegal. */
public static final int ERROR_BAD_VALUE = -3;
/** Error code representing that the command is not allowed. */
public static final int ERROR_PERMISSION_DENIED = -4;
/** Error code representing that a file or network related error happened. */
public static final int ERROR_IO = -5;
/** Error code representing that the command is not supported. */
public static final int ERROR_NOT_SUPPORTED = -6;
/** Error code representing that the session and controller were disconnected. */
public static final int ERROR_SESSION_DISCONNECTED = -100;
/** Error code representing that the authentication has expired. */
public static final int ERROR_SESSION_AUTHENTICATION_EXPIRED = -102;
/** Error code representing that a premium account is required. */
public static final int ERROR_SESSION_PREMIUM_ACCOUNT_REQUIRED = -103;
/** Error code representing that too many concurrent streams are detected. */
public static final int ERROR_SESSION_CONCURRENT_STREAM_LIMIT = -104;
/** Error code representing that the content is blocked due to parental controls. */
public static final int ERROR_SESSION_PARENTAL_CONTROL_RESTRICTED = -105;
/** Error code representing that the content is blocked due to being regionally unavailable. */
public static final int ERROR_SESSION_NOT_AVAILABLE_IN_REGION = -106;
/**
* Error code representing that the application cannot skip any more because the skip limit is
* reached.
*/
public static final int ERROR_SESSION_SKIP_LIMIT_REACHED = -107;
/** Error code representing that the session needs user's manual intervention. */
public static final int ERROR_SESSION_SETUP_REQUIRED = -108;
/** Default error message. Only used by deprecated methods and for backwards compatibility. */
public static final String DEFAULT_ERROR_MESSAGE = "no error message provided";
public @SessionError.Code int code;
public String message;
public Bundle extras;
/**
* Creates an instance with {@linkplain Bundle#EMPTY an empty extras bundle}.
*
* @param code The error result code.
* @param message The error message.
* @throws IllegalArgumentException if the result code is not an error result code.
*/
public SessionError(@SessionError.Code int code, String message) {
this(code, message, Bundle.EMPTY);
}
/**
* Creates an instance.
*
* @param code The error result code.
* @param message The error message.
* @param extras The error extras.
* @throws IllegalArgumentException if the result code is not an error result code.
*/
public SessionError(@SessionError.Code int code, String message, Bundle extras) {
Assertions.checkArgument(code < 0 || code == INFO_SKIPPED);
this.code = code;
this.message = message;
this.extras = extras;
}
/** Checks the given error for equality while ignoring {@link #extras}. */
@Override
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
if (!(o instanceof SessionError)) {
return false;
}
SessionError that = (SessionError) o;
return code == that.code && Objects.equals(message, that.message);
}
@Override
public int hashCode() {
return Objects.hash(code, message);
}
// Bundleable implementation.
private static final String FIELD_CODE = Util.intToStringMaxRadix(0);
private static final String FIELD_MESSAGE = Util.intToStringMaxRadix(1);
private static final String FIELD_EXTRAS = Util.intToStringMaxRadix(2);
/** Returns a {@link Bundle} representing the information stored in this object. */
public Bundle toBundle() {
Bundle bundle = new Bundle();
bundle.putInt(FIELD_CODE, code);
bundle.putString(FIELD_MESSAGE, message);
if (!extras.isEmpty()) {
bundle.putBundle(FIELD_EXTRAS, extras);
}
return bundle;
}
/** Restores a {@code SessionError} from a {@link Bundle}. */
public static SessionError fromBundle(Bundle bundle) {
int code =
bundle.getInt(FIELD_CODE, /* defaultValue= */ PlaybackException.ERROR_CODE_UNSPECIFIED);
String message = bundle.getString(FIELD_MESSAGE, /* defaultValue= */ "");
@Nullable Bundle extras = bundle.getBundle(FIELD_EXTRAS);
return new SessionError(code, message, extras == null ? Bundle.EMPTY : extras);
}
}

View File

@ -15,6 +15,7 @@
*/
package androidx.media3.session;
import static androidx.media3.common.util.Assertions.checkArgument;
import static java.lang.annotation.ElementType.TYPE_USE;
import android.os.Bundle;
@ -57,21 +58,21 @@ public final class SessionResult implements Bundleable {
@Target(TYPE_USE)
@IntDef({
RESULT_SUCCESS,
RESULT_ERROR_UNKNOWN,
RESULT_ERROR_INVALID_STATE,
RESULT_ERROR_BAD_VALUE,
RESULT_ERROR_PERMISSION_DENIED,
RESULT_ERROR_IO,
RESULT_INFO_SKIPPED,
RESULT_ERROR_SESSION_DISCONNECTED,
RESULT_ERROR_NOT_SUPPORTED,
RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED,
RESULT_ERROR_SESSION_PREMIUM_ACCOUNT_REQUIRED,
RESULT_ERROR_SESSION_CONCURRENT_STREAM_LIMIT,
RESULT_ERROR_SESSION_PARENTAL_CONTROL_RESTRICTED,
RESULT_ERROR_SESSION_NOT_AVAILABLE_IN_REGION,
RESULT_ERROR_SESSION_SKIP_LIMIT_REACHED,
RESULT_ERROR_SESSION_SETUP_REQUIRED
SessionError.INFO_SKIPPED,
SessionError.ERROR_UNKNOWN,
SessionError.ERROR_INVALID_STATE,
SessionError.ERROR_BAD_VALUE,
SessionError.ERROR_PERMISSION_DENIED,
SessionError.ERROR_IO,
SessionError.ERROR_SESSION_DISCONNECTED,
SessionError.ERROR_NOT_SUPPORTED,
SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED,
SessionError.ERROR_SESSION_PREMIUM_ACCOUNT_REQUIRED,
SessionError.ERROR_SESSION_CONCURRENT_STREAM_LIMIT,
SessionError.ERROR_SESSION_PARENTAL_CONTROL_RESTRICTED,
SessionError.ERROR_SESSION_NOT_AVAILABLE_IN_REGION,
SessionError.ERROR_SESSION_SKIP_LIMIT_REACHED,
SessionError.ERROR_SESSION_SETUP_REQUIRED
})
public @interface Code {}
@ -85,56 +86,64 @@ public final class SessionResult implements Bundleable {
*/
public static final int RESULT_SUCCESS = 0;
/** Result code representing that the command is skipped. */
public static final int RESULT_INFO_SKIPPED = SessionError.INFO_SKIPPED;
/** Result code representing that the command is ended with an unknown error. */
public static final int RESULT_ERROR_UNKNOWN = -1;
public static final int RESULT_ERROR_UNKNOWN = SessionError.ERROR_UNKNOWN;
/**
* Result code representing that the command cannot be completed because the current state is not
* valid for the command.
*/
public static final int RESULT_ERROR_INVALID_STATE = -2;
public static final int RESULT_ERROR_INVALID_STATE = SessionError.ERROR_INVALID_STATE;
/** Result code representing that an argument is illegal. */
public static final int RESULT_ERROR_BAD_VALUE = -3;
public static final int RESULT_ERROR_BAD_VALUE = SessionError.ERROR_BAD_VALUE;
/** Result code representing that the command is not allowed. */
public static final int RESULT_ERROR_PERMISSION_DENIED = -4;
public static final int RESULT_ERROR_PERMISSION_DENIED = SessionError.ERROR_PERMISSION_DENIED;
/** Result code representing that a file or network related error happened. */
public static final int RESULT_ERROR_IO = -5;
public static final int RESULT_ERROR_IO = SessionError.ERROR_IO;
/** Result code representing that the command is not supported. */
public static final int RESULT_ERROR_NOT_SUPPORTED = -6;
/** Result code representing that the command is skipped. */
public static final int RESULT_INFO_SKIPPED = 1;
public static final int RESULT_ERROR_NOT_SUPPORTED = SessionError.ERROR_NOT_SUPPORTED;
/** Result code representing that the session and controller were disconnected. */
public static final int RESULT_ERROR_SESSION_DISCONNECTED = -100;
public static final int RESULT_ERROR_SESSION_DISCONNECTED =
SessionError.ERROR_SESSION_DISCONNECTED;
/** Result code representing that the authentication has expired. */
public static final int RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED = -102;
public static final int RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED =
SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED;
/** Result code representing that a premium account is required. */
public static final int RESULT_ERROR_SESSION_PREMIUM_ACCOUNT_REQUIRED = -103;
public static final int RESULT_ERROR_SESSION_PREMIUM_ACCOUNT_REQUIRED =
SessionError.ERROR_SESSION_PREMIUM_ACCOUNT_REQUIRED;
/** Result code representing that too many concurrent streams are detected. */
public static final int RESULT_ERROR_SESSION_CONCURRENT_STREAM_LIMIT = -104;
public static final int RESULT_ERROR_SESSION_CONCURRENT_STREAM_LIMIT =
SessionError.ERROR_SESSION_CONCURRENT_STREAM_LIMIT;
/** Result code representing that the content is blocked due to parental controls. */
public static final int RESULT_ERROR_SESSION_PARENTAL_CONTROL_RESTRICTED = -105;
public static final int RESULT_ERROR_SESSION_PARENTAL_CONTROL_RESTRICTED =
SessionError.ERROR_SESSION_PARENTAL_CONTROL_RESTRICTED;
/** Result code representing that the content is blocked due to being regionally unavailable. */
public static final int RESULT_ERROR_SESSION_NOT_AVAILABLE_IN_REGION = -106;
public static final int RESULT_ERROR_SESSION_NOT_AVAILABLE_IN_REGION =
SessionError.ERROR_SESSION_NOT_AVAILABLE_IN_REGION;
/**
* Result code representing that the application cannot skip any more because the skip limit is
* reached.
*/
public static final int RESULT_ERROR_SESSION_SKIP_LIMIT_REACHED = -107;
public static final int RESULT_ERROR_SESSION_SKIP_LIMIT_REACHED =
SessionError.ERROR_SESSION_SKIP_LIMIT_REACHED;
/** Result code representing that the session needs user's manual intervention. */
public static final int RESULT_ERROR_SESSION_SETUP_REQUIRED = -108;
public static final int RESULT_ERROR_SESSION_SETUP_REQUIRED =
SessionError.ERROR_SESSION_SETUP_REQUIRED;
/** The {@link Code} of this result. */
public final @Code int resultCode;
@ -148,9 +157,15 @@ public final class SessionResult implements Bundleable {
*/
public final long completionTimeMs;
/** The optional session error. */
@UnstableApi @Nullable public final SessionError sessionError;
/**
* Creates an instance with a result code.
*
* <p>Note: Use {@link SessionResult#SessionResult(SessionError)} for errors to provide a
* localized error message for your users.
*
* @param resultCode The result code.
*/
public SessionResult(@Code int resultCode) {
@ -160,17 +175,64 @@ public final class SessionResult implements Bundleable {
/**
* Creates an instance with a result code and an extra {@link Bundle}.
*
* <p>Note: Use {@link SessionResult#SessionResult(SessionError, Bundle)} for errors to provide a
* localized error message for your users.
*
* @param resultCode The result code.
* @param extras The extra {@link Bundle}.
*/
public SessionResult(@Code int resultCode, Bundle extras) {
this(resultCode, extras, SystemClock.elapsedRealtime());
this(
resultCode,
extras,
/* completionTimeMs= */ SystemClock.elapsedRealtime(),
/* sessionError= */ null);
}
private SessionResult(@Code int resultCode, Bundle extras, long completionTimeMs) {
/**
* Creates an instance from a {@link SessionError}. The {@link #resultCode} is taken from {@link
* SessionError#code} and the session result extras {@link Bundle} is empty.
*
* @param sessionError The {@linkplain SessionError session error}.
*/
@UnstableApi
public SessionResult(SessionError sessionError) {
this(
sessionError.code,
Bundle.EMPTY,
/* completionTimeMs= */ SystemClock.elapsedRealtime(),
sessionError);
}
/**
* Creates an instance from a {@link SessionError} and an extras {@link Bundle}. The {@link
* #resultCode} is taken from the {@link SessionError}.
*
* @param sessionError The {@link SessionError}.
* @param extras The extra {@link Bundle}.
*/
@UnstableApi
public SessionResult(SessionError sessionError, Bundle extras) {
this(
sessionError.code,
extras,
/* completionTimeMs= */ SystemClock.elapsedRealtime(),
sessionError);
}
private SessionResult(
@Code int resultCode,
Bundle extras,
long completionTimeMs,
@Nullable SessionError sessionError) {
checkArgument(sessionError == null || resultCode < 0);
this.resultCode = resultCode;
this.extras = new Bundle(extras);
this.completionTimeMs = completionTimeMs;
this.sessionError =
sessionError == null && resultCode < 0
? new SessionError(resultCode, SessionError.DEFAULT_ERROR_MESSAGE)
: sessionError;
}
// Bundleable implementation.
@ -178,6 +240,7 @@ public final class SessionResult implements Bundleable {
private static final String FIELD_RESULT_CODE = Util.intToStringMaxRadix(0);
private static final String FIELD_EXTRAS = Util.intToStringMaxRadix(1);
private static final String FIELD_COMPLETION_TIME_MS = Util.intToStringMaxRadix(2);
private static final String FIELD_SESSION_ERROR = Util.intToStringMaxRadix(3);
@UnstableApi
@Override
@ -186,6 +249,9 @@ public final class SessionResult implements Bundleable {
bundle.putInt(FIELD_RESULT_CODE, resultCode);
bundle.putBundle(FIELD_EXTRAS, extras);
bundle.putLong(FIELD_COMPLETION_TIME_MS, completionTimeMs);
if (sessionError != null) {
bundle.putBundle(FIELD_SESSION_ERROR, sessionError.toBundle());
}
return bundle;
}
@ -202,10 +268,21 @@ public final class SessionResult implements Bundleable {
/** Restores a {@code SessionResult} from a {@link Bundle}. */
@UnstableApi
public static SessionResult fromBundle(Bundle bundle) {
int resultCode = bundle.getInt(FIELD_RESULT_CODE, /* defaultValue= */ RESULT_ERROR_UNKNOWN);
int resultCode =
bundle.getInt(FIELD_RESULT_CODE, /* defaultValue= */ SessionError.ERROR_UNKNOWN);
@Nullable Bundle extras = bundle.getBundle(FIELD_EXTRAS);
long completionTimeMs =
bundle.getLong(FIELD_COMPLETION_TIME_MS, /* defaultValue= */ SystemClock.elapsedRealtime());
return new SessionResult(resultCode, extras == null ? Bundle.EMPTY : extras, completionTimeMs);
@Nullable SessionError sessionError = null;
@Nullable Bundle sessionErrorBundle = bundle.getBundle(FIELD_SESSION_ERROR);
if (sessionErrorBundle != null) {
sessionError = SessionError.fromBundle(sessionErrorBundle);
} else if (resultCode != RESULT_SUCCESS) {
// Populate the session error if the session is of a library version that doesn't have the
// SessionError yet.
sessionError = new SessionError(resultCode, SessionError.DEFAULT_ERROR_MESSAGE);
}
return new SessionResult(
resultCode, extras == null ? Bundle.EMPTY : extras, completionTimeMs, sessionError);
}
}

View File

@ -15,7 +15,7 @@
*/
package androidx.media3.session;
import static androidx.media3.session.LibraryResult.RESULT_ERROR_NOT_SUPPORTED;
import static androidx.media3.session.SessionError.ERROR_NOT_SUPPORTED;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
@ -99,8 +99,7 @@ public class LibraryResultTest {
@Test
public void toBundle_errorResultThatWasUnbundledAsAnUnknownType_noException() {
LibraryResult<ImmutableList<Error>> libraryResult =
LibraryResult.ofError(LibraryResult.RESULT_ERROR_NOT_SUPPORTED);
LibraryResult<ImmutableList<Error>> libraryResult = LibraryResult.ofError(ERROR_NOT_SUPPORTED);
Bundle errorLibraryResultBundle = libraryResult.toBundle();
LibraryResult<?> libraryResultFromUntyped =
LibraryResult.fromUnknownBundle(errorLibraryResultBundle);
@ -109,13 +108,12 @@ public class LibraryResultTest {
assertThat(LibraryResult.fromUnknownBundle(bundleOfUntyped).value).isNull();
assertThat(LibraryResult.fromUnknownBundle(bundleOfUntyped).resultCode)
.isEqualTo(RESULT_ERROR_NOT_SUPPORTED);
.isEqualTo(ERROR_NOT_SUPPORTED);
}
@Test
public void toBundle_voidResultThatWasUnbundledAsAnUnknownType_noException() {
LibraryResult<ImmutableList<Error>> libraryResult =
LibraryResult.ofError(LibraryResult.RESULT_ERROR_NOT_SUPPORTED);
LibraryResult<ImmutableList<Error>> libraryResult = LibraryResult.ofError(ERROR_NOT_SUPPORTED);
Bundle errorLibraryResultBundle = libraryResult.toBundle();
LibraryResult<?> libraryResultFromUntyped =
LibraryResult.fromUnknownBundle(errorLibraryResultBundle);
@ -124,6 +122,27 @@ public class LibraryResultTest {
assertThat(LibraryResult.fromUnknownBundle(bundleOfUntyped).value).isNull();
assertThat(LibraryResult.fromUnknownBundle(bundleOfUntyped).resultCode)
.isEqualTo(RESULT_ERROR_NOT_SUPPORTED);
.isEqualTo(ERROR_NOT_SUPPORTED);
}
@Test
public void toBundle_roundTrip_equalsWithOriginal() {
Bundle errorExtras = new Bundle();
errorExtras.putString("errorKey", "errorValue");
LibraryResult<SessionError> errorLibraryResult =
LibraryResult.ofError(new SessionError(ERROR_NOT_SUPPORTED, "error message", errorExtras));
LibraryResult<?> errorLibraryResultFromBundle =
LibraryResult.fromUnknownBundle(errorLibraryResult.toBundle());
assertThat(errorLibraryResultFromBundle.resultCode).isEqualTo(errorLibraryResult.resultCode);
assertThat(errorLibraryResultFromBundle.sessionError)
.isEqualTo(errorLibraryResult.sessionError);
assertThat(errorLibraryResultFromBundle.sessionError.extras.size()).isEqualTo(1);
assertThat(errorLibraryResultFromBundle.sessionError.extras.getString("errorKey"))
.isEqualTo("errorValue");
assertThat(errorLibraryResultFromBundle.value).isEqualTo(errorLibraryResult.value);
assertThat(errorLibraryResultFromBundle.completionTimeMs)
.isEqualTo(errorLibraryResult.completionTimeMs);
}
}

View File

@ -0,0 +1,67 @@
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.session;
import static androidx.media3.session.SessionError.ERROR_BAD_VALUE;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import android.os.Bundle;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Tests for {@link SessionError}. */
@RunWith(AndroidJUnit4.class)
public class SessionErrorTest {
@Test
public void constructor_twoArguments_usesEmptyBundle() {
SessionError error = new SessionError(ERROR_BAD_VALUE, "error message");
assertThat(error.extras.size()).isEqualTo(0);
}
@Test
public void constructor_withNonErrorCode_throwsIllegalArgumentException() {
assertThrows(
IllegalArgumentException.class,
() -> new SessionError(SessionResult.RESULT_SUCCESS, "error message"));
}
@Test
public void equals_differentBundles_bundleIgnored() {
Bundle errorBundle1 = new Bundle();
errorBundle1.putString("key", "value");
SessionError error1 = new SessionError(ERROR_BAD_VALUE, "error message", errorBundle1);
SessionError error2 = new SessionError(ERROR_BAD_VALUE, "error message");
assertThat(error1).isEqualTo(error2);
}
@Test
public void toBundle_roundTrip_resultsInEqualObjectWithSameBundle() {
Bundle errorBundle = new Bundle();
errorBundle.putString("key", "value");
SessionError error = new SessionError(ERROR_BAD_VALUE, "error message", errorBundle);
SessionError sessionErrorFromBundle = SessionError.fromBundle(error.toBundle());
assertThat(sessionErrorFromBundle).isEqualTo(error);
assertThat(sessionErrorFromBundle.extras.size()).isEqualTo(1);
assertThat(sessionErrorFromBundle.extras.getString("key")).isEqualTo("value");
}
}

View File

@ -0,0 +1,75 @@
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.session;
import static androidx.media3.session.SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED;
import static androidx.media3.session.SessionError.ERROR_SESSION_CONCURRENT_STREAM_LIMIT;
import static com.google.common.truth.Truth.assertThat;
import android.os.Bundle;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Tests for {@link SessionResult}. */
@RunWith(AndroidJUnit4.class)
public class SessionResultTest {
@Test
public void constructor_errorCodeOnly_createsDefaultSessionError() {
SessionResult sessionResult = new SessionResult(ERROR_SESSION_AUTHENTICATION_EXPIRED);
assertThat(sessionResult.resultCode).isEqualTo(ERROR_SESSION_AUTHENTICATION_EXPIRED);
assertThat(sessionResult.extras.size()).isEqualTo(0);
assertThat(sessionResult.sessionError.code).isEqualTo(ERROR_SESSION_AUTHENTICATION_EXPIRED);
assertThat(sessionResult.sessionError.message).isEqualTo(SessionError.DEFAULT_ERROR_MESSAGE);
assertThat(sessionResult.sessionError.extras.size()).isEqualTo(0);
}
@Test
public void constructor_errorCodeAndBundleOnly_createsDefaultSessionError() {
Bundle bundle = new Bundle();
bundle.putString("key", "value");
SessionResult sessionResult = new SessionResult(ERROR_SESSION_CONCURRENT_STREAM_LIMIT, bundle);
assertThat(sessionResult.resultCode).isEqualTo(ERROR_SESSION_CONCURRENT_STREAM_LIMIT);
assertThat(sessionResult.extras.size()).isEqualTo(1);
assertThat(sessionResult.extras.getString("key")).isEqualTo("value");
assertThat(sessionResult.sessionError.code).isEqualTo(ERROR_SESSION_CONCURRENT_STREAM_LIMIT);
assertThat(sessionResult.sessionError.message).isEqualTo(SessionError.DEFAULT_ERROR_MESSAGE);
assertThat(sessionResult.sessionError.extras.size()).isEqualTo(0);
}
@Test
public void toBundle_roundTrip_resultsInEqualObjectWithSameBundle() {
Bundle errorExtras = new Bundle();
errorExtras.putString("errorKey", "errorValue");
SessionResult sessionResult =
new SessionResult(
new SessionError(SessionError.ERROR_NOT_SUPPORTED, "error message", errorExtras));
SessionResult resultFromBundle = SessionResult.fromBundle(sessionResult.toBundle());
assertThat(resultFromBundle.resultCode).isEqualTo(sessionResult.resultCode);
assertThat(resultFromBundle.completionTimeMs).isEqualTo(sessionResult.completionTimeMs);
assertThat(resultFromBundle.sessionError.code).isEqualTo(sessionResult.sessionError.code);
assertThat(resultFromBundle.sessionError.message).isEqualTo(sessionResult.sessionError.message);
assertThat(resultFromBundle.sessionError.extras.size()).isEqualTo(1);
assertThat(resultFromBundle.sessionError.extras.getString("errorKey")).isEqualTo("errorValue");
assertThat(resultFromBundle.extras.size()).isEqualTo(0);
}
}

View File

@ -38,6 +38,8 @@ 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_AUTH_EXPIRED_ERROR_DEPRECATED =
"parent_auth_expired_error_deprecated";
public static final String PARENT_ID_AUTH_EXPIRED_ERROR_KEY_ERROR_RESOLUTION_ACTION_LABEL =
"parent_auth_expired_error_label";

View File

@ -23,6 +23,8 @@ public class MediaBrowserServiceCompatConstants {
"testOnChildrenChanged_subscribeAndUnsubscribe";
public static final String TEST_GET_LIBRARY_ROOT = "getLibraryRoot_correctExtraKeyAndValue";
public static final String TEST_GET_CHILDREN = "getChildren_correctMetadataExtras";
public static final String TEST_GET_CHILDREN_WITH_NULL_LIST =
"onChildrenChanged_withNullChildrenListInLegacyService_convertedToSessionError";
private MediaBrowserServiceCompatConstants() {}
}

View File

@ -38,6 +38,7 @@ import static androidx.media3.test.session.common.MediaBrowserConstants.MEDIA_ID
import static androidx.media3.test.session.common.MediaBrowserConstants.MEDIA_ID_GET_PLAYABLE_ITEM;
import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID;
import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_AUTH_EXPIRED_ERROR;
import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_AUTH_EXPIRED_ERROR_DEPRECATED;
import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_AUTH_EXPIRED_ERROR_KEY_ERROR_RESOLUTION_ACTION_LABEL;
import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_ERROR;
import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_LONG_LIST;
@ -381,8 +382,20 @@ public class MediaBrowserCompatWithMediaLibraryServiceTest
}
@Test
public void getChildren_authErrorResult() throws Exception {
String testParentId = PARENT_ID_AUTH_EXPIRED_ERROR;
public void getChildren_authErrorResult_correctPlaybackStateCompatUpdates() throws Exception {
assertGetChildrenAuthenticationRequired(PARENT_ID_AUTH_EXPIRED_ERROR);
}
@Test
public void getChildren_authErrorResultDeprecated_correctPlaybackStateCompatUpdates()
throws Exception {
// 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);
}
public void assertGetChildrenAuthenticationRequired(String testParentId) throws Exception {
connectAndWait(/* rootHints= */ Bundle.EMPTY);
CountDownLatch errorLatch = new CountDownLatch(1);
AtomicReference<String> parentIdRefOnError = new AtomicReference<>();
@ -399,6 +412,7 @@ public class MediaBrowserCompatWithMediaLibraryServiceTest
assertThat(errorLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(parentIdRefOnError.get()).isEqualTo(testParentId);
assertThat(firstPlaybackStateCompatReported.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(lastReportedPlaybackStateCompat.getState())
.isEqualTo(PlaybackStateCompat.STATE_ERROR);
assertThat(
@ -456,12 +470,11 @@ public class MediaBrowserCompatWithMediaLibraryServiceTest
}
@Test
public void getChildren_nullResult() throws Exception {
public void getChildren_errorLibraryResult() throws Exception {
String testParentId = PARENT_ID_ERROR;
connectAndWait(/* rootHints= */ Bundle.EMPTY);
CountDownLatch latch = new CountDownLatch(1);
AtomicReference<String> parentIdRef = new AtomicReference<>();
AtomicBoolean onChildrenLoadedWithBundleCalled = new AtomicBoolean();
browserCompat.subscribe(
testParentId,
@ -471,16 +484,10 @@ public class MediaBrowserCompatWithMediaLibraryServiceTest
parentIdRef.set(parentId);
latch.countDown();
}
@Override
public void onChildrenLoaded(String parentId, List<MediaItem> children, Bundle options) {
onChildrenLoadedWithBundleCalled.set(true);
}
});
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(parentIdRef.get()).isEqualTo(testParentId);
assertThat(onChildrenLoadedWithBundleCalled.get()).isFalse();
}
@Test

View File

@ -58,16 +58,18 @@ public class MediaBrowserCompatWithMediaSessionServiceTest {
Context context;
TestHandler handler;
MediaBrowserCompat browserCompat;
@Nullable MediaBrowserCompat browserCompat;
@Nullable MediaControllerCompat controllerCompat;
TestConnectionCallback connectionCallback;
@Nullable PlaybackStateCompat lastReportedPlaybackStateCompat;
@Nullable CountDownLatch firstPlaybackStateCompatReported;
@Before
public void setUp() {
context = ApplicationProvider.getApplicationContext();
handler = threadTestRule.getHandler();
connectionCallback = new TestConnectionCallback();
firstPlaybackStateCompatReported = new CountDownLatch(1);
}
@After
@ -131,13 +133,13 @@ public class MediaBrowserCompatWithMediaSessionServiceTest {
@Override
public void onConnected() {
super.onConnected();
// Make browser's internal handler to be initialized with test thread.
controllerCompat = new MediaControllerCompat(context, browserCompat.getSessionToken());
controllerCompatCallback =
new MediaControllerCompat.Callback() {
@Override
public void onPlaybackStateChanged(PlaybackStateCompat state) {
lastReportedPlaybackStateCompat = state;
firstPlaybackStateCompatReported.countDown();
}
};
controllerCompat.registerCallback(controllerCompatCallback);

View File

@ -15,11 +15,11 @@
*/
package androidx.media3.session;
import static androidx.media3.session.LibraryResult.RESULT_ERROR_BAD_VALUE;
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.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(RESULT_ERROR_BAD_VALUE);
assertThat(result.resultCode).isEqualTo(ERROR_BAD_VALUE);
assertThat(result.value).isNull();
}
@ -246,14 +246,33 @@ public class MediaBrowserListenerTest extends MediaControllerListenerTest {
@Test
public void getChildren_nullResult() throws Exception {
String parentId = MediaBrowserConstants.PARENT_ID_ERROR;
MediaBrowser browser = createBrowser();
LibraryResult<ImmutableList<MediaItem>> result =
threadTestRule
.getHandler()
.postAndSync(() -> browser.getChildren(parentId, 1, 1, null))
.get(TIMEOUT_MS, MILLISECONDS);
assertThat(result.resultCode).isNotEqualTo(RESULT_SUCCESS);
assertThat(result.resultCode).isLessThan(0);
assertThat(result.value).isNull();
}
@Test
public void getChildren_errorLibraryResult() throws Exception {
String parentId = MediaBrowserConstants.PARENT_ID_ERROR;
MediaBrowser browser = createBrowser();
LibraryResult<ImmutableList<MediaItem>> result =
threadTestRule
.getHandler()
.postAndSync(() -> browser.getChildren(parentId, 1, 1, null))
.get(TIMEOUT_MS, MILLISECONDS);
assertThat(result.resultCode).isLessThan(0);
assertThat(result.sessionError.code).isLessThan(0);
assertThat(result.sessionError.message).isEqualTo("error message");
assertThat(result.sessionError.extras.getString("key")).isEqualTo("value");
assertThat(result.value).isNull();
}

View File

@ -27,6 +27,7 @@ import static androidx.media3.test.session.common.MediaBrowserConstants.ROOT_ID;
import static androidx.media3.test.session.common.MediaBrowserConstants.ROOT_ID_SUPPORTS_BROWSABLE_CHILDREN_ONLY;
import static androidx.media3.test.session.common.MediaBrowserServiceCompatConstants.TEST_CONNECT_REJECTED;
import static androidx.media3.test.session.common.MediaBrowserServiceCompatConstants.TEST_GET_CHILDREN;
import static androidx.media3.test.session.common.MediaBrowserServiceCompatConstants.TEST_GET_CHILDREN_WITH_NULL_LIST;
import static androidx.media3.test.session.common.MediaBrowserServiceCompatConstants.TEST_GET_LIBRARY_ROOT;
import static androidx.media3.test.session.common.MediaBrowserServiceCompatConstants.TEST_ON_CHILDREN_CHANGED_SUBSCRIBE_AND_UNSUBSCRIBE;
import static androidx.media3.test.session.common.TestUtils.TIMEOUT_MS;
@ -151,6 +152,25 @@ public class MediaBrowserListenerWithMediaBrowserServiceCompatTest {
Thread.sleep(TIMEOUT_MS);
}
@Test
public void onChildrenChanged_withNullChildrenListInLegacyService_convertedToSessionError()
throws Exception {
String testParentId = TEST_GET_CHILDREN_WITH_NULL_LIST;
remoteService.setProxyForTest(TEST_GET_CHILDREN_WITH_NULL_LIST);
MediaBrowser browser = createBrowser(/* listener= */ null);
LibraryResult<Void> resultForSubscribe =
threadTestRule
.getHandler()
.postAndSync(() -> browser.subscribe(testParentId, null))
.get(TIMEOUT_MS, MILLISECONDS);
assertThat(resultForSubscribe.resultCode).isEqualTo(SessionError.ERROR_UNKNOWN);
assertThat(resultForSubscribe.sessionError.code).isEqualTo(SessionError.ERROR_UNKNOWN);
assertThat(resultForSubscribe.sessionError.message)
.isEqualTo(SessionError.DEFAULT_ERROR_MESSAGE);
}
@Test
public void getLibraryRoot_correctExtraKeyAndValue() throws Exception {
remoteService.setProxyForTest(TEST_GET_LIBRARY_ROOT);

View File

@ -15,8 +15,9 @@
*/
package androidx.media3.session;
import static androidx.media3.session.LibraryResult.RESULT_ERROR_NOT_SUPPORTED;
import static androidx.media3.session.LibraryResult.RESULT_ERROR_SESSION_SETUP_REQUIRED;
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_SETUP_REQUIRED;
import static androidx.media3.test.session.common.MediaBrowserConstants.SUBSCRIBE_PARENT_ID_1;
import static androidx.media3.test.session.common.TestUtils.TIMEOUT_MS;
import static com.google.common.truth.Truth.assertThat;
@ -166,7 +167,7 @@ public class MediaLibrarySessionCallbackTest {
@Nullable LibraryParams params) {
latch.countDown();
subscribedControllers.addAll(session.getSubscribedControllers(parentId));
return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_NOT_SUPPORTED));
return Futures.immediateFuture(LibraryResult.ofError(ERROR_NOT_SUPPORTED));
}
};
MockMediaLibraryService service = new MockMediaLibraryService();
@ -215,7 +216,7 @@ public class MediaLibrarySessionCallbackTest {
int resultCode = browser.subscribe(testParentId, testParams).resultCode;
assertThat(session.getSubscribedControllers(testParentId)).isEmpty();
assertThat(resultCode).isEqualTo(RESULT_ERROR_NOT_SUPPORTED);
assertThat(resultCode).isEqualTo(ERROR_NOT_SUPPORTED);
assertThat(session.getSubscribedControllers(testParentId)).isEmpty();
}
@ -234,7 +235,7 @@ public class MediaLibrarySessionCallbackTest {
public ListenableFuture<LibraryResult<MediaItem>> onGetItem(
MediaLibrarySession session, ControllerInfo browser, String mediaId) {
return Futures.immediateFuture(
LibraryResult.ofError(RESULT_ERROR_SESSION_SETUP_REQUIRED));
LibraryResult.ofError(ERROR_SESSION_SETUP_REQUIRED));
}
})
.setId("testOnSubscribe")
@ -244,7 +245,7 @@ public class MediaLibrarySessionCallbackTest {
int resultCode = browser.subscribe(SUBSCRIBE_PARENT_ID_1, testParams).resultCode;
assertThat(resultCode).isEqualTo(RESULT_ERROR_SESSION_SETUP_REQUIRED);
assertThat(resultCode).isEqualTo(ERROR_SESSION_SETUP_REQUIRED);
assertThat(session.getSubscribedControllers(SUBSCRIBE_PARENT_ID_1)).isEmpty();
}
@ -413,7 +414,7 @@ public class MediaLibrarySessionCallbackTest {
/* params= */ null);
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(recentItem.resultCode).isEqualTo(LibraryResult.RESULT_ERROR_INVALID_STATE);
assertThat(recentItem.resultCode).isEqualTo(ERROR_INVALID_STATE);
}
@Test

View File

@ -17,8 +17,8 @@ package androidx.media3.session;
import static androidx.media3.common.Player.COMMAND_PLAY_PAUSE;
import static androidx.media3.session.MediaTestUtils.createMediaItem;
import static androidx.media3.session.SessionResult.RESULT_ERROR_INVALID_STATE;
import static androidx.media3.session.SessionResult.RESULT_ERROR_PERMISSION_DENIED;
import static androidx.media3.session.SessionError.ERROR_INVALID_STATE;
import static androidx.media3.session.SessionError.ERROR_PERMISSION_DENIED;
import static androidx.media3.session.SessionResult.RESULT_INFO_SKIPPED;
import static androidx.media3.session.SessionResult.RESULT_SUCCESS;
import static androidx.media3.test.session.common.CommonConstants.METADATA_MEDIA_URI;
@ -203,7 +203,7 @@ public class MediaSessionCallbackTest {
assertThat(layout).containsExactly(button1Disabled, button2).inOrder();
assertThat(remoteController.sendCustomCommand(button1.sessionCommand, Bundle.EMPTY).resultCode)
.isEqualTo(RESULT_ERROR_PERMISSION_DENIED);
.isEqualTo(ERROR_PERMISSION_DENIED);
assertThat(remoteController.sendCustomCommand(button2.sessionCommand, Bundle.EMPTY).resultCode)
.isEqualTo(RESULT_SUCCESS);
}
@ -369,7 +369,7 @@ public class MediaSessionCallbackTest {
assertThat(controllerInfo.isTrusted()).isFalse();
commands.add(command);
if (command == Player.COMMAND_PREPARE) {
return RESULT_ERROR_INVALID_STATE;
return ERROR_INVALID_STATE;
}
return RESULT_SUCCESS;
}

View File

@ -23,7 +23,7 @@ import static androidx.media3.common.Player.STATE_ENDED;
import static androidx.media3.common.Player.STATE_IDLE;
import static androidx.media3.common.Player.STATE_READY;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.session.SessionResult.RESULT_ERROR_INVALID_STATE;
import static androidx.media3.session.SessionError.ERROR_INVALID_STATE;
import static androidx.media3.session.SessionResult.RESULT_SUCCESS;
import static androidx.media3.test.session.common.CommonConstants.SUPPORT_APP_PACKAGE_NAME;
import static androidx.media3.test.session.common.TestUtils.LONG_TIMEOUT_MS;
@ -1883,7 +1883,7 @@ public class MediaSessionCallbackWithMediaControllerCompatTest {
commands.add(command);
if (command == COMMAND_PLAY_PAUSE) {
latchForPause.countDown();
return RESULT_ERROR_INVALID_STATE;
return ERROR_INVALID_STATE;
}
return RESULT_SUCCESS;
}

View File

@ -25,6 +25,7 @@ import static androidx.media3.test.session.common.MediaBrowserConstants.ROOT_ID;
import static androidx.media3.test.session.common.MediaBrowserConstants.ROOT_ID_SUPPORTS_BROWSABLE_CHILDREN_ONLY;
import static androidx.media3.test.session.common.MediaBrowserServiceCompatConstants.TEST_CONNECT_REJECTED;
import static androidx.media3.test.session.common.MediaBrowserServiceCompatConstants.TEST_GET_CHILDREN;
import static androidx.media3.test.session.common.MediaBrowserServiceCompatConstants.TEST_GET_CHILDREN_WITH_NULL_LIST;
import static androidx.media3.test.session.common.MediaBrowserServiceCompatConstants.TEST_GET_LIBRARY_ROOT;
import static androidx.media3.test.session.common.MediaBrowserServiceCompatConstants.TEST_ON_CHILDREN_CHANGED_SUBSCRIBE_AND_UNSUBSCRIBE;
@ -250,6 +251,9 @@ public class MockMediaBrowserServiceCompat extends MediaBrowserServiceCompat {
case TEST_GET_CHILDREN:
setProxyForTestGetChildren_correctMetadataExtras();
break;
case TEST_GET_CHILDREN_WITH_NULL_LIST:
setProxyForTestOnChildrenChanged_withNullChildrenListInLegacyService_convertedToSessionError();
break;
default:
throw new IllegalArgumentException("Unknown testName: " + testName);
}
@ -298,6 +302,23 @@ public class MockMediaBrowserServiceCompat extends MediaBrowserServiceCompat {
});
}
private void
setProxyForTestOnChildrenChanged_withNullChildrenListInLegacyService_convertedToSessionError() {
setMediaBrowserServiceProxy(
new MockMediaBrowserServiceCompat.Proxy() {
@Override
public void onLoadChildren(String parentId, Result<List<MediaItem>> result) {
onLoadChildren(parentId, result, new Bundle());
}
@Override
public void onLoadChildren(
String parentId, Result<List<MediaItem>> result, Bundle bundle) {
result.sendResult(null);
}
});
}
private void setProxyForTestGetLibraryRoot_correctExtraKeyAndValue() {
setMediaBrowserServiceProxy(
new MockMediaBrowserServiceCompat.Proxy() {

View File

@ -16,12 +16,13 @@
package androidx.media3.session;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.session.LibraryResult.RESULT_ERROR_BAD_VALUE;
import static androidx.media3.session.MediaConstants.EXTRAS_KEY_COMPLETION_STATUS;
import static androidx.media3.session.MediaConstants.EXTRAS_KEY_ERROR_RESOLUTION_ACTION_INTENT_COMPAT;
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.SessionError.ERROR_BAD_VALUE;
import static androidx.media3.session.SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED;
import static androidx.media3.test.session.common.CommonConstants.SUPPORT_APP_PACKAGE_NAME;
import static androidx.media3.test.session.common.MediaBrowserConstants.CUSTOM_ACTION;
import static androidx.media3.test.session.common.MediaBrowserConstants.CUSTOM_ACTION_ASSERT_PARAMS;
@ -37,6 +38,7 @@ import static androidx.media3.test.session.common.MediaBrowserConstants.MEDIA_ID
import static androidx.media3.test.session.common.MediaBrowserConstants.MEDIA_ID_GET_PLAYABLE_ITEM;
import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID;
import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_AUTH_EXPIRED_ERROR;
import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_AUTH_EXPIRED_ERROR_DEPRECATED;
import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_AUTH_EXPIRED_ERROR_KEY_ERROR_RESOLUTION_ACTION_LABEL;
import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_ERROR;
import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_LONG_LIST;
@ -319,7 +321,7 @@ public class MockMediaLibraryService extends MediaLibraryService {
LibraryResult.ofItem(createMediaItemWithMetadata(mediaId), /* params= */ null));
default: // fall out
}
return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_BAD_VALUE));
return Futures.immediateFuture(LibraryResult.ofError(ERROR_BAD_VALUE));
}
@Override
@ -345,8 +347,12 @@ public class MockMediaLibraryService extends MediaLibraryService {
}
return Futures.immediateFuture(LibraryResult.ofItemList(list, params));
} else if (Objects.equals(parentId, PARENT_ID_ERROR)) {
return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_BAD_VALUE));
} else if (Objects.equals(parentId, PARENT_ID_AUTH_EXPIRED_ERROR)) {
Bundle errorBundle = new Bundle();
errorBundle.putString("key", "value");
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)) {
Bundle bundle = new Bundle();
Intent signInIntent = new Intent("action");
int flags = Util.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0;
@ -357,12 +363,17 @@ public class MockMediaLibraryService extends MediaLibraryService {
bundle.putString(
EXTRAS_KEY_ERROR_RESOLUTION_ACTION_LABEL_COMPAT,
PARENT_ID_AUTH_EXPIRED_ERROR_KEY_ERROR_RESOLUTION_ACTION_LABEL);
return Futures.immediateFuture(
return Objects.equals(parentId, PARENT_ID_AUTH_EXPIRED_ERROR)
? Futures.immediateFuture(
LibraryResult.ofError(
LibraryResult.RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED,
new SessionError(ERROR_SESSION_AUTHENTICATION_EXPIRED, "error message", bundle),
new LibraryParams.Builder().build()))
: Futures.immediateFuture(
LibraryResult.ofError(
ERROR_SESSION_AUTHENTICATION_EXPIRED,
new LibraryParams.Builder().setExtras(bundle).build()));
}
return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_BAD_VALUE, params));
return Futures.immediateFuture(LibraryResult.ofError(ERROR_BAD_VALUE, params));
}
@Override
@ -451,7 +462,7 @@ public class MockMediaLibraryService extends MediaLibraryService {
return Futures.immediateFuture(LibraryResult.ofItemList(ImmutableList.of(), params));
} else {
// SEARCH_QUERY_ERROR will be handled here.
return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_BAD_VALUE));
return Futures.immediateFuture(LibraryResult.ofError(ERROR_BAD_VALUE));
}
}
@ -474,7 +485,7 @@ public class MockMediaLibraryService extends MediaLibraryService {
return Futures.immediateFuture(new SessionResult(SessionResult.RESULT_SUCCESS));
default: // fall out
}
return Futures.immediateFuture(new SessionResult(SessionResult.RESULT_ERROR_BAD_VALUE));
return Futures.immediateFuture(new SessionResult(ERROR_BAD_VALUE));
}
private void assertLibraryParams(@Nullable LibraryParams params) {