| /* |
| * Copyright 2018 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.media; |
| |
| import static android.support.v4.media.MediaBrowserCompat.EXTRA_PAGE; |
| import static android.support.v4.media.MediaBrowserCompat.EXTRA_PAGE_SIZE; |
| |
| import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP; |
| |
| import android.app.PendingIntent; |
| import android.content.Intent; |
| import android.os.Bundle; |
| import android.os.IBinder; |
| import android.support.v4.media.MediaBrowserCompat.MediaItem; |
| |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| import androidx.annotation.RestrictTo; |
| import androidx.media.MediaLibraryService2.MediaLibrarySession.Builder; |
| import androidx.media.MediaLibraryService2.MediaLibrarySession.MediaLibrarySessionCallback; |
| import androidx.media.MediaSession2.ControllerInfo; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.concurrent.CountDownLatch; |
| import java.util.concurrent.Executor; |
| |
| /** |
| * @hide |
| * Base class for media library services. |
| * <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 MediaSession2}. |
| * <p> |
| * When extending this class, also add the following to your {@code AndroidManifest.xml}. |
| * <pre> |
| * <service android:name="component_name_of_your_implementation" > |
| * <intent-filter> |
| * <action android:name="android.media.MediaLibraryService2" /> |
| * </intent-filter> |
| * </service></pre> |
| * <p> |
| * The {@link MediaLibraryService2} class derives from {@link MediaSessionService2}. IDs shouldn't |
| * be shared between the {@link MediaSessionService2} and {@link MediaSession2}. By |
| * default, an empty string will be used for ID of the service. If you want to specify an ID, |
| * declare metadata in the manifest as follows. |
| * |
| * @see MediaSessionService2 |
| */ |
| @RestrictTo(LIBRARY_GROUP) |
| public abstract class MediaLibraryService2 extends MediaSessionService2 { |
| /** |
| * This is the interface name that a service implementing a session service should say that it |
| * support -- that is, this is the action it uses for its intent filter. |
| */ |
| public static final String SERVICE_INTERFACE = "android.media.MediaLibraryService2"; |
| |
| // TODO: Revisit this value. |
| |
| /** |
| * Session for the {@link MediaLibraryService2}. Build this object with |
| * {@link Builder} and return in {@link #onCreateSession(String)}. |
| */ |
| public static final class MediaLibrarySession extends MediaSession2 { |
| /** |
| * Callback for the {@link MediaLibrarySession}. |
| */ |
| public static class MediaLibrarySessionCallback extends MediaSession2.SessionCallback { |
| /** |
| * Called to get the root information for browsing by a particular client. |
| * <p> |
| * The implementation should verify that the client package has permission |
| * to access browse media information before returning the root id; it |
| * should return null if the client is not allowed to access this |
| * information. |
| * <p> |
| * Note: this callback may be called on the main thread, regardless of the callback |
| * executor. |
| * |
| * @param session the session for this event |
| * @param controllerInfo information of the controller requesting access to browse |
| * media. |
| * @param extras An optional bundle of service-specific arguments to send |
| * to the media library service when connecting and retrieving the |
| * root id for browsing, or null if none. The contents of this |
| * bundle may affect the information returned when browsing. |
| * @return The {@link LibraryRoot} for accessing this app's content or null. |
| * @see LibraryRoot#EXTRA_RECENT |
| * @see LibraryRoot#EXTRA_OFFLINE |
| * @see LibraryRoot#EXTRA_SUGGESTED |
| * @see SessionCommand2#COMMAND_CODE_LIBRARY_GET_LIBRARY_ROOT |
| */ |
| public @Nullable LibraryRoot onGetLibraryRoot(@NonNull MediaLibrarySession session, |
| @NonNull ControllerInfo controllerInfo, @Nullable Bundle extras) { |
| return null; |
| } |
| |
| /** |
| * Called to get an item. Return result here for the browser. |
| * <p> |
| * Return {@code null} for no result or error. |
| * |
| * @param session the session for this event |
| * @param mediaId item id to get media item. |
| * @return a media item. {@code null} for no result or error. |
| * @see SessionCommand2#COMMAND_CODE_LIBRARY_GET_ITEM |
| */ |
| public @Nullable MediaItem2 onGetItem(@NonNull MediaLibrarySession session, |
| @NonNull ControllerInfo controllerInfo, @NonNull String mediaId) { |
| return null; |
| } |
| |
| /** |
| * Called to get children of given parent id. Return the children here for the browser. |
| * <p> |
| * Return an empty list for no children, and return {@code null} for the error. |
| * |
| * @param session the session for this event |
| * @param parentId parent id to get children |
| * @param page number of page |
| * @param pageSize size of the page |
| * @param extras extra bundle |
| * @return list of children. Can be {@code null}. |
| * @see SessionCommand2#COMMAND_CODE_LIBRARY_GET_CHILDREN |
| */ |
| public @Nullable List<MediaItem2> onGetChildren(@NonNull MediaLibrarySession session, |
| @NonNull ControllerInfo controller, @NonNull String parentId, int page, |
| int pageSize, @Nullable Bundle extras) { |
| return null; |
| } |
| |
| /** |
| * 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, Bundle)} |
| * when the parent is changed. |
| * |
| * @param session the session for this event |
| * @param controller controller |
| * @param parentId parent id |
| * @param extras extra bundle |
| * @see SessionCommand2#COMMAND_CODE_LIBRARY_SUBSCRIBE |
| */ |
| public void onSubscribe(@NonNull MediaLibrarySession session, |
| @NonNull ControllerInfo controller, @NonNull String parentId, |
| @Nullable Bundle extras) { |
| } |
| |
| /** |
| * Called when a controller unsubscribes to the parent. |
| * |
| * @param session the session for this event |
| * @param controller controller |
| * @param parentId parent id |
| * @see SessionCommand2#COMMAND_CODE_LIBRARY_UNSUBSCRIBE |
| */ |
| // TODO: Make this to be called. |
| public void onUnsubscribe(@NonNull MediaLibrarySession session, |
| @NonNull ControllerInfo controller, @NonNull String parentId) { |
| } |
| |
| /** |
| * Called when a controller requests search. |
| * |
| * @param session the session for this event |
| * @param query The search query sent from the media browser. It contains keywords |
| * separated by space. |
| * @param extras The bundle of service-specific arguments sent from the media browser. |
| * @see SessionCommand2#COMMAND_CODE_LIBRARY_SEARCH |
| */ |
| public void onSearch(@NonNull MediaLibrarySession session, |
| @NonNull ControllerInfo controllerInfo, @NonNull String query, |
| @Nullable Bundle extras) { |
| } |
| |
| /** |
| * Called to get the search result. Return search result here for the browser which has |
| * requested search previously. |
| * <p> |
| * Return an empty list for no search result, and return {@code null} for the error. |
| * |
| * @param session the session for this event |
| * @param controllerInfo Information of the controller requesting the search result. |
| * @param query The search query which was previously sent through |
| * {@link #onSearch(MediaLibrarySession, ControllerInfo, String, Bundle)}. |
| * @param page page number. Starts from {@code 1}. |
| * @param pageSize page size. Should be greater or equal to {@code 1}. |
| * @param extras The bundle of service-specific arguments sent from the media browser. |
| * @return search result. {@code null} for error. |
| * @see SessionCommand2#COMMAND_CODE_LIBRARY_GET_SEARCH_RESULT |
| */ |
| public @Nullable List<MediaItem2> onGetSearchResult( |
| @NonNull MediaLibrarySession session, @NonNull ControllerInfo controllerInfo, |
| @NonNull String query, int page, int pageSize, @Nullable Bundle extras) { |
| return null; |
| } |
| } |
| |
| /** |
| * Builder for {@link MediaLibrarySession}. |
| */ |
| // Override all methods just to show them with the type instead of generics in Javadoc. |
| // This workarounds javadoc issue described in the MediaSession2.BuilderBase. |
| public static final class Builder extends MediaSession2.BuilderBase<MediaLibrarySession, |
| Builder, MediaLibrarySessionCallback> { |
| private MediaLibrarySessionImplBase.Builder mImpl; |
| |
| // Builder requires MediaLibraryService2 instead of Context just to ensure that the |
| // builder can be only instantiated within the MediaLibraryService2. |
| // Ideally it's better to make it inner class of service to enforce, it violates API |
| // guideline that Builders should be the inner class of the building target. |
| public Builder(@NonNull MediaLibraryService2 service, |
| @NonNull Executor callbackExecutor, |
| @NonNull MediaLibrarySessionCallback callback) { |
| super(service); |
| mImpl = new MediaLibrarySessionImplBase.Builder(service); |
| setImpl(mImpl); |
| setSessionCallback(callbackExecutor, callback); |
| } |
| |
| @Override |
| public @NonNull Builder setPlayer(@NonNull MediaPlayerBase player) { |
| return super.setPlayer(player); |
| } |
| |
| @Override |
| public @NonNull Builder setPlaylistAgent(@NonNull MediaPlaylistAgent playlistAgent) { |
| return super.setPlaylistAgent(playlistAgent); |
| } |
| |
| @Override |
| public @NonNull Builder setVolumeProvider( |
| @Nullable VolumeProviderCompat volumeProvider) { |
| return super.setVolumeProvider(volumeProvider); |
| } |
| |
| @Override |
| public @NonNull Builder setSessionActivity(@Nullable PendingIntent pi) { |
| return super.setSessionActivity(pi); |
| } |
| |
| @Override |
| public @NonNull Builder setId(@NonNull String id) { |
| return super.setId(id); |
| } |
| |
| @Override |
| public @NonNull Builder setSessionCallback(@NonNull Executor executor, |
| @NonNull MediaLibrarySessionCallback callback) { |
| return super.setSessionCallback(executor, callback); |
| } |
| |
| @Override |
| public @NonNull MediaLibrarySession build() { |
| return super.build(); |
| } |
| } |
| |
| MediaLibrarySession(SupportLibraryImpl impl) { |
| super(impl); |
| } |
| |
| /** |
| * Notify 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 MediaBrowser2#getChildren(String, int, int, Bundle)} to get |
| * the list of children. |
| * |
| * @param controller controller to notify |
| * @param parentId parent id with changes in its children |
| * @param itemCount number of children. |
| * @param extras extra information from session to controller |
| */ |
| public void notifyChildrenChanged(@NonNull ControllerInfo controller, |
| @NonNull String parentId, int itemCount, @Nullable Bundle extras) { |
| Bundle options = new Bundle(extras); |
| options.putInt(MediaBrowser2.EXTRA_ITEM_COUNT, itemCount); |
| options.putBundle(MediaBrowser2.EXTRA_TARGET, controller.toBundle()); |
| } |
| |
| /** |
| * Notify all controllers that subscribed to the parent about change in the parent's |
| * children, regardless of the extra bundle supplied by |
| * {@link MediaBrowser2#subscribe(String, Bundle)}. |
| * |
| * @param parentId parent id |
| * @param itemCount number of children |
| * @param extras extra information from session to controller |
| */ |
| // This is for the backward compatibility. |
| public void notifyChildrenChanged(@NonNull String parentId, int itemCount, |
| @Nullable Bundle extras) { |
| Bundle options = new Bundle(extras); |
| options.putInt(MediaBrowser2.EXTRA_ITEM_COUNT, itemCount); |
| getServiceCompat().notifyChildrenChanged(parentId, options); |
| } |
| |
| /** |
| * Notify controller about change in the search result. |
| * |
| * @param controller controller to notify |
| * @param query previously sent search query from the controller. |
| * @param itemCount the number of items that have been found in the search. |
| * @param extras extra bundle |
| */ |
| public void notifySearchResultChanged(@NonNull ControllerInfo controller, |
| @NonNull String query, int itemCount, @NonNull Bundle extras) { |
| // TODO: Implement |
| } |
| |
| private MediaLibraryService2 getService() { |
| return (MediaLibraryService2) getContext(); |
| } |
| |
| private MediaBrowserServiceCompat getServiceCompat() { |
| return getService().getServiceCompat(); |
| } |
| |
| @Override |
| MediaLibrarySessionCallback getCallback() { |
| return (MediaLibrarySessionCallback) super.getCallback(); |
| } |
| } |
| |
| @Override |
| MediaBrowserServiceCompat createBrowserServiceCompat() { |
| return new MyBrowserService(); |
| } |
| |
| @Override |
| int getSessionType() { |
| return SessionToken2.TYPE_LIBRARY_SERVICE; |
| } |
| |
| @Override |
| public void onCreate() { |
| super.onCreate(); |
| |
| MediaSession2 session = getSession(); |
| if (!(session instanceof MediaLibrarySession)) { |
| throw new RuntimeException("Expected MediaLibrarySession, but returned MediaSession2"); |
| } |
| } |
| |
| private MediaLibrarySession getLibrarySession() { |
| return (MediaLibrarySession) getSession(); |
| } |
| |
| @Override |
| public IBinder onBind(Intent intent) { |
| return super.onBind(intent); |
| } |
| |
| /** |
| * Called when another app requested to start this service. |
| * <p> |
| * Library service will accept or reject the connection with the |
| * {@link MediaLibrarySessionCallback} in the created session. |
| * <p> |
| * Service wouldn't run if {@code null} is returned or session's ID doesn't match with the |
| * expected ID that you've specified through the AndroidManifest.xml. |
| * <p> |
| * This method will be called on the main thread. |
| * |
| * @param sessionId session id written in the AndroidManifest.xml. |
| * @return a new library session |
| * @see Builder |
| * @see #getSession() |
| * @throws RuntimeException if returned session is invalid |
| */ |
| @Override |
| public @NonNull abstract MediaLibrarySession onCreateSession(String sessionId); |
| |
| /** |
| * Contains information that the library service needs to send to the client when |
| * {@link MediaBrowser2#getLibraryRoot(Bundle)} is called. |
| */ |
| public static final class LibraryRoot { |
| /** |
| * The lookup key for a boolean that indicates whether the library service should return a |
| * librar root for recently played media items. |
| * |
| * <p>When creating a media browser for a given media library service, this key can be |
| * supplied as a root hint for retrieving media items that are recently played. |
| * If the media library service can provide such media items, the implementation must return |
| * the key in the root hint when |
| * {@link MediaLibrarySessionCallback#onGetLibraryRoot} |
| * is called back. |
| * |
| * <p>The root hint may contain multiple keys. |
| * |
| * @see #EXTRA_OFFLINE |
| * @see #EXTRA_SUGGESTED |
| */ |
| public static final String EXTRA_RECENT = "android.media.extra.RECENT"; |
| |
| /** |
| * The lookup key for a boolean that indicates whether the library service should return a |
| * library root for offline media items. |
| * |
| * <p>When creating a media browser for a given media library service, this key can be |
| * supplied as a root hint for retrieving media items that are can be played without an |
| * internet connection. |
| * If the media library service can provide such media items, the implementation must return |
| * the key in the root hint when |
| * {@link MediaLibrarySessionCallback#onGetLibraryRoot} |
| * is called back. |
| * |
| * <p>The root hint may contain multiple keys. |
| * |
| * @see #EXTRA_RECENT |
| * @see #EXTRA_SUGGESTED |
| */ |
| public static final String EXTRA_OFFLINE = "android.media.extra.OFFLINE"; |
| |
| /** |
| * The lookup key for a boolean that indicates whether the library service should return a |
| * library root for suggested media items. |
| * |
| * <p>When creating a media browser for a given media library service, this key can be |
| * supplied as a root hint for retrieving the media items suggested by the media library |
| * service. The list of media items is considered ordered by relevance, first being the top |
| * suggestion. |
| * If the media library service can provide such media items, the implementation must return |
| * the key in the root hint when |
| * {@link MediaLibrarySessionCallback#onGetLibraryRoot} |
| * is called back. |
| * |
| * <p>The root hint may contain multiple keys. |
| * |
| * @see #EXTRA_RECENT |
| * @see #EXTRA_OFFLINE |
| */ |
| public static final String EXTRA_SUGGESTED = "android.media.extra.SUGGESTED"; |
| |
| private final String mRootId; |
| private final Bundle mExtras; |
| |
| //private final LibraryRootProvider mProvider; |
| |
| /** |
| * Constructs a library root. |
| * @param rootId The root id for browsing. |
| * @param extras Any extras about the library service. |
| */ |
| public LibraryRoot(@NonNull String rootId, @Nullable Bundle extras) { |
| if (rootId == null) { |
| throw new IllegalArgumentException("rootId shouldn't be null"); |
| } |
| mRootId = rootId; |
| mExtras = extras; |
| } |
| |
| /** |
| * Gets the root id for browsing. |
| */ |
| public String getRootId() { |
| return mRootId; |
| } |
| |
| /** |
| * Gets any extras about the library service. |
| */ |
| public Bundle getExtras() { |
| return mExtras; |
| } |
| } |
| |
| private class MyBrowserService extends MediaBrowserServiceCompat { |
| @Override |
| public BrowserRoot onGetRoot(String clientPackageName, int clientUid, |
| final Bundle extras) { |
| if (MediaUtils2.isDefaultLibraryRootHint(extras)) { |
| // For connection request from the MediaController2. accept the connection from |
| // here, and let MediaLibrarySession decide whether to accept or reject the |
| // controller. |
| return sDefaultBrowserRoot; |
| } |
| final CountDownLatch latch = new CountDownLatch(1); |
| // TODO: Revisit this when we support caller information. |
| final ControllerInfo info = new ControllerInfo(MediaLibraryService2.this, clientUid, -1, |
| clientPackageName, null); |
| MediaLibrarySession session = getLibrarySession(); |
| // Call onGetLibraryRoot() directly instead of execute on the executor. Here's the |
| // reason. |
| // We need to return browser root here. So if we run the callback on the executor, we |
| // should wait for the completion. |
| // However, we cannot wait if the callback executor is the main executor, which posts |
| // the runnable to the main thread's. In that case, since this onGetRoot() always runs |
| // on the main thread, the posted runnable for calling onGetLibraryRoot() wouldn't run |
| // in here. Even worse, we cannot know whether it would be run on the main thread or |
| // not. |
| // Because of the reason, just call onGetLibraryRoot directly here. onGetLibraryRoot() |
| // has documentation that it may be called on the main thread. |
| LibraryRoot libraryRoot = session.getCallback().onGetLibraryRoot( |
| session, info, extras); |
| if (libraryRoot == null) { |
| return null; |
| } |
| return new BrowserRoot(libraryRoot.getRootId(), libraryRoot.getExtras()); |
| } |
| |
| @Override |
| public void onLoadChildren(String parentId, Result<List<MediaItem>> result) { |
| onLoadChildren(parentId, result, null); |
| } |
| |
| @Override |
| public void onLoadChildren(final String parentId, final Result<List<MediaItem>> result, |
| final Bundle options) { |
| final ControllerInfo controller = getController(); |
| getLibrarySession().getCallbackExecutor().execute(new Runnable() { |
| @Override |
| public void run() { |
| int page = options.getInt(EXTRA_PAGE, -1); |
| int pageSize = options.getInt(EXTRA_PAGE_SIZE, -1); |
| if (page >= 0 && pageSize >= 0) { |
| // Requesting the list of children through the pagenation. |
| List<MediaItem2> children = getLibrarySession().getCallback().onGetChildren( |
| getLibrarySession(), controller, parentId, page, pageSize, options); |
| if (children == null) { |
| result.sendError(null); |
| } else { |
| List<MediaItem> list = new ArrayList<>(); |
| for (int i = 0; i < children.size(); i++) { |
| list.add(MediaUtils2.createMediaItem(children.get(i))); |
| } |
| result.sendResult(list); |
| } |
| } else { |
| // Only wants to register callbacks |
| getLibrarySession().getCallback().onSubscribe(getLibrarySession(), |
| controller, parentId, options); |
| } |
| } |
| }); |
| } |
| |
| @Override |
| public void onLoadItem(final String itemId, final Result<MediaItem> result) { |
| final ControllerInfo controller = getController(); |
| getLibrarySession().getCallbackExecutor().execute(new Runnable() { |
| @Override |
| public void run() { |
| MediaItem2 item = getLibrarySession().getCallback().onGetItem( |
| getLibrarySession(), controller, itemId); |
| if (item == null) { |
| result.sendError(null); |
| } else { |
| result.sendResult(MediaUtils2.createMediaItem(item)); |
| } |
| } |
| }); |
| } |
| |
| @Override |
| public void onSearch(String query, Bundle extras, Result<List<MediaItem>> result) { |
| // TODO: Implement |
| } |
| |
| @Override |
| public void onCustomAction(String action, Bundle extras, Result<Bundle> result) { |
| // TODO: Implement |
| } |
| |
| private ControllerInfo getController() { |
| // TODO: Implement, by using getBrowserRootHints() / getCurrentBrowserInfo() / ... |
| return null; |
| } |
| } |
| } |