Merge "MediaBrowser: Support pagination of child media items"
This commit is contained in:
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
72
media/java/android/media/browse/MediaBrowserUtils.java
Normal file
72
media/java/android/media/browse/MediaBrowserUtils.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user