blob: 9e5b8041ba0e7a245d1da02d2c06831be09dcd66 [file] [log] [blame]
/*
* 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.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}.
* <p>
* 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}.
* <p>
* When extending this class, also add the following to your {@code AndroidManifest.xml}.
* <pre>
* &lt;service android:name="component_name_of_your_implementation" &gt;
* &lt;intent-filter&gt;
* &lt;action android:name="androidx.media2.session.MediaLibraryService" /&gt;
* &lt;/intent-filter&gt;
* &lt;/service&gt;</pre>
* <p>
* You may also declare <pre>android.media.browse.MediaBrowserService</pre> for 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)}.
*/
public static final class MediaLibrarySession extends MediaSession {
/**
* Callback for the {@link MediaLibrarySession}.
* <p>
* 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}.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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}.
* <p>
* 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<MediaLibrarySession,
Builder, MediaLibrarySessionCallback> {
// 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(@NonNull MediaLibraryService service,
@NonNull SessionPlayer player,
@NonNull Executor callbackExecutor,
@NonNull MediaLibrarySessionCallback callback) {
super(service, player);
setSessionCallback(callbackExecutor, callback);
}
@Override
@NonNull
public Builder setSessionActivity(@Nullable PendingIntent pi) {
return super.setSessionActivity(pi);
}
@Override
@NonNull
public Builder setId(@NonNull String id) {
return super.setId(id);
}
@Override
@NonNull
public Builder setExtras(@NonNull Bundle extras) {
return super.setExtras(extras);
}
@Override
@NonNull
public MediaLibrarySession build() {
if (mCallbackExecutor == null) {
mCallbackExecutor = ContextCompat.getMainExecutor(mContext);
}
if (mCallback == null) {
mCallback = new MediaLibrarySession.MediaLibrarySessionCallback() {};
}
return new MediaLibrarySession(mContext, mId, mPlayer, mSessionActivity,
mCallbackExecutor, mCallback, mExtras);
}
}
MediaLibrarySession(Context context, String id, SessionPlayer player,
PendingIntent sessionActivity, Executor callbackExecutor,
MediaSession.SessionCallback callback, Bundle tokenExtras) {
super(context, id, player, sessionActivity, callbackExecutor, callback, tokenExtras);
}
@Override
MediaLibrarySessionImpl createImpl(Context context, String id, SessionPlayer player,
PendingIntent sessionActivity, Executor callbackExecutor,
MediaSession.SessionCallback callback, Bundle tokenExtras) {
return new MediaLibrarySessionImplBase(this, context, id, player, sessionActivity,
callbackExecutor, callback, tokenExtras);
}
@Override
MediaLibrarySessionImpl getImpl() {
return (MediaLibrarySessionImpl) super.getImpl();
}
/**
* Notifies the controller of the change in a parent's children.
* <p>
* If the controller hasn't subscribed to the parent, the API will do nothing.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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;
// 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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);
}
}
}
}