diff options
Diffstat (limited to 'androidx/media/MediaLibraryService2.java')
-rw-r--r-- | androidx/media/MediaLibraryService2.java | 589 |
1 files changed, 589 insertions, 0 deletions
diff --git a/androidx/media/MediaLibraryService2.java b/androidx/media/MediaLibraryService2.java new file mode 100644 index 00000000..edd97c3a --- /dev/null +++ b/androidx/media/MediaLibraryService2.java @@ -0,0 +1,589 @@ +/* + * 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; + } + } +} |