Merge "MediaBrowser: Support pagination of child media items"

This commit is contained in:
Sungsoo Lim
2016-01-27 09:57:38 +00:00
committed by Android (Google) Code Review
8 changed files with 521 additions and 95 deletions

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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");
}
subscribeInternal(parentId, null, callback);
}
// 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;
// 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.");
}
unsubscribeInternal(parentId, null);
}
// Remove from our list.
final Subscription sub = mSubscriptions.remove(parentId);
// 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.
*/
@@ -734,6 +851,19 @@ public final class MediaBrowser {
public void onChildrenLoaded(@NonNull String parentId, List<MediaItem> children) {
}
/**
* 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>
@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
void addSubscriptionWithOptions(String uri, in Bundle options,
IMediaBrowserServiceCallbacks callbacks);
void removeSubscriptionWithOptions(String uri, in Bundle options,
IMediaBrowserServiceCallbacks callbacks);
}

View File

@@ -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);
}

View File

@@ -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");
}
@@ -344,6 +365,33 @@ public abstract class MediaBrowserService extends Service {
public abstract void onLoadChildren(@NonNull String parentId,
@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>
@@ -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);