diff options
| -rw-r--r-- | api/current.txt | 8 | ||||
| -rw-r--r-- | api/system-current.txt | 8 | ||||
| -rw-r--r-- | api/test-current.txt | 8 | ||||
| -rw-r--r-- | media/java/android/media/browse/MediaBrowser.java | 332 | ||||
| -rw-r--r-- | media/java/android/media/browse/MediaBrowserUtils.java | 72 | ||||
| -rw-r--r-- | media/java/android/service/media/IMediaBrowserService.aidl | 11 | ||||
| -rw-r--r-- | media/java/android/service/media/IMediaBrowserServiceCallbacks.aidl | 2 | ||||
| -rw-r--r-- | media/java/android/service/media/MediaBrowserService.java | 175 |
8 files changed, 521 insertions, 95 deletions
diff --git a/api/current.txt b/api/current.txt index 737670baca94..47a635062807 100644 --- a/api/current.txt +++ b/api/current.txt @@ -21746,7 +21746,11 @@ package android.media.browse { method public android.media.session.MediaSession.Token getSessionToken(); method public boolean isConnected(); 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); + method public void unsubscribe(java.lang.String, android.os.Bundle); + field public static final java.lang.String EXTRA_PAGE = "android.media.browse.extra.PAGE"; + field public static final java.lang.String EXTRA_PAGE_SIZE = "android.media.browse.extra.PAGE_SIZE"; } public static class MediaBrowser.ConnectionCallback { @@ -21779,7 +21783,9 @@ package android.media.browse { public static abstract class MediaBrowser.SubscriptionCallback { ctor public MediaBrowser.SubscriptionCallback(); method public void onChildrenLoaded(java.lang.String, java.util.List<android.media.browse.MediaBrowser.MediaItem>); + method public void onChildrenLoaded(java.lang.String, java.util.List<android.media.browse.MediaBrowser.MediaItem>, android.os.Bundle); method public void onError(java.lang.String); + method public void onError(java.lang.String, android.os.Bundle); } } @@ -33800,9 +33806,11 @@ package android.service.media { method public void dump(java.io.FileDescriptor, java.io.PrintWriter, java.lang.String[]); method public android.media.session.MediaSession.Token getSessionToken(); method public void notifyChildrenChanged(java.lang.String); + method public void notifyChildrenChanged(java.lang.String, android.os.Bundle); method public android.os.IBinder onBind(android.content.Intent); method public abstract android.service.media.MediaBrowserService.BrowserRoot onGetRoot(java.lang.String, int, android.os.Bundle); method public abstract void onLoadChildren(java.lang.String, android.service.media.MediaBrowserService.Result<java.util.List<android.media.browse.MediaBrowser.MediaItem>>); + method public void onLoadChildren(java.lang.String, android.service.media.MediaBrowserService.Result<java.util.List<android.media.browse.MediaBrowser.MediaItem>>, android.os.Bundle); method public void onLoadItem(java.lang.String, android.service.media.MediaBrowserService.Result<android.media.browse.MediaBrowser.MediaItem>); 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 3c39edb202b2..30c464a2f2d9 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -23163,7 +23163,11 @@ package android.media.browse { method public android.media.session.MediaSession.Token getSessionToken(); method public boolean isConnected(); 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); + method public void unsubscribe(java.lang.String, android.os.Bundle); + field public static final java.lang.String EXTRA_PAGE = "android.media.browse.extra.PAGE"; + field public static final java.lang.String EXTRA_PAGE_SIZE = "android.media.browse.extra.PAGE_SIZE"; } public static class MediaBrowser.ConnectionCallback { @@ -23196,7 +23200,9 @@ package android.media.browse { public static abstract class MediaBrowser.SubscriptionCallback { ctor public MediaBrowser.SubscriptionCallback(); method public void onChildrenLoaded(java.lang.String, java.util.List<android.media.browse.MediaBrowser.MediaItem>); + method public void onChildrenLoaded(java.lang.String, java.util.List<android.media.browse.MediaBrowser.MediaItem>, android.os.Bundle); method public void onError(java.lang.String); + method public void onError(java.lang.String, android.os.Bundle); } } @@ -36022,9 +36028,11 @@ package android.service.media { method public void dump(java.io.FileDescriptor, java.io.PrintWriter, java.lang.String[]); method public android.media.session.MediaSession.Token getSessionToken(); method public void notifyChildrenChanged(java.lang.String); + method public void notifyChildrenChanged(java.lang.String, android.os.Bundle); method public android.os.IBinder onBind(android.content.Intent); method public abstract android.service.media.MediaBrowserService.BrowserRoot onGetRoot(java.lang.String, int, android.os.Bundle); method public abstract void onLoadChildren(java.lang.String, android.service.media.MediaBrowserService.Result<java.util.List<android.media.browse.MediaBrowser.MediaItem>>); + method public void onLoadChildren(java.lang.String, android.service.media.MediaBrowserService.Result<java.util.List<android.media.browse.MediaBrowser.MediaItem>>, android.os.Bundle); method public void onLoadItem(java.lang.String, android.service.media.MediaBrowserService.Result<android.media.browse.MediaBrowser.MediaItem>); 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 52c5b5a8f90c..fab1f045f529 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -21754,7 +21754,11 @@ package android.media.browse { method public android.media.session.MediaSession.Token getSessionToken(); method public boolean isConnected(); 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); + method public void unsubscribe(java.lang.String, android.os.Bundle); + field public static final java.lang.String EXTRA_PAGE = "android.media.browse.extra.PAGE"; + field public static final java.lang.String EXTRA_PAGE_SIZE = "android.media.browse.extra.PAGE_SIZE"; } public static class MediaBrowser.ConnectionCallback { @@ -21787,7 +21791,9 @@ package android.media.browse { public static abstract class MediaBrowser.SubscriptionCallback { ctor public MediaBrowser.SubscriptionCallback(); method public void onChildrenLoaded(java.lang.String, java.util.List<android.media.browse.MediaBrowser.MediaItem>); + method public void onChildrenLoaded(java.lang.String, java.util.List<android.media.browse.MediaBrowser.MediaItem>, android.os.Bundle); method public void onError(java.lang.String); + method public void onError(java.lang.String, android.os.Bundle); } } @@ -33814,9 +33820,11 @@ package android.service.media { method public void dump(java.io.FileDescriptor, java.io.PrintWriter, java.lang.String[]); method public android.media.session.MediaSession.Token getSessionToken(); method public void notifyChildrenChanged(java.lang.String); + method public void notifyChildrenChanged(java.lang.String, android.os.Bundle); method public android.os.IBinder onBind(android.content.Intent); method public abstract android.service.media.MediaBrowserService.BrowserRoot onGetRoot(java.lang.String, int, android.os.Bundle); method public abstract void onLoadChildren(java.lang.String, android.service.media.MediaBrowserService.Result<java.util.List<android.media.browse.MediaBrowser.MediaItem>>); + method public void onLoadChildren(java.lang.String, android.service.media.MediaBrowserService.Result<java.util.List<android.media.browse.MediaBrowser.MediaItem>>, android.os.Bundle); method public void onLoadItem(java.lang.String, android.service.media.MediaBrowserService.Result<android.media.browse.MediaBrowser.MediaItem>); 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 4ca89a8b6ec8..869512dd254c 100644 --- a/media/java/android/media/browse/MediaBrowser.java +++ b/media/java/android/media/browse/MediaBrowser.java @@ -34,9 +34,9 @@ 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; +import android.service.media.MediaBrowserService; import android.text.TextUtils; import android.util.ArrayMap; import android.util.Log; @@ -44,7 +44,9 @@ import android.util.Log; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.ref.WeakReference; +import java.util.ArrayList; import java.util.List; +import java.util.Map.Entry; /** * Browses media content offered by a link MediaBrowserService. @@ -52,11 +54,39 @@ import java.util.List; * This object is not thread-safe. All calls should happen on the thread on which the browser * was constructed. * </p> + * <h3>Standard Extra Data</h3> + * + * <p>These are the current standard fields that can be used as extra data via + * {@link #subscribe(String, Bundle, SubscriptionCallback)}, {@link #unsubscribe(String, Bundle)}, + * and {@link SubscriptionCallback#onChildrenLoaded(String, List, Bundle)}. + * + * <ul> + * <li> {@link #EXTRA_PAGE} + * <li> {@link #EXTRA_PAGE_SIZE} + * </ul> */ public final class MediaBrowser { private static final String TAG = "MediaBrowser"; private static final boolean DBG = false; + /** + * Used as an int extra field to denote the page number to subscribe. + * The value of {@code EXTRA_PAGE} should be greater than or equal to 1. + * + * @see android.service.media.MediaBrowserService.BrowserRoot + * @see #EXTRA_PAGE_SIZE + */ + public static final String EXTRA_PAGE = "android.media.browse.extra.PAGE"; + + /** + * Used as an int extra field to denote the number of media items in a page. + * The value of {@code EXTRA_PAGE_SIZE} should be greater than or equal to 1. + * + * @see android.service.media.MediaBrowserService.BrowserRoot + * @see #EXTRA_PAGE + */ + public static final String EXTRA_PAGE_SIZE = "android.media.browse.extra.PAGE_SIZE"; + private static final int CONNECT_STATE_DISCONNECTED = 0; private static final int CONNECT_STATE_CONNECTING = 1; private static final int CONNECT_STATE_CONNECTED = 2; @@ -67,8 +97,7 @@ public final class MediaBrowser { private final ConnectionCallback mCallback; private final Bundle mRootHints; private final Handler mHandler = new Handler(); - private final ArrayMap<String,Subscription> mSubscriptions = - new ArrayMap<String, MediaBrowser.Subscription>(); + private final ArrayMap<String, Subscription> mSubscriptions = new ArrayMap<>(); private int mState = CONNECT_STATE_DISCONNECTED; private MediaServiceConnection mServiceConnection; @@ -291,7 +320,7 @@ public final class MediaBrowser { * the specified id and subscribes to receive updates when they change. * <p> * The list of subscriptions is maintained even when not connected and is - * restored after reconnection. It is ok to subscribe while not connected + * restored after the reconnection. It is ok to subscribe while not connected * but the results will not be returned until the connection completes. * </p> * <p> @@ -305,34 +334,37 @@ public final class MediaBrowser { * @param callback The callback to receive the list of children. */ public void subscribe(@NonNull String parentId, @NonNull SubscriptionCallback callback) { - // Check arguments. - if (parentId == null) { - throw new IllegalArgumentException("parentId is null"); - } - if (callback == null) { - throw new IllegalArgumentException("callback is null"); - } - - // Update or create the subscription. - Subscription sub = mSubscriptions.get(parentId); - boolean newSubscription = sub == null; - if (newSubscription) { - sub = new Subscription(parentId); - mSubscriptions.put(parentId, sub); - } - sub.callback = callback; + subscribeInternal(parentId, null, callback); + } - // If we are connected, tell the service that we are watching. If we aren't - // connected, the service will be told when we connect. - if (mState == CONNECT_STATE_CONNECTED) { - try { - mServiceBinder.addSubscription(parentId, mServiceCallbacks); - } catch (RemoteException ex) { - // Process is crashing. We will disconnect, and upon reconnect we will - // automatically reregister. So nothing to do here. - Log.d(TAG, "addSubscription failed with RemoteException parentId=" + parentId); - } + /** + * Queries with service-specific arguments for information about the media items + * that are contained within the specified id and subscribes to receive updates + * when they change. + * <p> + * The list of subscriptions is maintained even when not connected and is + * restored after the reconnection. It is ok to subscribe while not connected + * but the results will not be returned until the connection completes. + * </p> + * <p> + * If the id is already subscribed with a different callback then the new + * callback will replace the previous one and the child data will be + * reloaded. + * </p> + * + * @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 + * browse service. The contents of this bundle may affect the + * information returned when browsing. + * @param callback The callback to receive the list of children. + */ + public void subscribe(@NonNull String parentId, @NonNull Bundle options, + @NonNull SubscriptionCallback callback) { + if (options == null) { + throw new IllegalArgumentException("options are null"); } + subscribeInternal(parentId, options, callback); } /** @@ -343,27 +375,28 @@ public final class MediaBrowser { * </p> * * @param parentId The id of the parent media item whose list of children - * will be unsubscribed. + * will be unsubscribed. */ public void unsubscribe(@NonNull String parentId) { - // Check arguments. - if (TextUtils.isEmpty(parentId)) { - throw new IllegalArgumentException("parentId is empty."); - } - - // Remove from our list. - final Subscription sub = mSubscriptions.remove(parentId); + unsubscribeInternal(parentId, null); + } - // Tell the service if necessary. - if (mState == CONNECT_STATE_CONNECTED && sub != null) { - try { - mServiceBinder.removeSubscription(parentId, mServiceCallbacks); - } catch (RemoteException ex) { - // Process is crashing. We will disconnect, and upon reconnect we will - // automatically reregister. So nothing to do here. - Log.d(TAG, "removeSubscription failed with RemoteException parentId=" + parentId); - } + /** + * Unsubscribes for changes to the children of the specified media id. + * <p> + * The query callback will no longer be invoked for results associated with + * this id once this method returns. + * </p> + * + * @param parentId The id of the parent media item whose list of children + * will be unsubscribed. + * @param options A bundle sent to the media browse service to subscribe. + */ + public void unsubscribe(@NonNull String parentId, @NonNull Bundle options) { + if (options == null) { + throw new IllegalArgumentException("options are null"); } + unsubscribeInternal(parentId, options); } /** @@ -420,6 +453,73 @@ public final class MediaBrowser { } } + private void subscribeInternal(String parentId, Bundle options, SubscriptionCallback callback) { + // Check arguments. + if (parentId == null) { + throw new IllegalArgumentException("parentId is null"); + } + if (callback == null) { + throw new IllegalArgumentException("callback is null"); + } + // Update or create the subscription. + Subscription sub = mSubscriptions.get(parentId); + if (sub == null) { + sub = new Subscription(); + mSubscriptions.put(parentId, sub); + } + sub.add(callback, options); + + // If we are connected, tell the service that we are watching. If we aren't connected, + // the service will be told when we connect. + if (mState == CONNECT_STATE_CONNECTED) { + try { + // NOTE: In order not to break the behavior of the support library, call + // addSubscription instead of addSubscriptionWithOptions when the options are null. + if (options == null) { + mServiceBinder.addSubscription(parentId, mServiceCallbacks); + } else { + mServiceBinder.addSubscriptionWithOptions(parentId, options, mServiceCallbacks); + } + } catch (RemoteException ex) { + // Process is crashing. We will disconnect, and upon reconnect we will + // automatically reregister. So nothing to do here. + Log.d(TAG, "addSubscription failed with RemoteException parentId=" + parentId); + } + } + } + + private void unsubscribeInternal(String parentId, Bundle options) { + // Check arguments. + if (TextUtils.isEmpty(parentId)) { + throw new IllegalArgumentException("parentId is empty."); + } + + // Remove from our list. + Subscription sub = mSubscriptions.get(parentId); + + // Tell the service if necessary. + if (sub != null && sub.remove(options) && mState == CONNECT_STATE_CONNECTED) { + try { + // NOTE: In order not to break the behavior of the support library, call + // removeSubscription instead of removeSubscriptionWithOptions when the options + // are null. + if (options == null) { + mServiceBinder.removeSubscription(parentId, mServiceCallbacks); + } else { + mServiceBinder.removeSubscriptionWithOptions( + parentId, options, mServiceCallbacks); + } + } catch (RemoteException ex) { + // Process is crashing. We will disconnect, and upon reconnect we will + // automatically reregister. So nothing to do here. + Log.d(TAG, "removeSubscription failed with RemoteException parentId=" + parentId); + } + } + if (sub != null && sub.isEmpty()) { + mSubscriptions.remove(parentId); + } + } + /** * For debugging. */ @@ -467,13 +567,26 @@ public final class MediaBrowser { // we may receive some subscriptions before we are connected, so re-subscribe // everything now - for (String id : mSubscriptions.keySet()) { - try { - mServiceBinder.addSubscription(id, mServiceCallbacks); - } catch (RemoteException ex) { - // Process is crashing. We will disconnect, and upon reconnect we will - // automatically reregister. So nothing to do here. - Log.d(TAG, "addSubscription failed with RemoteException parentId=" + id); + for (Entry<String, Subscription> subscriptionEntry : mSubscriptions.entrySet()) { + String id = subscriptionEntry.getKey(); + Subscription sub = subscriptionEntry.getValue(); + for (Bundle options : sub.getOptionsList()) { + try { + // NOTE: In order not to break the behavior of the support library, + // call addSubscription instead of addSubscriptionWithOptions when + // the options are null. + if (options == null) { + mServiceBinder.addSubscription(id, mServiceCallbacks); + } else { + mServiceBinder.addSubscriptionWithOptions( + id, options, mServiceCallbacks); + } + } catch (RemoteException ex) { + // Process is crashing. We will disconnect, and upon reconnect we will + // automatically reregister. So nothing to do here. + Log.d(TAG, "addSubscription failed with RemoteException parentId=" + + id); + } } } } @@ -508,7 +621,7 @@ public final class MediaBrowser { } private final void onLoadChildren(final IMediaBrowserServiceCallbacks callback, - final String parentId, final ParceledListSlice list) { + final String parentId, final ParceledListSlice list, final Bundle options) { mHandler.post(new Runnable() { @Override public void run() { @@ -525,16 +638,21 @@ public final class MediaBrowser { // Check that the subscription is still subscribed. final Subscription subscription = mSubscriptions.get(parentId); - if (subscription == null) { - if (DBG) { - Log.d(TAG, "onLoadChildren for id that isn't subscribed id=" - + parentId); + if (subscription != null) { + // Tell the app. + SubscriptionCallback subscriptionCallback = subscription.getCallback(options); + if (subscriptionCallback != null) { + if (options == null) { + subscriptionCallback.onChildrenLoaded(parentId, data); + } else { + subscriptionCallback.onChildrenLoaded(parentId, data, options); + } + return; } - return; } - - // Tell the app. - subscription.callback.onChildrenLoaded(parentId, data); + if (DBG) { + Log.d(TAG, "onLoadChildren for id that isn't subscribed id=" + parentId); + } } }); } @@ -697,7 +815,6 @@ public final class MediaBrowser { } } - /** * Callbacks for connection related events. */ @@ -735,6 +852,19 @@ public final class MediaBrowser { } /** + * Called when the list of children is loaded or updated. + * + * @param parentId The media id of the parent media item. + * @param children The children which were loaded, or null if the id is invalid. + * @param options A bundle of service-specific arguments to send to the media + * browse service. The contents of this bundle may affect the + * information returned when browsing. + */ + public void onChildrenLoaded(@NonNull String parentId, List<MediaItem> children, + @NonNull Bundle options) { + } + + /** * Called when the id doesn't exist or other errors in subscribing. * <p> * If this is called, the subscription remains until {@link MediaBrowser#unsubscribe} @@ -742,10 +872,25 @@ public final class MediaBrowser { * </p> * * @param parentId The media id of the parent media item whose children could - * not be loaded. + * not be loaded. */ public void onError(@NonNull String parentId) { } + + /** + * Called when the id doesn't exist or other errors in subscribing. + * <p> + * If this is called, the subscription remains until {@link MediaBrowser#unsubscribe} + * called, because some errors may heal themselves. + * </p> + * + * @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. + */ + public void onError(@NonNull String parentId, @NonNull Bundle options) { + } } /** @@ -909,20 +1054,65 @@ public final class MediaBrowser { } @Override - public void onLoadChildren(final String parentId, final ParceledListSlice list) { + public void onLoadChildren(final String parentId, final ParceledListSlice list, + final Bundle options) { MediaBrowser mediaBrowser = mMediaBrowser.get(); if (mediaBrowser != null) { - mediaBrowser.onLoadChildren(this, parentId, list); + mediaBrowser.onLoadChildren(this, parentId, list, options); } } } private static class Subscription { - final String id; - SubscriptionCallback callback; + private final List<SubscriptionCallback> mCallbacks; + private final List<Bundle> mOptionsList; - Subscription(String id) { - this.id = id; + public Subscription() { + mCallbacks = new ArrayList<>(); + mOptionsList = new ArrayList<>(); + } + + public boolean isEmpty() { + return mCallbacks.isEmpty(); + } + + public List<Bundle> getOptionsList() { + return mOptionsList; + } + + public List<SubscriptionCallback> getCallbacks() { + return mCallbacks; + } + + public void add(SubscriptionCallback callback, Bundle options) { + for (int i = 0; i < mOptionsList.size(); ++i) { + if (MediaBrowserUtils.areSameOptions(mOptionsList.get(i), options)) { + mCallbacks.set(i, callback); + return; + } + } + mCallbacks.add(callback); + mOptionsList.add(options); + } + + public boolean remove(Bundle options) { + for (int i = 0; i < mOptionsList.size(); ++i) { + if (MediaBrowserUtils.areSameOptions(mOptionsList.get(i), options)) { + mCallbacks.remove(i); + mOptionsList.remove(i); + return true; + } + } + return false; + } + + public SubscriptionCallback getCallback(Bundle options) { + for (int i = 0; i < mOptionsList.size(); ++i) { + if (MediaBrowserUtils.areSameOptions(mOptionsList.get(i), options)) { + return mCallbacks.get(i); + } + } + return null; } } } diff --git a/media/java/android/media/browse/MediaBrowserUtils.java b/media/java/android/media/browse/MediaBrowserUtils.java new file mode 100644 index 000000000000..4f198aca96ab --- /dev/null +++ b/media/java/android/media/browse/MediaBrowserUtils.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2016 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 android.media.browse; + +import android.os.Bundle; + +/** + * @hide + */ +public class MediaBrowserUtils { + public static boolean areSameOptions(Bundle options1, Bundle options2) { + if (options1 == options2) { + return true; + } else if (options1 == null) { + return options2.getInt(MediaBrowser.EXTRA_PAGE, -1) == -1 + && options2.getInt(MediaBrowser.EXTRA_PAGE_SIZE, -1) == -1; + } else if (options2 == null) { + return options1.getInt(MediaBrowser.EXTRA_PAGE, -1) == -1 + && options1.getInt(MediaBrowser.EXTRA_PAGE_SIZE, -1) == -1; + } else { + return options1.getInt(MediaBrowser.EXTRA_PAGE, -1) + == options2.getInt(MediaBrowser.EXTRA_PAGE, -1) + && options1.getInt(MediaBrowser.EXTRA_PAGE_SIZE, -1) + == options2.getInt(MediaBrowser.EXTRA_PAGE_SIZE, -1); + } + } + + public static boolean hasDuplicatedItems(Bundle options1, Bundle options2) { + int page1 = options1.getInt(MediaBrowser.EXTRA_PAGE, -1); + int page2 = options2.getInt(MediaBrowser.EXTRA_PAGE, -1); + int pageSize1 = options1.getInt(MediaBrowser.EXTRA_PAGE_SIZE, -1); + int pageSize2 = options2.getInt(MediaBrowser.EXTRA_PAGE_SIZE, -1); + + int startIndex1, startIndex2, endIndex1, endIndex2; + if (page1 == -1 || pageSize1 == -1) { + startIndex1 = 0; + endIndex1 = Integer.MAX_VALUE; + } else { + startIndex1 = pageSize1 * (page1 - 1); + endIndex1 = startIndex1 + pageSize1 - 1; + } + + if (page2 == -1 || pageSize2 == -1) { + startIndex2 = 0; + endIndex2 = Integer.MAX_VALUE; + } else { + startIndex2 = pageSize2 * (page2 - 1); + endIndex2 = startIndex2 + pageSize2 - 1; + } + + if (startIndex1 <= startIndex2 && startIndex2 <= endIndex1) { + return true; + } else if (startIndex1 <= endIndex2 && endIndex2 <= endIndex1) { + return true; + } + return false; + } +} diff --git a/media/java/android/service/media/IMediaBrowserService.aidl b/media/java/android/service/media/IMediaBrowserService.aidl index f01fc076badf..fe7ebfa95423 100644 --- a/media/java/android/service/media/IMediaBrowserService.aidl +++ b/media/java/android/service/media/IMediaBrowserService.aidl @@ -14,10 +14,19 @@ import android.os.ResultReceiver; * @hide */ oneway interface IMediaBrowserService { + + // Warning: DO NOT CHANGE the methods signature and order of methods. + // The change of the order or the method signatures could break the support library. + void connect(String pkg, in Bundle rootHints, IMediaBrowserServiceCallbacks callbacks); void disconnect(IMediaBrowserServiceCallbacks callbacks); 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 + + void addSubscriptionWithOptions(String uri, in Bundle options, + IMediaBrowserServiceCallbacks callbacks); + void removeSubscriptionWithOptions(String uri, in Bundle options, + IMediaBrowserServiceCallbacks callbacks); +} diff --git a/media/java/android/service/media/IMediaBrowserServiceCallbacks.aidl b/media/java/android/service/media/IMediaBrowserServiceCallbacks.aidl index 2a37ada470ff..dadb025060b7 100644 --- a/media/java/android/service/media/IMediaBrowserServiceCallbacks.aidl +++ b/media/java/android/service/media/IMediaBrowserServiceCallbacks.aidl @@ -22,5 +22,5 @@ oneway interface IMediaBrowserServiceCallbacks { */ void onConnect(String root, in MediaSession.Token session, in Bundle extras); void onConnectFailed(); - void onLoadChildren(String mediaId, in ParceledListSlice list); + void onLoadChildren(String mediaId, in ParceledListSlice list, in Bundle options); } diff --git a/media/java/android/service/media/MediaBrowserService.java b/media/java/android/service/media/MediaBrowserService.java index 8edccacfda09..6cf90d594dce 100644 --- a/media/java/android/service/media/MediaBrowserService.java +++ b/media/java/android/service/media/MediaBrowserService.java @@ -25,11 +25,12 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ParceledListSlice; import android.media.browse.MediaBrowser; +import android.media.browse.MediaBrowserUtils; import android.media.session.MediaSession; import android.os.Binder; import android.os.Bundle; -import android.os.IBinder; import android.os.Handler; +import android.os.IBinder; import android.os.RemoteException; import android.os.ResultReceiver; import android.service.media.IMediaBrowserService; @@ -40,7 +41,8 @@ import android.util.Log; import java.io.FileDescriptor; import java.io.PrintWriter; -import java.util.HashSet; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; /** @@ -82,7 +84,9 @@ public abstract class MediaBrowserService extends Service { */ public static final String KEY_MEDIA_ITEM = "media_item"; - private final ArrayMap<IBinder, ConnectionRecord> mConnections = new ArrayMap(); + private static final int RESULT_FLAG_OPTION_NOT_HANDLED = 0x00000001; + + private final ArrayMap<IBinder, ConnectionRecord> mConnections = new ArrayMap<>(); private final Handler mHandler = new Handler(); private ServiceBinder mBinder; MediaSession.Token mSession; @@ -95,7 +99,7 @@ public abstract class MediaBrowserService extends Service { Bundle rootHints; IMediaBrowserServiceCallbacks callbacks; BrowserRoot root; - HashSet<String> subscriptions = new HashSet(); + HashMap<String, List<Bundle>> subscriptions = new HashMap<>(); } /** @@ -115,6 +119,7 @@ public abstract class MediaBrowserService extends Service { private Object mDebug; private boolean mDetachCalled; private boolean mSendResultCalled; + private int mFlag; Result(Object debug) { mDebug = debug; @@ -128,7 +133,7 @@ public abstract class MediaBrowserService extends Service { throw new IllegalStateException("sendResult() called twice for: " + mDebug); } mSendResultCalled = true; - onResultSent(result); + onResultSent(result, mFlag); } /** @@ -151,11 +156,15 @@ public abstract class MediaBrowserService extends Service { return mDetachCalled || mSendResultCalled; } + void setFlag(int flag) { + mFlag = flag; + } + /** * Called when the result is sent, after assertions about not being called twice * have happened. */ - void onResultSent(T result) { + void onResultSent(T result, int flag) { } } @@ -228,9 +237,15 @@ public abstract class MediaBrowserService extends Service { }); } + @Override + public void addSubscription(final String id, + final IMediaBrowserServiceCallbacks callbacks) { + addSubscriptionWithOptions(id, null, callbacks); + } @Override - public void addSubscription(final String id, final IMediaBrowserServiceCallbacks callbacks) { + public void addSubscriptionWithOptions(final String id, final Bundle options, + final IMediaBrowserServiceCallbacks callbacks) { mHandler.post(new Runnable() { @Override public void run() { @@ -244,7 +259,7 @@ public abstract class MediaBrowserService extends Service { return; } - MediaBrowserService.this.addSubscription(id, connection); + MediaBrowserService.this.addSubscription(id, connection, options); } }); } @@ -252,6 +267,12 @@ public abstract class MediaBrowserService extends Service { @Override public void removeSubscription(final String id, final IMediaBrowserServiceCallbacks callbacks) { + removeSubscriptionWithOptions(id, null, callbacks); + } + + @Override + public void removeSubscriptionWithOptions(final String id, final Bundle options, + final IMediaBrowserServiceCallbacks callbacks) { mHandler.post(new Runnable() { @Override public void run() { @@ -263,7 +284,7 @@ public abstract class MediaBrowserService extends Service { + id); return; } - if (!connection.subscriptions.remove(id)) { + if (!MediaBrowserService.this.removeSubscription(id, connection, options)) { Log.w(TAG, "removeSubscription called for " + id + " which is not subscribed"); } @@ -345,6 +366,33 @@ public abstract class MediaBrowserService extends Service { @NonNull Result<List<MediaBrowser.MediaItem>> result); /** + * Called to get information about the children of a media item. + * <p> + * 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. + * @param result The Result to send the list of children to, or null if the + * id is invalid. + * @param options A bundle of service-specific arguments sent from the media + * browse. The information returned through the result should be + * affected by the contents of this bundle. + */ + public void onLoadChildren(@NonNull String parentId, + @NonNull Result<List<MediaBrowser.MediaItem>> result, @NonNull Bundle options) { + // To support backward compatibility, when the implementation of MediaBrowserService doesn't + // override onLoadChildren() with options, onLoadChildren() without options will be used + // instead, and the options will be applied in the implementation of result.onResultSent(). + result.setFlag(RESULT_FLAG_OPTION_NOT_HANDLED); + onLoadChildren(parentId, result); + } + + /** * Called to get information about a specific media item. * <p> * Implementations must call {@link Result#sendResult result.sendResult}. If @@ -413,7 +461,29 @@ public abstract class MediaBrowserService extends Service { * @param parentId The id of the parent media item whose * children changed. */ - public void notifyChildrenChanged(@NonNull final String parentId) { + public void notifyChildrenChanged(@NonNull String parentId) { + notifyChildrenChangedInternal(parentId, null); + } + + /** + * Notifies all connected media browsers that the children of + * the specified parent id have changed in some way. + * This will cause browsers to fetch subscribed content again. + * + * @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 + * contain the information about the change. + */ + public void notifyChildrenChanged(@NonNull String parentId, @NonNull Bundle options) { + if (options == null) { + throw new IllegalArgumentException("options cannot be null in notifyChildrenChanged"); + } + notifyChildrenChangedInternal(parentId, options); + } + + private void notifyChildrenChangedInternal(final String parentId, final Bundle options) { if (parentId == null) { throw new IllegalArgumentException("parentId cannot be null in notifyChildrenChanged"); } @@ -422,8 +492,13 @@ public abstract class MediaBrowserService extends Service { public void run() { for (IBinder binder : mConnections.keySet()) { ConnectionRecord connection = mConnections.get(binder); - if (connection.subscriptions.contains(parentId)) { - performLoadChildren(parentId, connection); + List<Bundle> optionsList = connection.subscriptions.get(parentId); + if (optionsList != null) { + for (Bundle bundle : optionsList) { + if (MediaBrowserUtils.hasDuplicatedItems(options, bundle)) { + performLoadChildren(parentId, connection, bundle); + } + } } } } @@ -451,12 +526,42 @@ public abstract class MediaBrowserService extends Service { /** * Save the subscription and if it is a new subscription send the results. */ - private void addSubscription(String id, ConnectionRecord connection) { + private void addSubscription(String id, ConnectionRecord connection, Bundle options) { // Save the subscription - connection.subscriptions.add(id); - + List<Bundle> optionsList = connection.subscriptions.get(id); + if (optionsList == null) { + optionsList = new ArrayList<>(); + } + for (Bundle bundle : optionsList) { + if (MediaBrowserUtils.areSameOptions(options, bundle)) { + return; + } + } + optionsList.add(options); + connection.subscriptions.put(id, optionsList); // send the results - performLoadChildren(id, connection); + performLoadChildren(id, connection, options); + } + + /** + * Remove the subscription. + */ + private boolean removeSubscription(String id, ConnectionRecord connection, Bundle options) { + boolean removed = false; + List<Bundle> optionsList = connection.subscriptions.get(id); + if (optionsList != null) { + for (Bundle bundle : optionsList) { + if (MediaBrowserUtils.areSameOptions(options, bundle)) { + removed = true; + optionsList.remove(bundle); + break; + } + } + if (optionsList.size() == 0) { + connection.subscriptions.remove(id); + } + } + return removed; } /** @@ -464,11 +569,12 @@ public abstract class MediaBrowserService extends Service { * <p> * Callers must make sure that this connection is still connected. */ - private void performLoadChildren(final String parentId, final ConnectionRecord connection) { + private void performLoadChildren(final String parentId, final ConnectionRecord connection, + final Bundle options) { final Result<List<MediaBrowser.MediaItem>> result = new Result<List<MediaBrowser.MediaItem>>(parentId) { @Override - void onResultSent(List<MediaBrowser.MediaItem> list) { + void onResultSent(List<MediaBrowser.MediaItem> list, int flag) { if (mConnections.get(connection.callbacks.asBinder()) != connection) { if (DBG) { Log.d(TAG, "Not sending onLoadChildren result for connection that has" @@ -477,10 +583,13 @@ public abstract class MediaBrowserService extends Service { return; } + List<MediaBrowser.MediaItem> filteredList = + (flag & RESULT_FLAG_OPTION_NOT_HANDLED) != 0 + ? applyOptions(list, options) : list; final ParceledListSlice<MediaBrowser.MediaItem> pls = - list == null ? null : new ParceledListSlice(list); + filteredList == null ? null : new ParceledListSlice<>(filteredList); try { - connection.callbacks.onLoadChildren(parentId, pls); + connection.callbacks.onLoadChildren(parentId, pls, options); } catch (RemoteException ex) { // The other side is in the process of crashing. Log.w(TAG, "Calling onLoadChildren() failed for id=" + parentId @@ -489,7 +598,11 @@ public abstract class MediaBrowserService extends Service { } }; - onLoadChildren(parentId, result); + if (options == null) { + onLoadChildren(parentId, result); + } else { + onLoadChildren(parentId, result, options); + } if (!result.isDone()) { throw new IllegalStateException("onLoadChildren must call detach() or sendResult()" @@ -497,11 +610,29 @@ public abstract class MediaBrowserService extends Service { } } + private List<MediaBrowser.MediaItem> applyOptions(List<MediaBrowser.MediaItem> list, + final Bundle options) { + int page = options.getInt(MediaBrowser.EXTRA_PAGE, -1); + int pageSize = options.getInt(MediaBrowser.EXTRA_PAGE_SIZE, -1); + if (page == -1 && pageSize == -1) { + return list; + } + int fromIndex = pageSize * (page - 1); + int toIndex = fromIndex + pageSize; + if (page < 1 || pageSize < 1 || fromIndex >= list.size()) { + return null; + } + if (toIndex > list.size()) { + toIndex = list.size(); + } + return list.subList(fromIndex, toIndex); + } + private void performLoadItem(String itemId, final ResultReceiver receiver) { final Result<MediaBrowser.MediaItem> result = new Result<MediaBrowser.MediaItem>(itemId) { @Override - void onResultSent(MediaBrowser.MediaItem item) { + void onResultSent(MediaBrowser.MediaItem item, int flag) { Bundle bundle = new Bundle(); bundle.putParcelable(KEY_MEDIA_ITEM, item); receiver.send(0, bundle); |