/* * 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 static java.lang.annotation.ElementType.TYPE_USE; 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; import java.lang.annotation.Target; /** * 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 Callback#onConnect onConnect} * and {@link Callback#onGetLibraryRoot onGetLibraryRoot} * |
* Actual package name via {@link Context#getPackageName()} | *Actual UID | *
* {@code 21 <= SDK_INT < 28} * for other {@link Callback 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 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)}. * *
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: 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 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.
* @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.
* @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 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, extras);
}
}
/* package */ MediaLibrarySession(
Context context,
String id,
Player player,
@Nullable PendingIntent sessionActivity,
MediaSession.Callback callback,
Bundle tokenExtras) {
super(context, id, player, sessionActivity, callback, tokenExtras);
}
@Override
/* package */ MediaLibrarySessionImpl createImpl(
Context context,
String id,
Player player,
@Nullable PendingIntent sessionActivity,
MediaSession.Callback callback,
Bundle tokenExtras) {
return new MediaLibrarySessionImpl(
this, context, id, player, sessionActivity, (Callback) callback, tokenExtras);
}
@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.
*
* 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}.
*
* 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)
@Target(TYPE_USE)
@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);
}