From 8fb6addcbee93c57a2fc4e7073c13d502856aad5 Mon Sep 17 00:00:00 2001 From: Sungsoo Date: Mon, 21 Nov 2016 16:39:33 +0900 Subject: [PATCH] MediaBrowser: Introduce the search API Bug: 28210241 Test: passed cts test (MediaBrowserServiceTest and MediaBrowserTest) Change-Id: I73c58f5385637c626da1503d27e518b4f0c9ea80 --- api/current.txt | 8 + api/system-current.txt | 8 + api/test-current.txt | 8 + .../android/media/browse/MediaBrowser.java | 138 +++++++++++++++--- .../service/media/IMediaBrowserService.aidl | 4 +- .../service/media/MediaBrowserService.java | 110 +++++++++++--- 6 files changed, 234 insertions(+), 42 deletions(-) diff --git a/api/current.txt b/api/current.txt index c294d8ae4b974..4a41ba29c9745 100644 --- a/api/current.txt +++ b/api/current.txt @@ -22614,6 +22614,7 @@ package android.media.browse { method public android.content.ComponentName getServiceComponent(); method public android.media.session.MediaSession.Token getSessionToken(); method public boolean isConnected(); + method public void search(java.lang.String, android.os.Bundle, android.media.browse.MediaBrowser.SearchCallback); method public void subscribe(java.lang.String, android.media.browse.MediaBrowser.SubscriptionCallback); method public void subscribe(java.lang.String, android.os.Bundle, android.media.browse.MediaBrowser.SubscriptionCallback); method public void unsubscribe(java.lang.String); @@ -22649,6 +22650,12 @@ package android.media.browse { field public static final int FLAG_PLAYABLE = 2; // 0x2 } + public static abstract class MediaBrowser.SearchCallback { + ctor public MediaBrowser.SearchCallback(); + method public void onError(java.lang.String, android.os.Bundle); + method public void onSearchResult(java.lang.String, android.os.Bundle, java.util.List); + } + public static abstract class MediaBrowser.SubscriptionCallback { ctor public MediaBrowser.SubscriptionCallback(); method public void onChildrenLoaded(java.lang.String, java.util.List); @@ -35074,6 +35081,7 @@ package android.service.media { method public abstract void onLoadChildren(java.lang.String, android.service.media.MediaBrowserService.Result>); method public void onLoadChildren(java.lang.String, android.service.media.MediaBrowserService.Result>, android.os.Bundle); method public void onLoadItem(java.lang.String, android.service.media.MediaBrowserService.Result); + method public void onSearch(java.lang.String, android.os.Bundle, android.service.media.MediaBrowserService.Result>); method public void setSessionToken(android.media.session.MediaSession.Token); field public static final java.lang.String SERVICE_INTERFACE = "android.media.browse.MediaBrowserService"; } diff --git a/api/system-current.txt b/api/system-current.txt index a3672b3145b17..8196c61d9a188 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -24257,6 +24257,7 @@ package android.media.browse { method public android.content.ComponentName getServiceComponent(); method public android.media.session.MediaSession.Token getSessionToken(); method public boolean isConnected(); + method public void search(java.lang.String, android.os.Bundle, android.media.browse.MediaBrowser.SearchCallback); method public void subscribe(java.lang.String, android.media.browse.MediaBrowser.SubscriptionCallback); method public void subscribe(java.lang.String, android.os.Bundle, android.media.browse.MediaBrowser.SubscriptionCallback); method public void unsubscribe(java.lang.String); @@ -24292,6 +24293,12 @@ package android.media.browse { field public static final int FLAG_PLAYABLE = 2; // 0x2 } + public static abstract class MediaBrowser.SearchCallback { + ctor public MediaBrowser.SearchCallback(); + method public void onError(java.lang.String, android.os.Bundle); + method public void onSearchResult(java.lang.String, android.os.Bundle, java.util.List); + } + public static abstract class MediaBrowser.SubscriptionCallback { ctor public MediaBrowser.SubscriptionCallback(); method public void onChildrenLoaded(java.lang.String, java.util.List); @@ -37907,6 +37914,7 @@ package android.service.media { method public abstract void onLoadChildren(java.lang.String, android.service.media.MediaBrowserService.Result>); method public void onLoadChildren(java.lang.String, android.service.media.MediaBrowserService.Result>, android.os.Bundle); method public void onLoadItem(java.lang.String, android.service.media.MediaBrowserService.Result); + method public void onSearch(java.lang.String, android.os.Bundle, android.service.media.MediaBrowserService.Result>); method public void setSessionToken(android.media.session.MediaSession.Token); field public static final java.lang.String SERVICE_INTERFACE = "android.media.browse.MediaBrowserService"; } diff --git a/api/test-current.txt b/api/test-current.txt index 11f1c5c99c11c..b4afa04db4373 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -22701,6 +22701,7 @@ package android.media.browse { method public android.content.ComponentName getServiceComponent(); method public android.media.session.MediaSession.Token getSessionToken(); method public boolean isConnected(); + method public void search(java.lang.String, android.os.Bundle, android.media.browse.MediaBrowser.SearchCallback); method public void subscribe(java.lang.String, android.media.browse.MediaBrowser.SubscriptionCallback); method public void subscribe(java.lang.String, android.os.Bundle, android.media.browse.MediaBrowser.SubscriptionCallback); method public void unsubscribe(java.lang.String); @@ -22736,6 +22737,12 @@ package android.media.browse { field public static final int FLAG_PLAYABLE = 2; // 0x2 } + public static abstract class MediaBrowser.SearchCallback { + ctor public MediaBrowser.SearchCallback(); + method public void onError(java.lang.String, android.os.Bundle); + method public void onSearchResult(java.lang.String, android.os.Bundle, java.util.List); + } + public static abstract class MediaBrowser.SubscriptionCallback { ctor public MediaBrowser.SubscriptionCallback(); method public void onChildrenLoaded(java.lang.String, java.util.List); @@ -35171,6 +35178,7 @@ package android.service.media { method public abstract void onLoadChildren(java.lang.String, android.service.media.MediaBrowserService.Result>); method public void onLoadChildren(java.lang.String, android.service.media.MediaBrowserService.Result>, android.os.Bundle); method public void onLoadItem(java.lang.String, android.service.media.MediaBrowserService.Result); + method public void onSearch(java.lang.String, android.os.Bundle, android.service.media.MediaBrowserService.Result>); method public void setSessionToken(android.media.session.MediaSession.Token); field public static final java.lang.String SERVICE_INTERFACE = "android.media.browse.MediaBrowserService"; } diff --git a/media/java/android/media/browse/MediaBrowser.java b/media/java/android/media/browse/MediaBrowser.java index 957c2d6eb09f0..81db37ea3ad80 100644 --- a/media/java/android/media/browse/MediaBrowser.java +++ b/media/java/android/media/browse/MediaBrowser.java @@ -109,13 +109,13 @@ public final class MediaBrowser { private IMediaBrowserServiceCallbacks mServiceCallbacks; /** - * Creates a media browser for the specified media browse service. + * Creates a media browser for the specified media browser service. * * @param context The context. - * @param serviceComponent The component name of the media browse service. + * @param serviceComponent The component name of the media browser service. * @param callback The connection callback. * @param rootHints An optional bundle of service-specific arguments to send - * to the media browse service when connecting and retrieving the root id + * to the media browser 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. * @see android.service.media.MediaBrowserService.BrowserRoot#EXTRA_RECENT @@ -140,7 +140,7 @@ public final class MediaBrowser { } /** - * Connects to the media browse service. + * Connects to the media browser service. *

* The connection callback specified in the constructor will be invoked * when the connection completes or fails. @@ -206,7 +206,7 @@ public final class MediaBrowser { } /** - * Disconnects from the media browse service. + * Disconnects from the media browser service. * After this, no more callbacks will be received. */ public void disconnect() { @@ -362,7 +362,7 @@ public final class MediaBrowser { * * @param parentId The id of the parent media item whose list of children * will be subscribed. - * @param options A bundle of service-specific arguments to send to the media + * @param options The bundle of service-specific arguments to send to the media * browse service. The contents of this bundle may affect the * information returned when browsing. * @param callback The callback to receive the list of children. @@ -370,7 +370,7 @@ public final class MediaBrowser { public void subscribe(@NonNull String parentId, @NonNull Bundle options, @NonNull SubscriptionCallback callback) { if (options == null) { - throw new IllegalArgumentException("options are null"); + throw new IllegalArgumentException("options cannot be null"); } subscribeInternal(parentId, new Bundle(options), callback); } @@ -398,11 +398,11 @@ public final class MediaBrowser { * * @param parentId The id of the parent media item whose list of children * will be unsubscribed. - * @param callback A callback sent to the media browse service to subscribe. + * @param callback A callback sent to the media browser service to subscribe. */ public void unsubscribe(@NonNull String parentId, @NonNull SubscriptionCallback callback) { if (callback == null) { - throw new IllegalArgumentException("callback is null"); + throw new IllegalArgumentException("callback cannot be null"); } unsubscribeInternal(parentId, callback); } @@ -417,10 +417,10 @@ public final class MediaBrowser { */ public void getItem(final @NonNull String mediaId, @NonNull final ItemCallback cb) { if (TextUtils.isEmpty(mediaId)) { - throw new IllegalArgumentException("mediaId is empty."); + throw new IllegalArgumentException("mediaId cannot be empty."); } if (cb == null) { - throw new IllegalArgumentException("cb is null."); + throw new IllegalArgumentException("cb cannot be null."); } if (mState != CONNECT_STATE_CONNECTED) { Log.i(TAG, "Not connected, unable to retrieve the MediaItem."); @@ -451,7 +451,7 @@ public final class MediaBrowser { try { mServiceBinder.getMediaItem(mediaId, receiver, mServiceCallbacks); } catch (RemoteException e) { - Log.i(TAG, "Remote error getting media item."); + Log.i(TAG, "Remote error getting media item.", e); mHandler.post(new Runnable() { @Override public void run() { @@ -461,13 +461,74 @@ public final class MediaBrowser { } } + /** + * Searches {@link MediaItem media items} from the connected service. Not + * all services may support this, and {@link SearchCallback#onError} will be + * called if not implemented. + * + * @param query The search query that contains keywords separated by space. Should not be + * an empty string. + * @param extras The bundle of service-specific arguments to send to the media browser + * service. The contents of this bundle may affect the search result. + * @param callback The callback to receive the search result. + */ + public void search(@NonNull final String query, final Bundle extras, SearchCallback callback) { + if (TextUtils.isEmpty(query)) { + throw new IllegalArgumentException("query cannot be empty."); + } + if (callback == null) { + throw new IllegalArgumentException("callback cannot be null."); + } + if (mState != CONNECT_STATE_CONNECTED) { + Log.i(TAG, "Not connected, unable to search."); + mHandler.post(new Runnable() { + @Override + public void run() { + callback.onError(query, extras); + } + }); + return; + } + ResultReceiver receiver = new ResultReceiver(mHandler) { + @Override + protected void onReceiveResult(int resultCode, Bundle resultData) { + if (resultCode != 0 || resultData == null + || !resultData.containsKey(MediaBrowserService.KEY_SEARCH_RESULTS)) { + callback.onError(query, extras); + return; + } + Parcelable[] items = resultData.getParcelableArray( + MediaBrowserService.KEY_SEARCH_RESULTS); + List results = null; + if (items != null) { + results = new ArrayList<>(); + for (Parcelable item : items) { + results.add((MediaItem) item); + } + } + callback.onSearchResult(query, extras, results); + } + }; + try { + mServiceBinder.search(query, extras, receiver, mServiceCallbacks); + } catch (RemoteException e) { + Log.i(TAG, "Remote error getting media item.", e); + mHandler.post(new Runnable() { + @Override + public void run() { + callback.onError(query, extras); + } + }); + } + } + private void subscribeInternal(String parentId, Bundle options, SubscriptionCallback callback) { // Check arguments. if (TextUtils.isEmpty(parentId)) { - throw new IllegalArgumentException("parentId is empty."); + throw new IllegalArgumentException("parentId cannot be empty."); } if (callback == null) { - throw new IllegalArgumentException("callback is null"); + throw new IllegalArgumentException("callback cannot be null"); } // Update or create the subscription. Subscription sub = mSubscriptions.get(parentId); @@ -497,7 +558,7 @@ public final class MediaBrowser { private void unsubscribeInternal(String parentId, SubscriptionCallback callback) { // Check arguments. if (TextUtils.isEmpty(parentId)) { - throw new IllegalArgumentException("parentId is empty."); + throw new IllegalArgumentException("parentId cannot be empty."); } Subscription sub = mSubscriptions.get(parentId); @@ -712,7 +773,9 @@ public final class MediaBrowser { } /** - * A class with information on a single media item for use in browsing media. + * A class with information on a single media item for use in browsing/searching media. + * MediaItems are application dependent so we cannot guarantee that they contain the + * right values. */ public static class MediaItem implements Parcelable { private final int mFlags; @@ -830,7 +893,7 @@ public final class MediaBrowser { * Returns the media id in the {@link MediaDescription} for this item. * @see android.media.MediaMetadata#METADATA_KEY_MEDIA_ID */ - public @NonNull String getMediaId() { + public @Nullable String getMediaId() { return mDescription.getMediaId(); } } @@ -882,7 +945,7 @@ public final class MediaBrowser { * * @param parentId The media id of the parent media item. * @param children The children which were loaded. - * @param options A bundle of service-specific arguments sent to the media + * @param options The bundle of service-specific arguments sent to the media * browse service. The contents of this bundle may affect the * information returned when browsing. */ @@ -912,8 +975,8 @@ public final class MediaBrowser { * * @param parentId The media id of the parent media item whose children could * not be loaded. - * @param options A bundle of service-specific arguments sent to the media - * browse service. + * @param options The bundle of service-specific arguments sent to the media + * browser service. */ public void onError(@NonNull String parentId, @NonNull Bundle options) { } @@ -924,7 +987,7 @@ public final class MediaBrowser { */ public static abstract class ItemCallback { /** - * Called when the item has been returned by the browser service. + * Called when the item has been returned by the connected service. * * @param item The item that was returned or null if it doesn't exist. */ @@ -932,11 +995,38 @@ public final class MediaBrowser { } /** - * Called when the item doesn't exist or there was an error retrieving it. + * Called there was an error retrieving it or the connected service doesn't support + * {@link #getItem}. * - * @param itemId The media id of the media item which could not be loaded. + * @param mediaId The media id of the media item which could not be loaded. */ - public void onError(@NonNull String itemId) { + public void onError(@NonNull String mediaId) { + } + } + + /** + * Callback for receiving the result of {@link #search}. + */ + public static abstract class SearchCallback { + /** + * Called when the {@link #search} finished successfully. + * + * @param query The search query sent for the search request to the connected service. + * @param extras The bundle of service-specific arguments sent to the connected service. + * @param items The list of media items which contains the search result. + */ + public void onSearchResult(@NonNull String query, Bundle extras, + @NonNull List items) { + } + + /** + * Called when an error happens while {@link #search} or the connected service doesn't + * support {@link #search}. + * + * @param query The search query sent for the search request to the connected service. + * @param extras The bundle of service-specific arguments sent to the connected service. + */ + public void onError(@NonNull String query, Bundle extras) { } } diff --git a/media/java/android/service/media/IMediaBrowserService.aidl b/media/java/android/service/media/IMediaBrowserService.aidl index 84f41f6c3afe7..e95154ff45400 100644 --- a/media/java/android/service/media/IMediaBrowserService.aidl +++ b/media/java/android/service/media/IMediaBrowserService.aidl @@ -19,8 +19,10 @@ oneway interface IMediaBrowserService { void addSubscriptionDeprecated(String uri, IMediaBrowserServiceCallbacks callbacks); void removeSubscriptionDeprecated(String uri, IMediaBrowserServiceCallbacks callbacks); - void getMediaItem(String uri, in ResultReceiver cb, IMediaBrowserServiceCallbacks callbacks); + void search(String query, in Bundle extras, in ResultReceiver cb, + IMediaBrowserServiceCallbacks callbacks); + void addSubscription(String uri, in IBinder token, in Bundle options, IMediaBrowserServiceCallbacks callbacks); void removeSubscription(String uri, in IBinder token, IMediaBrowserServiceCallbacks callbacks); diff --git a/media/java/android/service/media/MediaBrowserService.java b/media/java/android/service/media/MediaBrowserService.java index a19e347b4507c..16847c1b76d19 100644 --- a/media/java/android/service/media/MediaBrowserService.java +++ b/media/java/android/service/media/MediaBrowserService.java @@ -52,9 +52,9 @@ import java.util.Iterator; import java.util.List; /** - * Base class for media browse services. + * Base class for media browser services. *

- * Media browse services enable applications to browse media content provided by an application + * Media browser 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}. *

@@ -85,18 +85,27 @@ public abstract class MediaBrowserService extends Service { /** * A key for passing the MediaItem to the ResultReceiver in getItem. - * * @hide */ public static final String KEY_MEDIA_ITEM = "media_item"; - private static final int RESULT_FLAG_OPTION_NOT_HANDLED = 0x00000001; - private static final int RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED = 0x00000002; + /** + * A key for passing the list of MediaItems to the ResultReceiver in search. + * @hide + */ + public static final String KEY_SEARCH_RESULTS = "search_results"; + + private static final int RESULT_FLAG_OPTION_NOT_HANDLED = 1 << 0; + private static final int RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED = 1 << 1; + private static final int RESULT_FLAG_ON_SEARCH_NOT_IMPLEMENTED = 1 << 2; + + private static final int RESULT_ERROR = -1; + private static final int RESULT_OK = 0; /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef(flag=true, value = { RESULT_FLAG_OPTION_NOT_HANDLED, - RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED }) + RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED, RESULT_FLAG_ON_SEARCH_NOT_IMPLEMENTED }) private @interface ResultFlags { } private final ArrayMap mConnections = new ArrayMap<>(); @@ -307,10 +316,6 @@ public abstract class MediaBrowserService extends Service { @Override public void getMediaItem(final String mediaId, final ResultReceiver receiver, final IMediaBrowserServiceCallbacks callbacks) { - if (TextUtils.isEmpty(mediaId) || receiver == null) { - return; - } - mHandler.post(new Runnable() { @Override public void run() { @@ -324,6 +329,23 @@ public abstract class MediaBrowserService extends Service { } }); } + + @Override + public void search(final String query, Bundle extras, ResultReceiver receiver, + final IMediaBrowserServiceCallbacks callbacks) { + mHandler.post(new Runnable() { + @Override + public void run() { + final IBinder b = callbacks.asBinder(); + ConnectionRecord connection = mConnections.get(b); + if (connection == null) { + Log.w(TAG, "search for callback that isn't registered query=" + query); + return; + } + performSearch(query, extras, connection, receiver); + } + }); + } } @Override @@ -358,7 +380,7 @@ public abstract class MediaBrowserService extends Service { * @param clientUid The uid of the application which is requesting access to * browse media. * @param rootHints An optional bundle of service-specific arguments to send - * to the media browse service when connecting and retrieving the + * to the media browser 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 BrowserRoot} for accessing this app's content or null. @@ -412,8 +434,8 @@ public abstract class MediaBrowserService extends Service { * @param parentId The id of the parent media item whose children are to be * queried. * @param result The Result to send the list of children to. - * @param options A bundle of service-specific arguments sent from the media - * browse. The information returned through the result should be + * @param options The bundle of service-specific arguments sent from the media + * browser. The information returned through the result should be * affected by the contents of this bundle. */ public void onLoadChildren(@NonNull String parentId, @@ -449,6 +471,32 @@ public abstract class MediaBrowserService extends Service { result.sendResult(null); } + /** + * Called to get the search result. + *

+ * Implementations must call {@link Result#sendResult result.sendResult}. If + * the search will be an expensive operation {@link Result#detach result.detach} + * may be called before returning from this function, and then {@link Result#sendResult + * result.sendResult} called when the search has been completed. + *

+ * In case there are no search results, call {@link Result#sendResult} with an empty list. + * In case there are some errors happened, call {@link Result#sendResult result.sendResult} + * with {@code null}, which will invoke {@link MediaBrowser.SearchCallback#onError}. + *

+ * The default implementation will invoke {@link MediaBrowser.SearchCallback#onError}. + *

+ * + * @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. + * @param result The {@link Result} to send the search result. + */ + public void onSearch(@NonNull String query, Bundle extras, + Result> result) { + result.setFlags(RESULT_FLAG_ON_SEARCH_NOT_IMPLEMENTED); + result.sendResult(null); + } + /** * Call to set the media session. *

@@ -531,8 +579,8 @@ public abstract class MediaBrowserService extends Service { * * @param parentId The id of the parent media item whose * children changed. - * @param options A bundle of service-specific arguments to send - * to the media browse. The contents of this bundle may + * @param options The bundle of service-specific arguments to send + * to the media browser. The contents of this bundle may * contain the information about the change. */ public void notifyChildrenChanged(@NonNull String parentId, @NonNull Bundle options) { @@ -705,12 +753,12 @@ public abstract class MediaBrowserService extends Service { @Override void onResultSent(MediaBrowser.MediaItem item, @ResultFlags int flag) { if ((flag & RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED) != 0) { - receiver.send(-1, null); + receiver.send(RESULT_ERROR, null); return; } Bundle bundle = new Bundle(); bundle.putParcelable(KEY_MEDIA_ITEM, item); - receiver.send(0, bundle); + receiver.send(RESULT_OK, bundle); } }; @@ -724,6 +772,34 @@ public abstract class MediaBrowserService extends Service { } } + private void performSearch(String query, Bundle extras, final ConnectionRecord connection, + final ResultReceiver receiver) { + final Result> result = + new Result>(query) { + @Override + void onResultSent(List items, @ResultFlags int flag) { + if ((flag & RESULT_FLAG_ON_SEARCH_NOT_IMPLEMENTED) != 0 + || items == null) { + receiver.send(RESULT_ERROR, null); + return; + } + Bundle bundle = new Bundle(); + bundle.putParcelableArray(KEY_SEARCH_RESULTS, + items.toArray(new MediaBrowser.MediaItem[0])); + receiver.send(RESULT_OK, bundle); + } + }; + + mCurConnection = connection; + onSearch(query, extras, result); + mCurConnection = null; + + if (!result.isDone()) { + throw new IllegalStateException("onSearch must call detach() or sendResult()" + + " before returning for query=" + query); + } + } + /** * Contains information that the browser service needs to send to the client * when first connected.