summaryrefslogtreecommitdiff
path: root/androidx/media/MediaLibraryService2.java
diff options
context:
space:
mode:
Diffstat (limited to 'androidx/media/MediaLibraryService2.java')
-rw-r--r--androidx/media/MediaLibraryService2.java589
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>
+ * &lt;service android:name="component_name_of_your_implementation" &gt;
+ * &lt;intent-filter&gt;
+ * &lt;action android:name="android.media.MediaLibraryService2" /&gt;
+ * &lt;/intent-filter&gt;
+ * &lt;/service&gt;</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;
+ }
+ }
+}