/*
* Copyright 2019 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.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Assertions.checkNotEmpty;
import static androidx.media3.common.util.Assertions.checkState;
import static java.lang.annotation.ElementType.TYPE_USE;
import android.os.Bundle;
import android.os.IBinder;
import android.os.SystemClock;
import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.MediaSessionCompat;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.core.app.BundleCompat;
import androidx.media3.common.BundleListRetriever;
import androidx.media3.common.Bundleable;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MediaMetadata;
import androidx.media3.common.util.BundleableUtil;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.session.MediaLibraryService.LibraryParams;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.ListenableFuture;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.List;
/**
* A result to be used with {@link ListenableFuture} for asynchronous calls between {@link
* MediaLibraryService.MediaLibrarySession} and {@link MediaBrowser}.
*/
public final class LibraryResult<V> implements Bundleable {
/** Result codes. */
@Documented
@Retention(RetentionPolicy.SOURCE)
@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
})
public @interface Code {}
/**
* Result code representing that the command is successfully completed.
*
* <p>Interoperability: This code is also used to tell that the command was successfully sent, but
* the result is unknown when connected with {@link MediaSessionCompat} or {@link
* MediaControllerCompat}.
*/
public static final int RESULT_SUCCESS = 0;
/** Result code representing that the command is ended with an unknown error. */
public static final int RESULT_ERROR_UNKNOWN = -1;
/**
* 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;
/** Result code representing that an argument is illegal. */
public static final int RESULT_ERROR_BAD_VALUE = -3;
/** Result code representing that the command is not allowed. */
public static final int RESULT_ERROR_PERMISSION_DENIED = -4;
/** Result code representing that a file or network related error happened. */
public static final int RESULT_ERROR_IO = -5;
/** 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;
/** Result code representing that the session and controller were disconnected. */
public static final int RESULT_ERROR_SESSION_DISCONNECTED = -100;
/** Result code representing that the authentication has expired. */
public static final int RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED = -102;
/** Result code representing that a premium account is required. */
public static final int RESULT_ERROR_SESSION_PREMIUM_ACCOUNT_REQUIRED = -103;
/** Result code representing that too many concurrent streams are detected. */
public static final int RESULT_ERROR_SESSION_CONCURRENT_STREAM_LIMIT = -104;
/** Result code representing that the content is blocked due to parental controls. */
public static final int RESULT_ERROR_SESSION_PARENTAL_CONTROL_RESTRICTED = -105;
/** 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;
/**
* 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;
/** Result code representing that the session needs user's manual intervention. */
public static final int RESULT_ERROR_SESSION_SETUP_REQUIRED = -108;
/** The {@link Code} of this result. */
public final @Code int resultCode;
/**
* The completion time of the command in milliseconds. It's the same as {@link
* SystemClock#elapsedRealtime()} when the command is completed.
*/
public final long completionTimeMs;
/**
* The value of this result. Will be {@code null} if {@link #resultCode} is not {@link
* #RESULT_SUCCESS}.
*/
@Nullable public final V value;
private final @ValueType int valueType;
/** The optional parameters. */
@Nullable public final MediaLibraryService.LibraryParams params;
/** Creates an instance with {@link #resultCode}{@code ==}{@link #RESULT_SUCCESS}. */
public static LibraryResult<Void> ofVoid() {
return new LibraryResult<>(
RESULT_SUCCESS,
SystemClock.elapsedRealtime(),
/* params= */ null,
/* value= */ null,
VALUE_TYPE_VOID);
}
/**
* Creates an instance with {@link #resultCode}{@code ==}{@link #RESULT_SUCCESS} and optional
* {@link LibraryParams params}.
*/
public static LibraryResult<Void> ofVoid(@Nullable LibraryParams params) {
return new LibraryResult<>(
RESULT_SUCCESS, SystemClock.elapsedRealtime(), params, /* value= */ null, VALUE_TYPE_VOID);
}
/**
* Creates an instance with a media item and {@link #resultCode}{@code ==}{@link #RESULT_SUCCESS}.
*
* <p>The {@link MediaItem#mediaMetadata} must specify {@link MediaMetadata#isBrowsable} (or
* {@link MediaMetadata#folderType}) and {@link MediaMetadata#isPlayable} fields.
*
* @param item The media item.
* @param params The optional parameters to describe the media item.
*/
public static LibraryResult<MediaItem> ofItem(MediaItem item, @Nullable LibraryParams params) {
verifyMediaItem(item);
return new LibraryResult<>(
RESULT_SUCCESS, SystemClock.elapsedRealtime(), params, item, VALUE_TYPE_ITEM);
}
/**
* Creates an instance with a list of media items and {@link #resultCode}{@code ==}{@link
* #RESULT_SUCCESS}.
*
* <p>The {@link MediaItem#mediaMetadata} of each item in the list must specify {@link
* MediaMetadata#isBrowsable} (or {@link MediaMetadata#folderType}) and {@link
* MediaMetadata#isPlayable} fields.
*
* @param items The list of media items.
* @param params The optional parameters to describe the list of media items.
*/
public static LibraryResult<ImmutableList<MediaItem>> ofItemList(
List<MediaItem> items, @Nullable LibraryParams params) {
for (MediaItem item : items) {
verifyMediaItem(item);
}
return new LibraryResult<>(
RESULT_SUCCESS,
SystemClock.elapsedRealtime(),
params,
ImmutableList.copyOf(items),
VALUE_TYPE_ITEM_LIST);
}
/**
* Creates an instance with an unsuccessful {@link Code result code}.
*
* <p>{@code errorCode} must not be {@link #RESULT_SUCCESS}.
*
* @param errorCode The error code.
*/
public static <V> LibraryResult<V> ofError(@Code int errorCode) {
return ofError(errorCode, /* params= */ null);
}
/**
* Creates an instance with an unsuccessful {@link Code result code} and {@link LibraryParams} to
* describe the error.
*
* <p>{@code errorCode} must not be {@link #RESULT_SUCCESS}.
*
* @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,
/* value= */ null,
VALUE_TYPE_ERROR);
}
private LibraryResult(
@Code int resultCode,
long completionTimeMs,
@Nullable LibraryParams params,
@Nullable V value,
@ValueType int valueType) {
this.resultCode = resultCode;
this.completionTimeMs = completionTimeMs;
this.params = params;
this.value = value;
this.valueType = valueType;
}
private static void verifyMediaItem(MediaItem item) {
checkNotEmpty(item.mediaId, "mediaId must not be empty");
checkArgument(item.mediaMetadata.isBrowsable != null, "mediaMetadata must specify isBrowsable");
checkArgument(item.mediaMetadata.isPlayable != null, "mediaMetadata must specify isPlayable");
}
// Bundleable implementation.
private static final String FIELD_RESULT_CODE = Util.intToStringMaxRadix(0);
private static final String FIELD_COMPLETION_TIME_MS = Util.intToStringMaxRadix(1);
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);
// Casting V to ImmutableList<MediaItem> is safe if valueType == VALUE_TYPE_ITEM_LIST.
@SuppressWarnings("unchecked")
@UnstableApi
@Override
public Bundle toBundle() {
Bundle bundle = new Bundle();
bundle.putInt(FIELD_RESULT_CODE, resultCode);
bundle.putLong(FIELD_COMPLETION_TIME_MS, completionTimeMs);
if (params != null) {
bundle.putBundle(FIELD_PARAMS, params.toBundle());
}
bundle.putInt(FIELD_VALUE_TYPE, valueType);
if (value == null) {
return bundle;
}
switch (valueType) {
case VALUE_TYPE_ITEM:
bundle.putBundle(FIELD_VALUE, ((MediaItem) value).toBundle());
break;
case VALUE_TYPE_ITEM_LIST:
BundleCompat.putBinder(
bundle,
FIELD_VALUE,
new BundleListRetriever(BundleableUtil.toBundleList((ImmutableList<MediaItem>) value)));
break;
case VALUE_TYPE_VOID:
case VALUE_TYPE_ERROR:
// value must be null for both these types, so we should have returned above.
throw new IllegalStateException();
}
return bundle;
}
/** Object that can restore a {@code LibraryResult<Void>} from a {@link Bundle}. */
@UnstableApi
public static final Creator<LibraryResult<Void>> VOID_CREATOR = LibraryResult::fromVoidBundle;
/** Object that can restore a {@code LibraryResult<MediaItem>} from a {@link Bundle}. */
@UnstableApi
public static final Creator<LibraryResult<MediaItem>> ITEM_CREATOR =
LibraryResult::fromItemBundle;
/**
* Object that can restore a {@code LibraryResult<ImmutableList<MediaItem>} from a {@link Bundle}.
*/
@UnstableApi
public static final Creator<LibraryResult<ImmutableList<MediaItem>>> ITEM_LIST_CREATOR =
LibraryResult::fromItemListBundle;
/**
* Object that can restore a {@code LibraryResult} with unknown value type from a {@link Bundle}.
*/
@UnstableApi
public static final Creator<LibraryResult<?>> UNKNOWN_TYPE_CREATOR =
LibraryResult::fromUnknownBundle;
// fromBundle will throw if the bundle doesn't have the right value type.
@SuppressWarnings("unchecked")
private static LibraryResult<Void> fromVoidBundle(Bundle bundle) {
return (LibraryResult<Void>) fromUnknownBundle(bundle);
}
// fromBundle will throw if the bundle doesn't have the right value type.
@SuppressWarnings("unchecked")
private static LibraryResult<MediaItem> fromItemBundle(Bundle bundle) {
return (LibraryResult<MediaItem>) fromBundle(bundle, VALUE_TYPE_ITEM);
}
// fromBundle will throw if the bundle doesn't have the right value type.
@SuppressWarnings("unchecked")
private static LibraryResult<ImmutableList<MediaItem>> fromItemListBundle(Bundle bundle) {
return (LibraryResult<ImmutableList<MediaItem>>) fromBundle(bundle, VALUE_TYPE_ITEM_LIST);
}
private static LibraryResult<?> fromUnknownBundle(Bundle bundle) {
return fromBundle(bundle, /* expectedType= */ null);
}
/**
* Constructs a new instance from {@code bundle}.
*
* @throws IllegalStateException if {@code expectedType} is non-null and doesn't match the value
* type read from {@code bundle}.
*/
private static LibraryResult<?> fromBundle(
Bundle bundle, @Nullable @ValueType Integer expectedType) {
int resultCode = bundle.getInt(FIELD_RESULT_CODE, /* defaultValue= */ RESULT_SUCCESS);
long completionTimeMs =
bundle.getLong(FIELD_COMPLETION_TIME_MS, /* defaultValue= */ SystemClock.elapsedRealtime());
@Nullable Bundle paramsBundle = bundle.getBundle(FIELD_PARAMS);
@Nullable
MediaLibraryService.LibraryParams params =
paramsBundle == null ? null : LibraryParams.CREATOR.fromBundle(paramsBundle);
@ValueType int valueType = bundle.getInt(FIELD_VALUE_TYPE);
@Nullable Object value;
switch (valueType) {
case VALUE_TYPE_ITEM:
checkState(expectedType == null || expectedType == VALUE_TYPE_ITEM);
@Nullable Bundle valueBundle = bundle.getBundle(FIELD_VALUE);
value = valueBundle == null ? null : MediaItem.CREATOR.fromBundle(valueBundle);
break;
case VALUE_TYPE_ITEM_LIST:
checkState(expectedType == null || expectedType == VALUE_TYPE_ITEM_LIST);
@Nullable IBinder valueRetriever = BundleCompat.getBinder(bundle, FIELD_VALUE);
value =
valueRetriever == null
? null
: BundleableUtil.fromBundleList(
MediaItem.CREATOR, BundleListRetriever.getList(valueRetriever));
break;
case VALUE_TYPE_VOID:
case VALUE_TYPE_ERROR:
value = null;
break;
default:
throw new IllegalStateException();
}
return new LibraryResult<>(resultCode, completionTimeMs, params, value, VALUE_TYPE_ITEM_LIST);
}
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef({VALUE_TYPE_VOID, VALUE_TYPE_ITEM, VALUE_TYPE_ITEM_LIST, VALUE_TYPE_ERROR})
private @interface ValueType {}
private static final int VALUE_TYPE_VOID = 1;
private static final int VALUE_TYPE_ITEM = 2;
private static final int VALUE_TYPE_ITEM_LIST = 3;
/** The value type isn't known because the result is carrying an error. */
private static final int VALUE_TYPE_ERROR = 4;
}