diff --git a/api/current.txt b/api/current.txt index 18b30da6aa250..13b1d11dcdb52 100644 --- a/api/current.txt +++ b/api/current.txt @@ -16217,6 +16217,7 @@ package android.media.browse { method public void connect(); method public void disconnect(); method public android.os.Bundle getExtras(); + method public void getMediaItem(java.lang.String, android.media.browse.MediaBrowser.MediaItemCallback); method public java.lang.String getRoot(); method public android.content.ComponentName getServiceComponent(); method public android.media.session.MediaSession.Token getSessionToken(); @@ -16246,6 +16247,12 @@ package android.media.browse { field public static final int FLAG_PLAYABLE = 2; // 0x2 } + public static abstract class MediaBrowser.MediaItemCallback { + ctor public MediaBrowser.MediaItemCallback(); + method public void onError(); + method public void onMediaItemLoaded(android.media.browse.MediaBrowser.MediaItem); + } + public static abstract class MediaBrowser.SubscriptionCallback { ctor public MediaBrowser.SubscriptionCallback(); method public void onChildrenLoaded(java.lang.String, java.util.List); @@ -27360,6 +27367,7 @@ package android.service.media { public abstract class MediaBrowserService extends android.app.Service { ctor public MediaBrowserService(); method public void dump(java.io.FileDescriptor, java.io.PrintWriter, java.lang.String[]); + method public void getMediaItem(java.lang.String, android.service.media.MediaBrowserService.Result) throws java.lang.UnsupportedOperationException; method public android.media.session.MediaSession.Token getSessionToken(); method public void notifyChildrenChanged(java.lang.String); method public android.os.IBinder onBind(android.content.Intent); diff --git a/media/java/android/media/browse/MediaBrowser.java b/media/java/android/media/browse/MediaBrowser.java index e82567c7b27dc..ef8d169203c89 100644 --- a/media/java/android/media/browse/MediaBrowser.java +++ b/media/java/android/media/browse/MediaBrowser.java @@ -33,6 +33,7 @@ import android.os.IBinder; import android.os.Parcel; import android.os.Parcelable; import android.os.RemoteException; +import android.os.ResultReceiver; import android.service.media.MediaBrowserService; import android.service.media.IMediaBrowserService; import android.service.media.IMediaBrowserServiceCallbacks; @@ -347,8 +348,8 @@ public final class MediaBrowser { */ public void unsubscribe(@NonNull String parentId) { // Check arguments. - if (parentId == null) { - throw new IllegalArgumentException("parentId is null"); + if (TextUtils.isEmpty(parentId)) { + throw new IllegalArgumentException("parentId is empty."); } // Remove from our list. @@ -366,6 +367,60 @@ public final class MediaBrowser { } } + /** + * Retrieves a specific {@link MediaItem} from the connected service. Not + * all services may support this, so falling back to subscribing to the + * parent's id should be used when unavailable. + * + * @param mediaId The id of the item to retrieve. + * @param cb The callback to receive the result on. + */ + public void getMediaItem(@NonNull String mediaId, @NonNull final MediaItemCallback cb) { + if (TextUtils.isEmpty(mediaId)) { + throw new IllegalArgumentException("mediaId is empty."); + } + if (cb == null) { + throw new IllegalArgumentException("cb is null."); + } + if (mState != CONNECT_STATE_CONNECTED) { + Log.i(TAG, "Not connected, unable to retrieve the MediaItem."); + mHandler.post(new Runnable() { + @Override + public void run() { + cb.onError(); + } + }); + return; + } + ResultReceiver receiver = new ResultReceiver(mHandler) { + @Override + protected void onReceiveResult(int resultCode, Bundle resultData) { + if (resultCode != 0 || resultData == null + || !resultData.containsKey(MediaBrowserService.KEY_MEDIA_ITEM)) { + cb.onError(); + return; + } + Parcelable item = resultData.getParcelable(MediaBrowserService.KEY_MEDIA_ITEM); + if (!(item instanceof MediaItem)) { + cb.onError(); + } + cb.onMediaItemLoaded((MediaItem) resultData.getParcelable( + MediaBrowserService.KEY_MEDIA_ITEM)); + } + }; + try { + mServiceBinder.getMediaItem(mediaId, receiver); + } catch (RemoteException e) { + Log.i(TAG, "Remote error getting media item."); + mHandler.post(new Runnable() { + @Override + public void run() { + cb.onError(); + } + }); + } + } + /** * For debugging. */ @@ -689,6 +744,27 @@ public final class MediaBrowser { } } + /** + * Callback for receiving the result of {@link #getMediaItem}. + */ + public static abstract class MediaItemCallback { + + /** + * Called when the item has been returned by the browser service. + * + * @param item The item that was returned or null if it doesn't exist. + */ + public void onMediaItemLoaded(MediaItem item) { + } + + /** + * Called when the id doesn't exist or there was an error retrieving the + * item. + */ + public void onError() { + } + } + /** * ServiceConnection to the other app. */ diff --git a/media/java/android/service/media/IMediaBrowserService.aidl b/media/java/android/service/media/IMediaBrowserService.aidl index 01285ee5c8ccf..f01fc076badf8 100644 --- a/media/java/android/service/media/IMediaBrowserService.aidl +++ b/media/java/android/service/media/IMediaBrowserService.aidl @@ -6,6 +6,7 @@ import android.content.res.Configuration; import android.service.media.IMediaBrowserServiceCallbacks; import android.net.Uri; import android.os.Bundle; +import android.os.ResultReceiver; /** * Media API allows clients to browse through hierarchy of a user’s media collection, @@ -18,4 +19,5 @@ oneway interface IMediaBrowserService { void addSubscription(String uri, IMediaBrowserServiceCallbacks callbacks); void removeSubscription(String uri, IMediaBrowserServiceCallbacks callbacks); + void getMediaItem(String uri, in ResultReceiver cb); } \ No newline at end of file diff --git a/media/java/android/service/media/MediaBrowserService.java b/media/java/android/service/media/MediaBrowserService.java index 47cb94b8af4cc..8287344bc791f 100644 --- a/media/java/android/service/media/MediaBrowserService.java +++ b/media/java/android/service/media/MediaBrowserService.java @@ -32,8 +32,10 @@ import android.os.Bundle; import android.os.IBinder; import android.os.Handler; import android.os.RemoteException; +import android.os.ResultReceiver; import android.service.media.IMediaBrowserService; import android.service.media.IMediaBrowserServiceCallbacks; +import android.text.TextUtils; import android.util.ArrayMap; import android.util.Log; @@ -74,6 +76,13 @@ public abstract class MediaBrowserService extends Service { @SdkConstant(SdkConstantType.SERVICE_ACTION) public static final String SERVICE_INTERFACE = "android.media.browse.MediaBrowserService"; + /** + * A key for passing the MediaItem to the ResultReceiver in getMediaItem. + * + * @hide + */ + public static final String KEY_MEDIA_ITEM = "media_item"; + private final ArrayMap mConnections = new ArrayMap(); private final Handler mHandler = new Handler(); private ServiceBinder mBinder; @@ -261,6 +270,33 @@ public abstract class MediaBrowserService extends Service { } }); } + + @Override + public void getMediaItem(final String mediaId, final ResultReceiver receiver) { + if (TextUtils.isEmpty(mediaId) || receiver == null) { + return; + } + + mHandler.post(new Runnable() { + @Override + public void run() { + final Result result + = new Result(mediaId) { + @Override + void onResultSent(MediaBrowser.MediaItem item) { + Bundle bundle = new Bundle(); + bundle.putParcelable(KEY_MEDIA_ITEM, item); + receiver.send(0, bundle); + } + }; + try { + MediaBrowserService.this.getMediaItem(mediaId, result); + } catch (UnsupportedOperationException e) { + receiver.send(-1, null); + } + } + }); + } } @Override @@ -284,20 +320,21 @@ public abstract class MediaBrowserService extends Service { /** * Called to get the root information for browsing by a particular client. *

- * 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. + * 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. *

* - * @param clientPackageName The package name of the application - * which is requesting access to browse media. - * @param clientUid The uid of the application which is requesting - * access to browse media. + * @param clientPackageName The package name of the application which is + * requesting access to browse media. + * @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 root id - * for browsing, or null if none. The contents of this bundle may affect - * the information returned when browsing. + * to the media browse 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. */ public abstract @Nullable BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid, @Nullable Bundle rootHints); @@ -305,24 +342,51 @@ public abstract class MediaBrowserService extends Service { /** * Called to get information about the children of a media item. *

- * Implementations must call result.{@link Result#sendResult result.sendResult} with the list - * of children. If loading the children will be an expensive operation that should be performed - * on another thread, result.{@link Result#detach result.detach} may be called before returning - * from this function, and then {@link Result#sendResult result.sendResult} called when - * the loading is complete. + * Implementations must call {@link Result#sendResult result.sendResult} + * with the list of children. If loading the children will be an expensive + * operation that should be performed on another thread, + * {@link Result#detach result.detach} may be called before returning from + * this function, and then {@link Result#sendResult result.sendResult} + * called when the loading is complete. * - * @param parentId The id of the parent media item whose - * children are to be queried. - * @return The list of children, or null if the id is invalid. + * @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, or null if the + * id is invalid. */ public abstract void onLoadChildren(@NonNull String parentId, @NonNull Result> result); + /** + * Called to get a specific media item. The mediaId should be the same id + * that would be returned for this item when it is in a list of child items. + *

+ * Implementations must call {@link Result#sendResult result.sendResult}. If + * loading the item 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 item has + * been loaded. + *

+ * The default implementation throws an exception. + * + * @param mediaId The id for the specific + * {@link android.media.browse.MediaBrowser.MediaItem}. + * @param result The Result to send the item to, or null if the id is + * invalid. + * @throws UnsupportedOperationException + */ + public void getMediaItem(String mediaId, Result result) + throws UnsupportedOperationException { + throw new UnsupportedOperationException("getMediaItem is not supported."); + } + /** * Call to set the media session. *

* This should be called as soon as possible during the service's startup. * It may only be called once. + * + * @param token The token for the service's {@link MediaSession}. */ public void setSessionToken(final MediaSession.Token token) { if (token == null) {