/* * 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.media2.session; import static androidx.media2.session.LibraryResult.RESULT_ERROR_NOT_SUPPORTED; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.media.browse.MediaBrowser; import android.os.Bundle; import android.os.IBinder; import android.text.TextUtils; import androidx.annotation.IntRange; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; import androidx.media.MediaSessionManager.RemoteUserInfo; import androidx.media2.common.MediaMetadata; import androidx.media2.common.SessionPlayer; import androidx.media2.session.LibraryResult.ResultCode; import androidx.media2.session.MediaSession.ControllerInfo; import androidx.versionedparcelable.ParcelField; import androidx.versionedparcelable.VersionedParcelable; import androidx.versionedparcelable.VersionedParcelize; import java.util.concurrent.Executor; /** * Base class for media library services, which is the service containing * {@link MediaLibrarySession}. *
* Media library services enable applications to browse media content provided by an application * and ask the application to start playing it. They may also be used to control content that * is already playing by way of a {@link MediaSession}. *
* When extending this class, also add the following to your {@code AndroidManifest.xml}. *
* <service android:name="component_name_of_your_implementation" > * <intent-filter> * <action android:name="androidx.media2.session.MediaLibraryService" /> * </intent-filter> * </service>*
* You may also declare
android.media.browse.MediaBrowserServicefor compatibility with * {@link android.support.v4.media.MediaBrowserCompat}. This service can handle it automatically. * * @see MediaSessionService */ public abstract class MediaLibraryService extends MediaSessionService { /** * The {@link Intent} that must be declared as handled by the service. */ public static final String SERVICE_INTERFACE = "androidx.media2.session.MediaLibraryService"; /** * Session for the {@link MediaLibraryService}. Build this object with * {@link Builder} and return in {@link MediaSessionService#onGetSession(ControllerInfo)}. * *
SDK version | *{@link ControllerInfo#getPackageName()} for legacy browser |
* {@link ControllerInfo#getUid()} for legacy browser |
---|---|---|
{@code SDK_VERSION} < 21 | *Actual package name via {@link Context#getPackageName()} | *Actual UID |
21 ≥ {@code SDK_VERSION} < 28, * {@code MediaLibrarySessionCallback#onConnect} and * {@code MediaLibrarySessionCallback#onGetLibraryRoot} |
* Actual package name via {@link Context#getPackageName()} | *Actual UID |
21 ≥ {@code SDK_VERSION} < 28, for other callbacks | *{@link RemoteUserInfo#LEGACY_CONTROLLER} | *Negative value |
28 ≥ {@code SDK_VERSION} | *Actual package name via {@link Context#getPackageName()} | *Actual UID |
* When you return {@link LibraryResult} with media items, * items must have valid {@link MediaMetadata#METADATA_KEY_MEDIA_ID} and * specify {@link MediaMetadata#METADATA_KEY_BROWSABLE} and * {@link MediaMetadata#METADATA_KEY_PLAYABLE}. */ public static class MediaLibrarySessionCallback extends MediaSession.SessionCallback { /** * Called to get the root information for browsing by a {@link MediaBrowser}. *
* To allow browsing media information, return the {@link LibraryResult} with the * {@link LibraryResult#RESULT_SUCCESS} and the root media item with the valid * {@link MediaMetadata#METADATA_KEY_MEDIA_ID media id}. The media id must be included * for the browser to get the children under it. *
* Interoperability: this callback may be called on the main thread, regardless of the * callback executor. * * @param session the session for this event * @param controller information of the controller requesting access to browse media. * @param params An optional library params of service-specific arguments to send * to the media library service when connecting and retrieving the * root id for browsing, or {@code null} if none. * @return a library result with the root media item with the id. A runtime exception * will be thrown if an invalid result is returned. * @see SessionCommand#COMMAND_CODE_LIBRARY_GET_LIBRARY_ROOT * @see MediaMetadata#METADATA_KEY_MEDIA_ID * @see LibraryParams */ @NonNull public LibraryResult onGetLibraryRoot(@NonNull MediaLibrarySession session, @NonNull ControllerInfo controller, @Nullable LibraryParams params) { return new LibraryResult(RESULT_ERROR_NOT_SUPPORTED); } /** * Called to get an item. *
* To allow getting the item, return the {@link LibraryResult} with the * {@link LibraryResult#RESULT_SUCCESS} and the media item. * * @param session the session for this event * @param controller controller * @param mediaId non-empty media id of the requested item * @return a library result with a media item with the id. A runtime exception * will be thrown if an invalid result is returned. * @see SessionCommand#COMMAND_CODE_LIBRARY_GET_ITEM */ @NonNull public LibraryResult onGetItem(@NonNull MediaLibrarySession session, @NonNull ControllerInfo controller, @NonNull String mediaId) { return new LibraryResult(RESULT_ERROR_NOT_SUPPORTED); } /** * Called to get children of given parent id. Return the children here for the browser. *
* To allow getting the children, return the {@link LibraryResult} with the * {@link LibraryResult#RESULT_SUCCESS} and the list of media item. Return an empty * list for no children rather than using result code for error. * * @param session the session for this event * @param controller controller * @param parentId non-empty parent id to get children * @param page page number. Starts from {@code 0}. * @param pageSize page size. Should be greater or equal to {@code 1}. * @param params library params * @return a library result with a list of media item with the id. A runtime exception * will be thrown if an invalid result is returned. * @see SessionCommand#COMMAND_CODE_LIBRARY_GET_CHILDREN * @see LibraryParams */ @NonNull public LibraryResult onGetChildren(@NonNull MediaLibrarySession session, @NonNull ControllerInfo controller, @NonNull String parentId, @IntRange(from = 0) int page, @IntRange(from = 1) int pageSize, @Nullable LibraryParams params) { return new LibraryResult(RESULT_ERROR_NOT_SUPPORTED); } /** * Called when a controller subscribes to the parent. *
* It's your responsibility to keep subscriptions by your own and call * {@link MediaLibrarySession#notifyChildrenChanged( * ControllerInfo, String, int, LibraryParams)} when the parent is changed until it's * unsubscribed. *
* Interoperability: This will be called when * {@link android.support.v4.media.MediaBrowserCompat#subscribe} is called. * However, this won't be called when {@link MediaBrowser#subscribe} is called. * * @param session the session for this event * @param controller controller * @param parentId non-empty parent id * @param params library params * @return result code * @see SessionCommand#COMMAND_CODE_LIBRARY_SUBSCRIBE * @see LibraryParams */ @ResultCode public int onSubscribe(@NonNull MediaLibrarySession session, @NonNull ControllerInfo controller, @NonNull String parentId, @Nullable LibraryParams params) { return RESULT_ERROR_NOT_SUPPORTED; } /** * Called when a controller unsubscribes to the parent. *
* Interoperability: This wouldn't be called if {@link MediaBrowser#unsubscribe} is * called while works well with * {@link android.support.v4.media.MediaBrowserCompat#unsubscribe}. * * @param session the session for this event * @param controller controller * @param parentId non-empty parent id * @return result code * @see SessionCommand#COMMAND_CODE_LIBRARY_UNSUBSCRIBE */ @ResultCode public int onUnsubscribe(@NonNull MediaLibrarySession session, @NonNull ControllerInfo controller, @NonNull String parentId) { return RESULT_ERROR_NOT_SUPPORTED; } /** * Called when a controller requests search. *
* Return immediately with the result of the attempt to search with the query. Notify * the number of search result through * {@link #notifySearchResultChanged(ControllerInfo, String, int, LibraryParams)}. * {@link MediaBrowser} will ask the search result with the pagination later. * * @param session the session for this event * @param controller controller * @param query The non-empty search query sent from the media browser. * It contains keywords separated by space. * @param params library params * @return result code * @see SessionCommand#COMMAND_CODE_LIBRARY_SEARCH * @see #notifySearchResultChanged(ControllerInfo, String, int, LibraryParams) * @see LibraryParams */ @ResultCode public int onSearch(@NonNull MediaLibrarySession session, @NonNull ControllerInfo controller, @NonNull String query, @Nullable LibraryParams params) { return RESULT_ERROR_NOT_SUPPORTED; } /** * Called to get the search result. *
* To allow getting the search result, return the {@link LibraryResult} with the * {@link LibraryResult#RESULT_SUCCESS} and the list of media item. Return an empty * list for no search result rather than using result code for error. *
* This may be called with a query that hasn't called with {@link #onSearch}, especially * when {@link android.support.v4.media.MediaBrowserCompat#search} is used. * * @param session the session for this event * @param controller controller * @param query The non-empty search query which was previously sent through * {@link #onSearch}. * @param page page number. Starts from {@code 0}. * @param pageSize page size. Should be greater or equal to {@code 1}. * @param params library params * @return a library result with a list of media item with the id. A runtime exception * will be thrown if an invalid result is returned. * @see SessionCommand#COMMAND_CODE_LIBRARY_GET_SEARCH_RESULT * @see LibraryParams */ @NonNull public LibraryResult onGetSearchResult( @NonNull MediaLibrarySession session, @NonNull ControllerInfo controller, @NonNull String query, @IntRange(from = 0) int page, @IntRange(from = 1) int pageSize, @Nullable LibraryParams params) { return new LibraryResult(RESULT_ERROR_NOT_SUPPORTED); } } /** * Builder for {@link MediaLibrarySession}. *
* Any incoming event from the {@link MediaController} will be handled on the callback
* executor. If it's not set, {@link ContextCompat#getMainExecutor(Context)} will be used by
* default.
*/
// Override all methods just to show them with the type instead of generics in Javadoc.
// This workarounds javadoc issue described in the MediaSession.BuilderBase.
// Note: Don't override #setSessionCallback() because the callback can be set by the
// constructor.
public static final class Builder extends MediaSession.BuilderBase
* If the controller hasn't subscribed to the parent, the API will do nothing.
*
* Controllers will use {@link MediaBrowser#getChildren(String, int, int, LibraryParams)}
* to get the list of children.
*
* @param controller controller to notify
* @param parentId non-empty parent id with changes in its children
* @param itemCount number of children.
* @param params library params
*/
public void notifyChildrenChanged(@NonNull ControllerInfo controller,
@NonNull String parentId, @IntRange(from = 0) int itemCount,
@Nullable LibraryParams params) {
if (controller == null) {
throw new NullPointerException("controller shouldn't be null");
}
if (parentId == null) {
throw new NullPointerException("parentId shouldn't be null");
} else if (TextUtils.isEmpty(parentId)) {
throw new IllegalArgumentException("parentId shouldn't be empty");
}
if (itemCount < 0) {
throw new IllegalArgumentException("itemCount shouldn't be negative");
}
getImpl().notifyChildrenChanged(controller, parentId, itemCount, params);
}
/**
* Notifies all controllers that subscribed to the parent about change in the parent's
* children, regardless of the library params supplied by
* {@link MediaBrowser#subscribe(String, LibraryParams)}.
* @param parentId non-empty parent id
* @param itemCount number of children
* @param params library params
*/
// This is for the backward compatibility.
public void notifyChildrenChanged(@NonNull String parentId, int itemCount,
@Nullable LibraryParams params) {
if (TextUtils.isEmpty(parentId)) {
throw new IllegalArgumentException("parentId shouldn't be empty");
}
if (itemCount < 0) {
throw new IllegalArgumentException("itemCount shouldn't be negative");
}
getImpl().notifyChildrenChanged(parentId, itemCount, params);
}
/**
* Notifies controller about change in the search result.
*
* @param controller controller to notify
* @param query previously sent non-empty search query from the controller.
* @param itemCount the number of items that have been found in the search.
* @param params library params
*/
public void notifySearchResultChanged(@NonNull ControllerInfo controller,
@NonNull String query, @IntRange(from = 0) int itemCount,
@Nullable LibraryParams params) {
if (controller == null) {
throw new NullPointerException("controller shouldn't be null");
}
if (query == null) {
throw new NullPointerException("query shouldn't be null");
} else if (TextUtils.isEmpty(query)) {
throw new IllegalArgumentException("query shouldn't be empty");
}
if (itemCount < 0) {
throw new IllegalArgumentException("itemCount shouldn't be negative");
}
getImpl().notifySearchResultChanged(controller, query, itemCount, params);
}
@Override
@NonNull
MediaLibrarySessionCallback getCallback() {
return (MediaLibrarySessionCallback) super.getCallback();
}
interface MediaLibrarySessionImpl extends MediaSessionImpl {
// LibrarySession methods
void notifyChildrenChanged(
@NonNull String parentId, int itemCount, @Nullable LibraryParams params);
void notifyChildrenChanged(@NonNull ControllerInfo controller,
@NonNull String parentId, int itemCount, @Nullable LibraryParams params);
void notifySearchResultChanged(@NonNull ControllerInfo controller,
@NonNull String query, int itemCount, @Nullable LibraryParams params);
// LibrarySession callback implementations called on the executors
LibraryResult onGetLibraryRootOnExecutor(@NonNull ControllerInfo controller,
@Nullable LibraryParams params);
LibraryResult onGetItemOnExecutor(@NonNull ControllerInfo controller,
@NonNull String mediaId);
LibraryResult onGetChildrenOnExecutor(@NonNull ControllerInfo controller,
@NonNull String parentId, int page, int pageSize,
@Nullable LibraryParams params);
int onSubscribeOnExecutor(@NonNull ControllerInfo controller,
@NonNull String parentId, @Nullable LibraryParams params);
int onUnsubscribeOnExecutor(@NonNull ControllerInfo controller,
@NonNull String parentId);
int onSearchOnExecutor(@NonNull ControllerInfo controller, @NonNull String query,
@Nullable LibraryParams params);
LibraryResult onGetSearchResultOnExecutor(@NonNull ControllerInfo controller,
@NonNull String query, int page, int pageSize, @Nullable LibraryParams params);
// Internally used methods - only changing return type
@Override
MediaLibrarySession getInstance();
@Override
MediaLibrarySessionCallback getCallback();
}
}
@Override
MediaSessionServiceImpl createImpl() {
return new MediaLibraryServiceImplBase();
}
@Override
public IBinder onBind(@NonNull Intent intent) {
return super.onBind(intent);
}
@Override
@Nullable
public abstract MediaLibrarySession onGetSession(@NonNull ControllerInfo controllerInfo);
/**
* Contains information that the library service needs to send to the client.
*
* When the browser supplies {@link LibraryParams}, it's optional field when getting the media
* item(s). The library session is recommended to do the best effort to provide such result.
* It's not an error even when the library session didn't return such items.
*
* The library params returned in the library session callback must include the information
* about the returned media item(s).
*/
@VersionedParcelize
public static final class LibraryParams implements VersionedParcelable {
@ParcelField(1)
Bundle mBundle;
// Types are intentionally Integer for future extension of the value with less effort.
@ParcelField(2)
int mRecent;
@ParcelField(3)
int mOffline;
@ParcelField(4)
int mSuggested;
// WARNING: Adding a new ParcelField may break old library users (b/152830728)
// For versioned parcelable.
LibraryParams() {
// no-op
}
@SuppressWarnings("WeakerAccess") /* synthetic access */
LibraryParams(Bundle bundle, boolean recent, boolean offline, boolean suggested) {
// Keeps the booleans in Integer type.
// Types are intentionally Integer for future extension of the value with less effort.
this(bundle,
convertToInteger(recent),
convertToInteger(offline),
convertToInteger(suggested));
}
private LibraryParams(Bundle bundle, int recent, int offline, int suggested) {
mBundle = bundle;
mRecent = recent;
mOffline = offline;
mSuggested = suggested;
}
private static int convertToInteger(boolean a) {
return a ? 1 : 0;
}
private static boolean convertToBoolean(int a) {
return a == 0 ? false : true;
}
/**
* Returns {@code true} for recent media items.
*
* When the browser supplies {@link LibraryParams} with the {@code true}, library
* session is recommended to provide such media items. If so, the library session
* implementation must return the params with the {@code true} as well. The list of
* media items is considered ordered by relevance, first being the top suggestion.
*
* @return {@code true} for recent items. {@code false} otherwise.
*/
public boolean isRecent() {
return convertToBoolean(mRecent);
}
/**
* Returns {@code true} for offline media items, which can be played without an internet
* connection.
*
* When the browser supplies {@link LibraryParams} with the {@code true}, library
* session is recommended to provide such media items. If so, the library session
* implementation must return the params with the {@code true} as well.
*
* @return {@code true} for offline items. {@code false} otherwise.
**/
public boolean isOffline() {
return convertToBoolean(mOffline);
}
/**
* Returns {@code true} for suggested media items.
*
* When the browser supplies {@link LibraryParams} with the {@code true}, library
* session is recommended to provide such media items. If so, the library session
* implementation must return the params with the {@code true} as well. The list of
* media items is considered ordered by relevance, first being the top suggestion.
*
* @return {@code true} for suggested items. {@code false} otherwise
**/
public boolean isSuggested() {
return convertToBoolean(mSuggested);
}
/**
* Gets the extras.
*
* Extras are the private contract between browser and library session.
*/
@Nullable
public Bundle getExtras() {
return mBundle;
}
/**
* Builds a {@link LibraryParams}.
*/
public static final class Builder {
private boolean mRecent;
private boolean mOffline;
private boolean mSuggested;
private Bundle mBundle;
/**
* Sets whether recently played media item.
*
* When the browser supplies the {@link LibraryParams} with the {@code true}, library
* session is recommended to provide such media items. If so, the library session
* implementation must return the params with the {@code true} as well.
*
* @param recent {@code true} for recent items. {@code false} otherwise.
* @return this builder
*/
@NonNull
public Builder setRecent(boolean recent) {
mRecent = recent;
return this;
}
/**
* Sets whether offline media items, which can be played without an internet connection.
*
* When the browser supplies {@link LibraryParams} with the {@code true}, library
* session is recommended to provide such media items. If so, the library session
* implementation must return the params with the {@code true} as well.
*
* @param offline {@code true} for offline items. {@code false} otherwise.
* @return this builder
*/
@NonNull
public Builder setOffline(boolean offline) {
mOffline = offline;
return this;
}
/**
* Sets whether suggested media items.
*
* When the browser supplies {@link LibraryParams} with the {@code true}, library
* session is recommended to provide such media items. If so, the library session
* implementation must return the params with the {@code true} as well. The list of
* media items is considered ordered by relevance, first being the top suggestion.
*
* @param suggested {@code true} for suggested items. {@code false} otherwise
* @return this builder
*/
@NonNull
public Builder setSuggested(boolean suggested) {
mSuggested = suggested;
return this;
}
/**
* Set a bundle of extras, that browser and library session can understand each other.
*
* @param extras The extras or null.
* @return this builder
*/
@NonNull
public Builder setExtras(@Nullable Bundle extras) {
mBundle = extras;
return this;
}
/**
* Builds a {@link LibraryParams}.
*
* @return new LibraryParams
*/
@NonNull
public LibraryParams build() {
return new LibraryParams(mBundle, mRecent, mOffline, mSuggested);
}
}
}
}