/*
* 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.checkNotNull;
import static androidx.media3.session.LibraryResult.RESULT_ERROR_NOT_SUPPORTED;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
import androidx.annotation.IntRange;
import androidx.annotation.Nullable;
import androidx.media.MediaSessionManager.RemoteUserInfo;
import androidx.media3.common.Bundleable;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MediaMetadata;
import androidx.media3.common.Player;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.session.MediaSession.ControllerInfo;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
/**
* Superclass to be extended by services hosting {@link MediaLibrarySession media library sessions}.
*
* <p>It enables applications to browse media content provided by an application, ask the
* application to start playback, and control the playback.
*
* <p>To extend this class, declare the intent filter in your {@code AndroidManifest.xml}.
*
* <pre>{@code
* <service android:name="NameOfYourService">
* <intent-filter>
* <action android:name="androidx.media3.session.MediaLibraryService">
* </intent-filter>
* </service>
* }</pre>
*
* <p>You may also declare {@code android.media.browse.MediaBrowserService} for compatibility with
* {@link android.support.v4.media.MediaBrowserCompat} so that this service can handle the case.
*
* @see MediaSessionService
*/
public abstract class MediaLibraryService extends MediaSessionService {
/** The action for {@link Intent} filter that must be declared by the service. */
public static final String SERVICE_INTERFACE = "androidx.media3.session.MediaLibraryService";
/**
* An extended {@link MediaSession} for the {@link MediaLibraryService}. Build an instance with
* {@link Builder} and return it from {@link MediaSessionService#onGetSession(ControllerInfo)}.
*
* <h2 id="BackwardCompatibility">Backward compatibility with legacy media browser APIs</h2>
*
* <p>A library session supports connection from both {@link MediaBrowser} and {@link
* android.support.v4.media.MediaBrowserCompat}, but the {@link ControllerInfo} may not be
* precise. Here are the details.
*
* <table>
* <caption>Summary when controller info isn't precise</caption>
* <tr>
* <th>SDK version</th>
* <th>{@link ControllerInfo#getPackageName()}<br>for legacy browser</th>
* <th>{@link ControllerInfo#getUid()}<br>for legacy browser</th></tr>
* <tr>
* <td>{@code SDK_INT < 21}</td>
* <td>Actual package name via {@link Context#getPackageName()}</td>
* <td>Actual UID</td>
* </tr>
* <tr>
* <td>
* {@code 21 <= SDK_INT < 28}<br>
* for {@link Callback#onConnect onConnect}<br>
* and {@link Callback#onGetLibraryRoot onGetLibraryRoot}
* </td>
* <td>Actual package name via {@link Context#getPackageName()}</td>
* <td>Actual UID</td>
* </tr>
* <tr>
* <td>
* {@code 21 <= SDK_INT < 28}<br>
* for other {@link Callback callbacks}
* </td>
* <td>{@link RemoteUserInfo#LEGACY_CONTROLLER}</td>
* <td>Negative value</td>
* </tr>
* <tr>
* <td>{@code 28 <= SDK_INT}</td>
* <td>Actual package name via {@link Context#getPackageName()}</td>
* <td>Actual UID</td>
* </tr>
* </table>
*/
public static final class MediaLibrarySession extends MediaSession {
/**
* An extended {@link MediaSession.Callback} for the {@link MediaLibrarySession}.
*
* <p>When you return {@link LibraryResult} with {@link MediaItem media items}, each item must
* have valid {@link MediaItem#mediaId} and specify {@link MediaMetadata#isBrowsable} (or {@link
* MediaMetadata#folderType}) and {@link MediaMetadata#isPlayable} in its {@link
* MediaItem#mediaMetadata}.
*/
public interface Callback extends MediaSession.Callback {
@Override
default ConnectionResult onConnect(MediaSession session, ControllerInfo controller) {
SessionCommands sessionCommands =
new SessionCommands.Builder().addAllLibraryCommands().addAllSessionCommands().build();
Player.Commands playerCommands = new Player.Commands.Builder().addAllCommands().build();
return ConnectionResult.accept(sessionCommands, playerCommands);
}
/**
* Called when a {@link MediaBrowser} requests the root {@link MediaItem} by {@link
* MediaBrowser#getLibraryRoot(LibraryParams)}.
*
* <p>Return a {@link ListenableFuture} to send a {@link LibraryResult} back to the browser
* asynchronously. You can also return a {@link LibraryResult} directly by using Guava's
* {@link Futures#immediateFuture(Object)}.
*
* <p>The {@link LibraryResult#params} may differ from the given {@link LibraryParams params}
* if the session can't provide a root that matches with the {@code params}.
*
* <p>To allow browsing the media library, return a {@link LibraryResult} with {@link
* LibraryResult#RESULT_SUCCESS} and a root {@link MediaItem} with a valid {@link
* MediaItem#mediaId}. The media id is required for the browser to get the children under the
* root.
*
* <p>Interoperability: If this callback is called because a legacy {@link
* android.support.v4.media.MediaBrowserCompat} has requested a {@link
* androidx.media.MediaBrowserServiceCompat.BrowserRoot}, then the main thread may be blocked
* until the returned future is done. If your service may be queried by a legacy {@link
* android.support.v4.media.MediaBrowserCompat}, you should ensure that the future completes
* quickly to avoid blocking the main thread for a long period of time.
*
* @param session The session for this event.
* @param browser The browser information.
* @param params The optional parameters passed by the browser.
* @return A pending result that will be resolved with a root media item.
* @see SessionCommand#COMMAND_CODE_LIBRARY_GET_LIBRARY_ROOT
*/
default ListenableFuture<LibraryResult<MediaItem>> onGetLibraryRoot(
MediaLibrarySession session, ControllerInfo browser, @Nullable LibraryParams params) {
return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_NOT_SUPPORTED));
}
/**
* Called when a {@link MediaBrowser} requests a {@link MediaItem} by {@link
* MediaBrowser#getItem(String)}.
*
* <p>Return a {@link ListenableFuture} to send a {@link LibraryResult} back to the browser
* asynchronously. You can also return a {@link LibraryResult} directly by using Guava's
* {@link Futures#immediateFuture(Object)}.
*
* <p>To allow getting the item, return a {@link LibraryResult} with {@link
* LibraryResult#RESULT_SUCCESS} and a {@link MediaItem} with a valid {@link
* MediaItem#mediaId}.
*
* @param session The session for this event.
* @param browser The browser information.
* @param mediaId The non-empty media id of the requested item.
* @return A pending result that will be resolved with a media item.
* @see SessionCommand#COMMAND_CODE_LIBRARY_GET_ITEM
*/
default ListenableFuture<LibraryResult<MediaItem>> onGetItem(
MediaLibrarySession session, ControllerInfo browser, String mediaId) {
return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_NOT_SUPPORTED));
}
/**
* Called when a {@link MediaBrowser} requests the child {@link MediaItem media items} of the
* given parent id by {@link MediaBrowser#getChildren(String, int, int, LibraryParams)}.
*
* <p>Return a {@link ListenableFuture} to send a {@link LibraryResult} back to the browser
* asynchronously. You can also return a {@link LibraryResult} directly by using Guava's
* {@link Futures#immediateFuture(Object)}.
*
* <p>The {@link LibraryResult#params} should be the same as the given {@link LibraryParams
* params}.
*
* <p>To allow getting the children, return a {@link LibraryResult} with {@link
* LibraryResult#RESULT_SUCCESS} and a list of {@link MediaItem media items}. Return an empty
* list for no children rather than using error codes.
*
* @param session The session for this event
* @param browser The browser information.
* @param parentId The non-empty parent id.
* @param page The page number to get the paginated result starting from {@code 0}.
* @param pageSize The page size to get the paginated result. Will be greater than {@code 0}.
* @param params The optional parameters passed by the browser.
* @return A pending result that will be resolved with a list of media items.
* @see SessionCommand#COMMAND_CODE_LIBRARY_GET_CHILDREN
*/
default ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> onGetChildren(
MediaLibrarySession session,
ControllerInfo browser,
String parentId,
@IntRange(from = 0) int page,
@IntRange(from = 1) int pageSize,
@Nullable LibraryParams params) {
return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_NOT_SUPPORTED));
}
/**
* Called when a {@link MediaBrowser} subscribes to the given parent id by {@link
* MediaBrowser#subscribe(String, LibraryParams)}.
*
* <p>Return a {@link ListenableFuture} to send a {@link LibraryResult} back to the browser
* asynchronously. You can also return a {@link LibraryResult} directly by using Guava's
* {@link Futures#immediateFuture(Object)}.
*
* <p>The {@link LibraryResult#params} should be the same as the given {@link LibraryParams
* params}.
*
* <p>It's your responsibility to keep subscriptions and call {@link
* MediaLibrarySession#notifyChildrenChanged(ControllerInfo, String, int, LibraryParams)} when
* the children of the parent are changed until it's {@link #onUnsubscribe unsubscribed}.
*
* <p>Interoperability: This will be called by {@link
* android.support.v4.media.MediaBrowserCompat#subscribe}, but won't be called by {@link
* android.media.browse.MediaBrowser#subscribe}.
*
* @param session The session for this event.
* @param browser The browser information.
* @param parentId The non-empty parent id.
* @param params The optional parameters passed by the browser.
* @return A pending result that will be resolved with a result code.
* @see SessionCommand#COMMAND_CODE_LIBRARY_SUBSCRIBE
*/
default ListenableFuture<LibraryResult<Void>> onSubscribe(
MediaLibrarySession session,
ControllerInfo browser,
String parentId,
@Nullable LibraryParams params) {
return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_NOT_SUPPORTED));
}
/**
* Called when a {@link MediaBrowser} unsubscribes from the given parent id by {@link
* MediaBrowser#unsubscribe(String)}.
*
* <p>Return a {@link ListenableFuture} to send a {@link LibraryResult} back to the browser
* asynchronously. You can also return a {@link LibraryResult} directly by using Guava's
* {@link Futures#immediateFuture(Object)}.
*
* <p>Interoperability: This will be called by {@link
* android.support.v4.media.MediaBrowserCompat#unsubscribe}, but won't be called by {@link
* android.media.browse.MediaBrowser#unsubscribe}.
*
* @param session The session for this event.
* @param browser The browser information.
* @param parentId The non-empty parent id.
* @return A pending result that will be resolved with a result code.
* @see SessionCommand#COMMAND_CODE_LIBRARY_UNSUBSCRIBE
*/
default ListenableFuture<LibraryResult<Void>> onUnsubscribe(
MediaLibrarySession session, ControllerInfo browser, String parentId) {
return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_NOT_SUPPORTED));
}
/**
* Called when a {@link MediaBrowser} requests a search with {@link
* MediaBrowser#search(String, LibraryParams)}.
*
* <p>Return a {@link ListenableFuture} to send a {@link LibraryResult} back to the browser
* asynchronously. You can also return a {@link LibraryResult} directly by using Guava's
* {@link Futures#immediateFuture(Object)}.
*
* <p>The {@link LibraryResult#params} should be the same as the given {@link LibraryParams
* params}.
*
* <p>Return {@link LibraryResult} with a result code for the search and notify the number of
* search result ({@link MediaItem media items}) through {@link
* #notifySearchResultChanged(ControllerInfo, String, int, LibraryParams)}. {@link
* MediaBrowser} will ask the search result afterwards through {@link
* #onGetSearchResult(MediaLibrarySession, ControllerInfo, String, int, int, LibraryParams)}.
*
* @param session The session for this event.
* @param browser The browser information.
* @param query The non-empty search query.
* @param params The optional parameters passed by the browser.
* @return A pending result that will be resolved with a result code.
* @see SessionCommand#COMMAND_CODE_LIBRARY_SEARCH
*/
default ListenableFuture<LibraryResult<Void>> onSearch(
MediaLibrarySession session,
ControllerInfo browser,
String query,
@Nullable LibraryParams params) {
return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_NOT_SUPPORTED));
}
/**
* Called when a {@link MediaBrowser} requests a search result with {@link
* MediaBrowser#getSearchResult(String, int, int, LibraryParams)}.
*
* <p>Return a {@link ListenableFuture} to send a {@link LibraryResult} back to the browser
* asynchronously. You can also return a {@link LibraryResult} directly by using Guava's
* {@link Futures#immediateFuture(Object)}.
*
* <p>The {@link LibraryResult#params} should be the same as the given {@link LibraryParams
* params}.
*
* <p>To allow getting the search result, return a {@link LibraryResult} with {@link
* LibraryResult#RESULT_SUCCESS} and a list of {@link MediaItem media items}. Return an empty
* list for no children rather than using error codes.
*
* <p>Typically, the {@code query} is requested through {@link #onSearch(MediaLibrarySession,
* ControllerInfo, String, LibraryParams)} before, but it may not especially when {@link
* android.support.v4.media.MediaBrowserCompat#search} is used.
*
* @param session The session for this event.
* @param browser The browser information.
* @param query The non-empty search query.
* @param page The page number to get the paginated result starting from {@code 0}.
* @param pageSize The page size to get the paginated result. Will be greater than {@code 0}.
* @param params The optional parameters passed by the browser.
* @return A pending result that will be resolved with a list of media items.
* @see SessionCommand#COMMAND_CODE_LIBRARY_GET_SEARCH_RESULT
*/
default ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> onGetSearchResult(
MediaLibrarySession session,
ControllerInfo browser,
String query,
@IntRange(from = 0) int page,
@IntRange(from = 1) int pageSize,
@Nullable LibraryParams params) {
return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_NOT_SUPPORTED));
}
}
/**
* A builder for {@link MediaLibrarySession}.
*
* <p>Any incoming requests from the {@link MediaBrowser} will be handled on the application
* thread of the underlying {@link Player}.
*/
// Note: Don't override #setSessionCallback() because the callback can be set by the
// constructor.
public static final class Builder extends BuilderBase<MediaLibrarySession, Builder, Callback> {
/**
* Creates a builder for {@link MediaLibrarySession}.
*
* @param service The {@link MediaLibraryService} that instantiates the {@link
* MediaLibrarySession}.
* @param player The underlying player to perform playback and handle transport controls.
* @param callback The callback to handle requests from {@link MediaBrowser}.
* @throws IllegalArgumentException if {@link Player#canAdvertiseSession()} returns false.
*/
// Builder requires MediaLibraryService instead of Context just to ensure that the
// builder can be only instantiated within the MediaLibraryService.
// Ideally it's better to make it inner class of service to enforce, but it violates API
// guideline that Builders should be the inner class of the building target.
public Builder(MediaLibraryService service, Player player, Callback callback) {
super(service, player, callback);
}
/**
* Sets a {@link PendingIntent} to launch an {@link android.app.Activity} for the {@link
* MediaLibrarySession}. This can be used as a quick link to an ongoing media screen.
*
* @param pendingIntent The pending intent.
* @return The builder to allow chaining.
*/
@Override
public Builder setSessionActivity(PendingIntent pendingIntent) {
return super.setSessionActivity(pendingIntent);
}
/**
* Sets an ID of the {@link MediaLibrarySession}. If not set, an empty string will be used.
*
* <p>Use this if and only if your app supports multiple playback at the same time and also
* wants to provide external apps to have finer-grained controls.
*
* @param id The ID. Must be unique among all {@link MediaSession sessions} per package.
* @return The builder to allow chaining.
*/
@Override
public Builder setId(String id) {
return super.setId(id);
}
/**
* Sets an extra {@link Bundle} for the {@link MediaLibrarySession}. The {@link
* MediaLibrarySession#getToken()} session token} will have the {@link
* SessionToken#getExtras() extras}. If not set, an empty {@link Bundle} will be used.
*
* @param extras The extra {@link Bundle}.
* @return The builder to allow chaining.
*/
@Override
public Builder setExtras(Bundle extras) {
return super.setExtras(extras);
}
/**
* Sets a {@link BitmapLoader} for the {@link MediaLibrarySession} to decode bitmaps from
* compressed binary data or load bitmaps from {@link Uri}. If not set, a {@link
* CacheBitmapLoader} with a {@link SimpleBitmapLoader} inside will be used.
*
* <p>The provided instance will likely be called repeatedly with the same request, so it
* would be best if any provided instance does some caching. Simple caching can be added to
* any {@link BitmapLoader} implementation by wrapping it in {@link CacheBitmapLoader} before
* passing it to this method.
*
* <p>If no instance is set, a {@link CacheBitmapLoader} with a {@link SimpleBitmapLoader}
* inside will be used.
*
* @param bitmapLoader The bitmap loader {@link BitmapLoader}.
* @return The builder to allow chaining.
*/
@UnstableApi
@Override
public Builder setBitmapLoader(BitmapLoader bitmapLoader) {
return super.setBitmapLoader(bitmapLoader);
}
/**
* Builds a {@link MediaLibrarySession}.
*
* @return A new session.
* @throws IllegalStateException if a {@link MediaSession} with the same {@link #setId(String)
* ID} already exists in the package.
*/
@Override
public MediaLibrarySession build() {
if (bitmapLoader == null) {
bitmapLoader = new CacheBitmapLoader(new SimpleBitmapLoader());
}
return new MediaLibrarySession(
context, id, player, sessionActivity, callback, extras, checkNotNull(bitmapLoader));
}
}
/* package */ MediaLibrarySession(
Context context,
String id,
Player player,
@Nullable PendingIntent sessionActivity,
MediaSession.Callback callback,
Bundle tokenExtras,
BitmapLoader bitmapLoader) {
super(context, id, player, sessionActivity, callback, tokenExtras, bitmapLoader);
}
@Override
/* package */ MediaLibrarySessionImpl createImpl(
Context context,
String id,
Player player,
@Nullable PendingIntent sessionActivity,
MediaSession.Callback callback,
Bundle tokenExtras,
BitmapLoader bitmapLoader) {
return new MediaLibrarySessionImpl(
this,
context,
id,
player,
sessionActivity,
(Callback) callback,
tokenExtras,
bitmapLoader);
}
@Override
/* package */ MediaLibrarySessionImpl getImpl() {
return (MediaLibrarySessionImpl) super.getImpl();
}
/**
* Notifies a browser that is {@link Callback#onSubscribe subscribing} to the change to a
* parent's children. If the browser isn't subscribing to the parent, nothing will happen.
*
* <p>This only tells the number of child {@link MediaItem media items}. {@link
* Callback#onGetChildren} will be called by the browser afterwards to get the list of {@link
* MediaItem media items}.
*
* @param browser The browser to notify.
* @param parentId The non-empty id of the parent with changes to its children.
* @param itemCount The number of children.
* @param params The parameters given by {@link Callback#onSubscribe}.
*/
public void notifyChildrenChanged(
ControllerInfo browser,
String parentId,
@IntRange(from = 0) int itemCount,
@Nullable LibraryParams params) {
checkArgument(itemCount >= 0);
getImpl()
.notifyChildrenChanged(checkNotNull(browser), checkNotEmpty(parentId), itemCount, params);
}
/**
* Notifies all browsers that are {@link Callback#onSubscribe subscribing} to the parent of the
* change to its children regardless of the {@link LibraryParams params} given by {@link
* Callback#onSubscribe}.
*
* @param parentId The non-empty id of the parent with changes to its children.
* @param itemCount The number of children.
* @param params The optional parameters.
*/
// This is for the backward compatibility.
public void notifyChildrenChanged(
String parentId, @IntRange(from = 0) int itemCount, @Nullable LibraryParams params) {
checkArgument(itemCount >= 0);
getImpl().notifyChildrenChanged(checkNotEmpty(parentId), itemCount, params);
}
/**
* Notifies a browser of a change to the {@link Callback#onSearch search} result.
*
* @param browser The browser to notify.
* @param query The non-empty search query given by {@link Callback#onSearch}.
* @param itemCount The number of items that have been found in the search.
* @param params The parameters given by {@link Callback#onSearch}.
*/
public void notifySearchResultChanged(
ControllerInfo browser,
String query,
@IntRange(from = 0) int itemCount,
@Nullable LibraryParams params) {
checkArgument(itemCount >= 0);
getImpl()
.notifySearchResultChanged(
checkNotNull(browser), checkNotEmpty(query), itemCount, params);
}
}
/**
* Parameters for the interaction between {@link MediaBrowser} and {@link MediaLibrarySession}.
*
* <p>When a {@link MediaBrowser} specifies the parameters, the {@link MediaLibrarySession} is
* recommended to do the best effort to provide a result regarding the parameters, but it's not an
* error even though {@link MediaLibrarySession} doesn't return the parameters since they are
* optional.
*/
public static final class LibraryParams implements Bundleable {
/**
* An extra {@link Bundle} for the private contract between {@link MediaBrowser} and {@link
* MediaLibrarySession}.
*/
@UnstableApi public final Bundle extras;
/**
* Whether the media items are recently played.
*
* <p>When a {@link MediaBrowser} specifies it as {@code true}, the {@link MediaLibrarySession}
* is recommended to provide the recently played media items. If so, the implementation must
* return the parameter with {@code true} as well. The list of media items is sorted by
* relevance, the first being the most recent.
*
* <p>When a {@link MediaBrowser} specifies it as {@code false}, the {@link MediaLibrarySession}
* doesn't have to care about the recentness of media items.
*/
public final boolean isRecent;
/**
* Whether the media items can be played without an internet connection.
*
* <p>When a {@link MediaBrowser} specifies it as {@code true}, the {@link MediaLibrarySession}
* is recommended to provide offline media items. If so, the implementation must return the
* parameter with {@code true} as well.
*
* <p>When a {@link MediaBrowser} specifies it as {@code false}, the {@link MediaLibrarySession}
* doesn't have to care about the offline playability of media items.
*/
public final boolean isOffline;
/**
* Whether the media items are suggested.
*
* <p>When a {@link MediaBrowser} specifies it as {@code true}, the {@link MediaLibrarySession}
* is recommended to provide suggested media items. If so, the implementation must return the
* parameter with {@code true} as well. The list of media items is sorted by relevance, the
* first being the top suggestion.
*
* <p>When a {@link MediaBrowser} specifies it as {@code false}, the {@link MediaLibrarySession}
* doesn't have to care about the suggestion for media items.
*/
public final boolean isSuggested;
private LibraryParams(Bundle extras, boolean recent, boolean offline, boolean suggested) {
this.extras = new Bundle(extras);
this.isRecent = recent;
this.isOffline = offline;
this.isSuggested = suggested;
}
/** A builder for {@link LibraryParams}. */
public static final class Builder {
private boolean recent;
private boolean offline;
private boolean suggested;
private Bundle extras;
public Builder() {
extras = Bundle.EMPTY;
}
/** Sets whether the media items are recently played. */
@CanIgnoreReturnValue
public Builder setRecent(boolean recent) {
this.recent = recent;
return this;
}
/** Sets whether the media items can be played without an internet connection. */
@CanIgnoreReturnValue
public Builder setOffline(boolean offline) {
this.offline = offline;
return this;
}
/** Sets whether the media items are suggested. */
@CanIgnoreReturnValue
public Builder setSuggested(boolean suggested) {
this.suggested = suggested;
return this;
}
/** Set an extra {@link Bundle}. */
@CanIgnoreReturnValue
@UnstableApi
public Builder setExtras(Bundle extras) {
this.extras = checkNotNull(extras);
return this;
}
/** Builds {@link LibraryParams}. */
public LibraryParams build() {
return new LibraryParams(extras, recent, offline, suggested);
}
}
// Bundleable implementation.
private static final String FIELD_EXTRAS = Util.intToStringMaxRadix(0);
private static final String FIELD_RECENT = Util.intToStringMaxRadix(1);
private static final String FIELD_OFFLINE = Util.intToStringMaxRadix(2);
private static final String FIELD_SUGGESTED = Util.intToStringMaxRadix(3);
@UnstableApi
@Override
public Bundle toBundle() {
Bundle bundle = new Bundle();
bundle.putBundle(FIELD_EXTRAS, extras);
bundle.putBoolean(FIELD_RECENT, isRecent);
bundle.putBoolean(FIELD_OFFLINE, isOffline);
bundle.putBoolean(FIELD_SUGGESTED, isSuggested);
return bundle;
}
/** Object that can restore {@link LibraryParams} from a {@link Bundle}. */
@UnstableApi public static final Creator<LibraryParams> CREATOR = LibraryParams::fromBundle;
private static LibraryParams fromBundle(Bundle bundle) {
@Nullable Bundle extras = bundle.getBundle(FIELD_EXTRAS);
boolean recent = bundle.getBoolean(FIELD_RECENT, /* defaultValue= */ false);
boolean offline = bundle.getBoolean(FIELD_OFFLINE, /* defaultValue= */ false);
boolean suggested = bundle.getBoolean(FIELD_SUGGESTED, /* defaultValue= */ false);
return new LibraryParams(extras == null ? Bundle.EMPTY : extras, recent, offline, suggested);
}
}
@Override
@Nullable
public IBinder onBind(@Nullable Intent intent) {
if (intent == null) {
return null;
}
if (MediaLibraryService.SERVICE_INTERFACE.equals(intent.getAction())) {
return getServiceBinder();
}
return super.onBind(intent);
}
/**
* {@inheritDoc}
*
* <p>It must return a {@link MediaLibrarySession} which is a subclass of {@link MediaSession}.
*/
@Override
@Nullable
public abstract MediaLibrarySession onGetSession(ControllerInfo controllerInfo);
}