/* * 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.os.Bundle; import android.os.IBinder; import androidx.annotation.IntDef; 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.session.MediaSession.ControllerInfo; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** * Superclass to be extended by services hosting {@link MediaLibrarySession media library sessions}. * *
It enables applications to browse media content provided by an application, ask the * application to start playback, and control the playback. * *
To extend this class, declare the intent filter in your {@code AndroidManifest.xml}. * *
{@code ** ** * }* **
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)}. * *
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. * *
SDK version | *{@link ControllerInfo#getPackageName()} for legacy browser |
* {@link ControllerInfo#getUid()} for legacy browser |
---|---|---|
{@code SDK_INT < 21} | *Actual package name via {@link Context#getPackageName()} | *Actual UID | *
* {@code 21 <= SDK_INT < 28} * for {@link MediaLibrarySessionCallback#onConnect onConnect} * and {@link MediaLibrarySessionCallback#onGetLibraryRoot onGetLibraryRoot} * |
* Actual package name via {@link Context#getPackageName()} | *Actual UID | *
* {@code 21 <= SDK_INT < 28} * for other {@link MediaLibrarySessionCallback callbacks} * |
* {@link RemoteUserInfo#LEGACY_CONTROLLER} | *Negative value | *
{@code 28 <= SDK_INT} | *Actual package name via {@link Context#getPackageName()} | *Actual UID | *
When you return {@link LibraryResult} with {@link MediaItem media items}, each item must * have valid {@link MediaItem#mediaId} and specify {@link MediaMetadata#folderType} and {@link * MediaMetadata#isPlayable} in its {@link MediaItem#mediaMetadata}. */ public interface MediaLibrarySessionCallback extends SessionCallback { @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)}. * *
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)}. * *
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}. * *
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. * *
Interoperability: This method may be called on the main thread regardless of the
* application thread if legacy {@link android.support.v4.media.MediaBrowserCompat} requested
* a {@link androidx.media.MediaBrowserServiceCompat.BrowserRoot}. In this case, you must
* return a completed future.
*
* @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.
* @throws AssertionError if {@link LibraryResult#value} is {@code null} while {@link
* LibraryResult#resultCode} is {@link LibraryResult#RESULT_SUCCESS}.
* @see SessionCommand#COMMAND_CODE_LIBRARY_GET_LIBRARY_ROOT
*/
default ListenableFuture 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)}.
*
* 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 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)}.
*
* The {@link LibraryResult#params} should be the same as the given {@link LibraryParams
* params}.
*
* 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.
* @throws AssertionError if {@link LibraryResult#value} is {@code null} or has size greater
* than the {@code pageSize} while {@link LibraryResult#resultCode} is {@link
* LibraryResult#RESULT_SUCCESS}.
* @see SessionCommand#COMMAND_CODE_LIBRARY_GET_CHILDREN
*/
default ListenableFuture 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)}.
*
* The {@link LibraryResult#params} should be the same as the given {@link LibraryParams
* params}.
*
* 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}.
*
* 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 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)}.
*
* 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 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)}.
*
* The {@link LibraryResult#params} should be the same as the given {@link LibraryParams
* params}.
*
* 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 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)}.
*
* The {@link LibraryResult#params} should be the same as the given {@link LibraryParams
* params}.
*
* 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.
*
* 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.
* @throws AssertionError if {@link LibraryResult#value} is {@code null} or has size greater
* than the {@code pageSize} while {@link LibraryResult#resultCode} is {@link
* LibraryResult#RESULT_SUCCESS}.
* @see SessionCommand#COMMAND_CODE_LIBRARY_GET_SEARCH_RESULT
*/
default ListenableFuture 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 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 the logic used to fill in the fields of a {@link MediaItem}.
*
* @param mediaItemFiller The filler.
* @return The builder to allow chaining.
*/
@Override
public Builder setMediaItemFiller(MediaItemFiller mediaItemFiller) {
return super.setMediaItemFiller(mediaItemFiller);
}
/**
* 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);
}
/**
* 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() {
return new MediaLibrarySession(
context, id, player, sessionActivity, callback, mediaItemFiller, extras);
}
}
/* package */ MediaLibrarySession(
Context context,
String id,
Player player,
@Nullable PendingIntent sessionActivity,
SessionCallback callback,
MediaItemFiller mediaItemFiller,
Bundle tokenExtras) {
super(context, id, player, sessionActivity, callback, mediaItemFiller, tokenExtras);
}
@Override
/* package */ MediaLibrarySessionImpl createImpl(
Context context,
String id,
Player player,
@Nullable PendingIntent sessionActivity,
SessionCallback callback,
MediaItemFiller mediaItemFiller,
Bundle tokenExtras) {
return new MediaLibrarySessionImplBase(
this, context, id, player, sessionActivity, callback, mediaItemFiller, tokenExtras);
}
@Override
/* package */ MediaLibrarySessionImpl getImpl() {
return (MediaLibrarySessionImpl) super.getImpl();
}
/**
* Notifies a browser that is {@link MediaLibrarySessionCallback#onSubscribe subscribing} to the
* change to a parent's children. If the browser isn't subscribing to the parent, nothing will
* happen.
*
* This only tells the number of child {@link MediaItem media items}. {@link
* MediaLibrarySessionCallback#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 MediaLibrarySessionCallback#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 MediaLibrarySessionCallback#onSubscribe subscribing} to
* the parent of the change to its children regardless of the {@link LibraryParams params} given
* by {@link MediaLibrarySessionCallback#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 MediaLibrarySessionCallback#onSearch search}
* result.
*
* @param browser The browser to notify.
* @param query The non-empty search query given by {@link
* MediaLibrarySessionCallback#onSearch}.
* @param itemCount The number of items that have been found in the search.
* @param params The parameters given by {@link MediaLibrarySessionCallback#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);
}
interface MediaLibrarySessionImpl extends MediaSessionImpl {
// LibrarySession methods
void notifyChildrenChanged(String parentId, int itemCount, @Nullable LibraryParams params);
void notifyChildrenChanged(
ControllerInfo browser, String parentId, int itemCount, @Nullable LibraryParams params);
void notifySearchResultChanged(
ControllerInfo browser, String query, int itemCount, @Nullable LibraryParams params);
// LibrarySession callback implementations called on the application thread
ListenableFuture 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.
*
* 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.
*
* 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.
*
* 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.
*
* 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.
*
* 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.
*
* 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. */
public Builder setRecent(boolean recent) {
this.recent = recent;
return this;
}
/** Sets whether the media items can be played without an internet connection. */
public Builder setOffline(boolean offline) {
this.offline = offline;
return this;
}
/** Sets whether the media items are suggested. */
public Builder setSuggested(boolean suggested) {
this.suggested = suggested;
return this;
}
/** Set an extra {@link Bundle}. */
@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.
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({
FIELD_EXTRAS,
FIELD_RECENT,
FIELD_OFFLINE,
FIELD_SUGGESTED,
})
private @interface FieldNumber {}
private static final int FIELD_EXTRAS = 0;
private static final int FIELD_RECENT = 1;
private static final int FIELD_OFFLINE = 2;
private static final int FIELD_SUGGESTED = 3;
@UnstableApi
@Override
public Bundle toBundle() {
Bundle bundle = new Bundle();
bundle.putBundle(keyForField(FIELD_EXTRAS), extras);
bundle.putBoolean(keyForField(FIELD_RECENT), isRecent);
bundle.putBoolean(keyForField(FIELD_OFFLINE), isOffline);
bundle.putBoolean(keyForField(FIELD_SUGGESTED), isSuggested);
return bundle;
}
/** Object that can restore {@link LibraryParams} from a {@link Bundle}. */
@UnstableApi public static final Creator It must return a {@link MediaLibrarySession} which is a subclass of {@link MediaSession}.
*/
@Override
@Nullable
public abstract MediaLibrarySession onGetSession(ControllerInfo controllerInfo);
}