1199 lines
44 KiB
Java
1199 lines
44 KiB
Java
![]() |
/*
|
||
|
* Copyright (C) 2014 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.annotation.IntDef;
|
||
|
import android.annotation.NonNull;
|
||
|
import android.annotation.Nullable;
|
||
|
import android.content.ComponentName;
|
||
|
import android.content.Context;
|
||
|
import android.content.Intent;
|
||
|
import android.content.ServiceConnection;
|
||
|
import android.content.pm.ParceledListSlice;
|
||
|
import android.media.MediaDescription;
|
||
|
import android.media.session.MediaSession;
|
||
|
import android.os.Binder;
|
||
|
import android.os.Bundle;
|
||
|
import android.os.Handler;
|
||
|
import android.os.IBinder;
|
||
|
import android.os.Parcel;
|
||
|
import android.os.Parcelable;
|
||
|
import android.os.RemoteException;
|
||
|
import android.os.ResultReceiver;
|
||
|
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;
|
||
|
|
||
|
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.
|
||
|
* <p>
|
||
|
* 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, SubscriptionCallback)}, 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 0.
|
||
|
*
|
||
|
* @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 #EXTRA_PAGE
|
||
|
*/
|
||
|
public static final String EXTRA_PAGE_SIZE = "android.media.browse.extra.PAGE_SIZE";
|
||
|
|
||
|
private static final int CONNECT_STATE_DISCONNECTING = 0;
|
||
|
private static final int CONNECT_STATE_DISCONNECTED = 1;
|
||
|
private static final int CONNECT_STATE_CONNECTING = 2;
|
||
|
private static final int CONNECT_STATE_CONNECTED = 3;
|
||
|
private static final int CONNECT_STATE_SUSPENDED = 4;
|
||
|
|
||
|
private final Context mContext;
|
||
|
private final ComponentName mServiceComponent;
|
||
|
private final ConnectionCallback mCallback;
|
||
|
private final Bundle mRootHints;
|
||
|
private final Handler mHandler = new Handler();
|
||
|
private final ArrayMap<String, Subscription> mSubscriptions = new ArrayMap<>();
|
||
|
|
||
|
private volatile int mState = CONNECT_STATE_DISCONNECTED;
|
||
|
private volatile String mRootId;
|
||
|
private volatile MediaSession.Token mMediaSessionToken;
|
||
|
private volatile Bundle mExtras;
|
||
|
|
||
|
private MediaServiceConnection mServiceConnection;
|
||
|
private IMediaBrowserService mServiceBinder;
|
||
|
private IMediaBrowserServiceCallbacks mServiceCallbacks;
|
||
|
|
||
|
/**
|
||
|
* Creates a media browser for the specified media browser service.
|
||
|
*
|
||
|
* @param context The context.
|
||
|
* @param serviceComponent The component name of the media browser service.
|
||
|
* @param callback The connection callback.
|
||
|
* @param rootHints An optional bundle of service-specific arguments to send
|
||
|
* to the media browser service when connecting and retrieving the root id
|
||
|
* for browsing, or null if none. The contents of this bundle may affect
|
||
|
* the information returned when browsing.
|
||
|
* @see android.service.media.MediaBrowserService.BrowserRoot#EXTRA_RECENT
|
||
|
* @see android.service.media.MediaBrowserService.BrowserRoot#EXTRA_OFFLINE
|
||
|
* @see android.service.media.MediaBrowserService.BrowserRoot#EXTRA_SUGGESTED
|
||
|
*/
|
||
|
public MediaBrowser(Context context, ComponentName serviceComponent,
|
||
|
ConnectionCallback callback, Bundle rootHints) {
|
||
|
if (context == null) {
|
||
|
throw new IllegalArgumentException("context must not be null");
|
||
|
}
|
||
|
if (serviceComponent == null) {
|
||
|
throw new IllegalArgumentException("service component must not be null");
|
||
|
}
|
||
|
if (callback == null) {
|
||
|
throw new IllegalArgumentException("connection callback must not be null");
|
||
|
}
|
||
|
mContext = context;
|
||
|
mServiceComponent = serviceComponent;
|
||
|
mCallback = callback;
|
||
|
mRootHints = rootHints == null ? null : new Bundle(rootHints);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Connects to the media browser service.
|
||
|
* <p>
|
||
|
* The connection callback specified in the constructor will be invoked
|
||
|
* when the connection completes or fails.
|
||
|
* </p>
|
||
|
*/
|
||
|
public void connect() {
|
||
|
if (mState != CONNECT_STATE_DISCONNECTING && mState != CONNECT_STATE_DISCONNECTED) {
|
||
|
throw new IllegalStateException("connect() called while neither disconnecting nor "
|
||
|
+ "disconnected (state=" + getStateLabel(mState) + ")");
|
||
|
}
|
||
|
|
||
|
mState = CONNECT_STATE_CONNECTING;
|
||
|
mHandler.post(new Runnable() {
|
||
|
@Override
|
||
|
public void run() {
|
||
|
if (mState == CONNECT_STATE_DISCONNECTING) {
|
||
|
return;
|
||
|
}
|
||
|
mState = CONNECT_STATE_CONNECTING;
|
||
|
// TODO: remove this extra check.
|
||
|
if (DBG) {
|
||
|
if (mServiceConnection != null) {
|
||
|
throw new RuntimeException("mServiceConnection should be null. Instead it"
|
||
|
+ " is " + mServiceConnection);
|
||
|
}
|
||
|
}
|
||
|
if (mServiceBinder != null) {
|
||
|
throw new RuntimeException("mServiceBinder should be null. Instead it is "
|
||
|
+ mServiceBinder);
|
||
|
}
|
||
|
if (mServiceCallbacks != null) {
|
||
|
throw new RuntimeException("mServiceCallbacks should be null. Instead it is "
|
||
|
+ mServiceCallbacks);
|
||
|
}
|
||
|
|
||
|
final Intent intent = new Intent(MediaBrowserService.SERVICE_INTERFACE);
|
||
|
intent.setComponent(mServiceComponent);
|
||
|
|
||
|
mServiceConnection = new MediaServiceConnection();
|
||
|
|
||
|
boolean bound = false;
|
||
|
try {
|
||
|
bound = mContext.bindService(intent, mServiceConnection,
|
||
|
Context.BIND_AUTO_CREATE | Context.BIND_INCLUDE_CAPABILITIES);
|
||
|
} catch (Exception ex) {
|
||
|
Log.e(TAG, "Failed binding to service " + mServiceComponent);
|
||
|
}
|
||
|
|
||
|
if (!bound) {
|
||
|
// Tell them that it didn't work.
|
||
|
forceCloseConnection();
|
||
|
mCallback.onConnectionFailed();
|
||
|
}
|
||
|
|
||
|
if (DBG) {
|
||
|
Log.d(TAG, "connect...");
|
||
|
dump();
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Disconnects from the media browser service.
|
||
|
* After this, no more callbacks will be received.
|
||
|
*/
|
||
|
public void disconnect() {
|
||
|
// It's ok to call this any state, because allowing this lets apps not have
|
||
|
// to check isConnected() unnecessarily. They won't appreciate the extra
|
||
|
// assertions for this. We do everything we can here to go back to a valid state.
|
||
|
mState = CONNECT_STATE_DISCONNECTING;
|
||
|
mHandler.post(new Runnable() {
|
||
|
@Override
|
||
|
public void run() {
|
||
|
// connect() could be called before this. Then we will disconnect and reconnect.
|
||
|
if (mServiceCallbacks != null) {
|
||
|
try {
|
||
|
mServiceBinder.disconnect(mServiceCallbacks);
|
||
|
} catch (RemoteException ex) {
|
||
|
// We are disconnecting anyway. Log, just for posterity but it's not
|
||
|
// a big problem.
|
||
|
Log.w(TAG, "RemoteException during connect for " + mServiceComponent);
|
||
|
}
|
||
|
}
|
||
|
int state = mState;
|
||
|
forceCloseConnection();
|
||
|
// If the state was not CONNECT_STATE_DISCONNECTING, keep the state so that
|
||
|
// the operation came after disconnect() can be handled properly.
|
||
|
if (state != CONNECT_STATE_DISCONNECTING) {
|
||
|
mState = state;
|
||
|
}
|
||
|
if (DBG) {
|
||
|
Log.d(TAG, "disconnect...");
|
||
|
dump();
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Null out the variables and unbind from the service. This doesn't include
|
||
|
* calling disconnect on the service, because we only try to do that in the
|
||
|
* clean shutdown cases.
|
||
|
* <p>
|
||
|
* Everywhere that calls this EXCEPT for disconnect() should follow it with
|
||
|
* a call to mCallback.onConnectionFailed(). Disconnect doesn't do that callback
|
||
|
* for a clean shutdown, but everywhere else is a dirty shutdown and should
|
||
|
* notify the app.
|
||
|
* <p>
|
||
|
* Also, mState should be updated properly. Mostly it should be CONNECT_STATE_DIACONNECTED
|
||
|
* except for disconnect().
|
||
|
*/
|
||
|
private void forceCloseConnection() {
|
||
|
if (mServiceConnection != null) {
|
||
|
try {
|
||
|
mContext.unbindService(mServiceConnection);
|
||
|
} catch (IllegalArgumentException e) {
|
||
|
if (DBG) {
|
||
|
Log.d(TAG, "unbindService failed", e);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
mState = CONNECT_STATE_DISCONNECTED;
|
||
|
mServiceConnection = null;
|
||
|
mServiceBinder = null;
|
||
|
mServiceCallbacks = null;
|
||
|
mRootId = null;
|
||
|
mMediaSessionToken = null;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns whether the browser is connected to the service.
|
||
|
*/
|
||
|
public boolean isConnected() {
|
||
|
return mState == CONNECT_STATE_CONNECTED;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Gets the service component that the media browser is connected to.
|
||
|
*/
|
||
|
public @NonNull ComponentName getServiceComponent() {
|
||
|
if (!isConnected()) {
|
||
|
throw new IllegalStateException("getServiceComponent() called while not connected"
|
||
|
+ " (state=" + mState + ")");
|
||
|
}
|
||
|
return mServiceComponent;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Gets the root id.
|
||
|
* <p>
|
||
|
* Note that the root id may become invalid or change when the
|
||
|
* browser is disconnected.
|
||
|
* </p>
|
||
|
*
|
||
|
* @throws IllegalStateException if not connected.
|
||
|
*/
|
||
|
public @NonNull String getRoot() {
|
||
|
if (!isConnected()) {
|
||
|
throw new IllegalStateException("getRoot() called while not connected (state="
|
||
|
+ getStateLabel(mState) + ")");
|
||
|
}
|
||
|
return mRootId;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Gets any extras for the media service.
|
||
|
*
|
||
|
* @throws IllegalStateException if not connected.
|
||
|
*/
|
||
|
public @Nullable Bundle getExtras() {
|
||
|
if (!isConnected()) {
|
||
|
throw new IllegalStateException("getExtras() called while not connected (state="
|
||
|
+ getStateLabel(mState) + ")");
|
||
|
}
|
||
|
return mExtras;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Gets the media session token associated with the media browser.
|
||
|
* <p>
|
||
|
* Note that the session token may become invalid or change when the
|
||
|
* browser is disconnected.
|
||
|
* </p>
|
||
|
*
|
||
|
* @return The session token for the browser, never null.
|
||
|
*
|
||
|
* @throws IllegalStateException if not connected.
|
||
|
*/
|
||
|
public @NonNull MediaSession.Token getSessionToken() {
|
||
|
if (!isConnected()) {
|
||
|
throw new IllegalStateException("getSessionToken() called while not connected (state="
|
||
|
+ mState + ")");
|
||
|
}
|
||
|
return mMediaSessionToken;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Queries 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 callback The callback to receive the list of children.
|
||
|
*/
|
||
|
public void subscribe(@NonNull String parentId, @NonNull SubscriptionCallback callback) {
|
||
|
subscribeInternal(parentId, null, callback);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 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 The bundle of service-specific arguments to send to the media
|
||
|
* browser 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 cannot be null");
|
||
|
}
|
||
|
subscribeInternal(parentId, new Bundle(options), callback);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 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.
|
||
|
*/
|
||
|
public void unsubscribe(@NonNull String parentId) {
|
||
|
unsubscribeInternal(parentId, null);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Unsubscribes for changes to the children of the specified media id through a callback.
|
||
|
* <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 callback A callback sent to the media browser service to subscribe.
|
||
|
*/
|
||
|
public void unsubscribe(@NonNull String parentId, @NonNull SubscriptionCallback callback) {
|
||
|
if (callback == null) {
|
||
|
throw new IllegalArgumentException("callback cannot be null");
|
||
|
}
|
||
|
unsubscribeInternal(parentId, callback);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Retrieves a specific {@link MediaItem} from the connected service. Not
|
||
|
* all services may support this, so falling back to subscribing to the
|
||
|
* parent's id should be used when unavailable.
|
||
|
*
|
||
|
* @param mediaId The id of the item to retrieve.
|
||
|
* @param cb The callback to receive the result on.
|
||
|
*/
|
||
|
public void getItem(final @NonNull String mediaId, @NonNull final ItemCallback cb) {
|
||
|
if (TextUtils.isEmpty(mediaId)) {
|
||
|
throw new IllegalArgumentException("mediaId cannot be empty.");
|
||
|
}
|
||
|
if (cb == null) {
|
||
|
throw new IllegalArgumentException("cb cannot be null.");
|
||
|
}
|
||
|
if (mState != CONNECT_STATE_CONNECTED) {
|
||
|
Log.i(TAG, "Not connected, unable to retrieve the MediaItem.");
|
||
|
mHandler.post(new Runnable() {
|
||
|
@Override
|
||
|
public void run() {
|
||
|
cb.onError(mediaId);
|
||
|
}
|
||
|
});
|
||
|
return;
|
||
|
}
|
||
|
ResultReceiver receiver = new ResultReceiver(mHandler) {
|
||
|
@Override
|
||
|
protected void onReceiveResult(int resultCode, Bundle resultData) {
|
||
|
if (!isConnected()) {
|
||
|
return;
|
||
|
}
|
||
|
if (resultCode != 0 || resultData == null
|
||
|
|| !resultData.containsKey(MediaBrowserService.KEY_MEDIA_ITEM)) {
|
||
|
cb.onError(mediaId);
|
||
|
return;
|
||
|
}
|
||
|
Parcelable item = resultData.getParcelable(MediaBrowserService.KEY_MEDIA_ITEM);
|
||
|
if (item != null && !(item instanceof MediaItem)) {
|
||
|
cb.onError(mediaId);
|
||
|
return;
|
||
|
}
|
||
|
cb.onItemLoaded((MediaItem) item);
|
||
|
}
|
||
|
};
|
||
|
try {
|
||
|
mServiceBinder.getMediaItem(mediaId, receiver, mServiceCallbacks);
|
||
|
} catch (RemoteException e) {
|
||
|
Log.i(TAG, "Remote error getting media item.");
|
||
|
mHandler.post(new Runnable() {
|
||
|
@Override
|
||
|
public void run() {
|
||
|
cb.onError(mediaId);
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void subscribeInternal(String parentId, Bundle options, SubscriptionCallback callback) {
|
||
|
// Check arguments.
|
||
|
if (TextUtils.isEmpty(parentId)) {
|
||
|
throw new IllegalArgumentException("parentId cannot be empty.");
|
||
|
}
|
||
|
if (callback == null) {
|
||
|
throw new IllegalArgumentException("callback cannot be null");
|
||
|
}
|
||
|
// Update or create the subscription.
|
||
|
Subscription sub = mSubscriptions.get(parentId);
|
||
|
if (sub == null) {
|
||
|
sub = new Subscription();
|
||
|
mSubscriptions.put(parentId, sub);
|
||
|
}
|
||
|
sub.putCallback(mContext, options, 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 (isConnected()) {
|
||
|
try {
|
||
|
if (options == null) {
|
||
|
mServiceBinder.addSubscriptionDeprecated(parentId, mServiceCallbacks);
|
||
|
}
|
||
|
mServiceBinder.addSubscription(parentId, callback.mToken, 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, SubscriptionCallback callback) {
|
||
|
// Check arguments.
|
||
|
if (TextUtils.isEmpty(parentId)) {
|
||
|
throw new IllegalArgumentException("parentId cannot be empty.");
|
||
|
}
|
||
|
|
||
|
Subscription sub = mSubscriptions.get(parentId);
|
||
|
if (sub == null) {
|
||
|
return;
|
||
|
}
|
||
|
// Tell the service if necessary.
|
||
|
try {
|
||
|
if (callback == null) {
|
||
|
if (isConnected()) {
|
||
|
mServiceBinder.removeSubscriptionDeprecated(parentId, mServiceCallbacks);
|
||
|
mServiceBinder.removeSubscription(parentId, null, mServiceCallbacks);
|
||
|
}
|
||
|
} else {
|
||
|
final List<SubscriptionCallback> callbacks = sub.getCallbacks();
|
||
|
final List<Bundle> optionsList = sub.getOptionsList();
|
||
|
for (int i = callbacks.size() - 1; i >= 0; --i) {
|
||
|
if (callbacks.get(i) == callback) {
|
||
|
if (isConnected()) {
|
||
|
mServiceBinder.removeSubscription(
|
||
|
parentId, callback.mToken, mServiceCallbacks);
|
||
|
}
|
||
|
callbacks.remove(i);
|
||
|
optionsList.remove(i);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
} 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.isEmpty() || callback == null) {
|
||
|
mSubscriptions.remove(parentId);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* For debugging.
|
||
|
*/
|
||
|
private static String getStateLabel(int state) {
|
||
|
switch (state) {
|
||
|
case CONNECT_STATE_DISCONNECTING:
|
||
|
return "CONNECT_STATE_DISCONNECTING";
|
||
|
case CONNECT_STATE_DISCONNECTED:
|
||
|
return "CONNECT_STATE_DISCONNECTED";
|
||
|
case CONNECT_STATE_CONNECTING:
|
||
|
return "CONNECT_STATE_CONNECTING";
|
||
|
case CONNECT_STATE_CONNECTED:
|
||
|
return "CONNECT_STATE_CONNECTED";
|
||
|
case CONNECT_STATE_SUSPENDED:
|
||
|
return "CONNECT_STATE_SUSPENDED";
|
||
|
default:
|
||
|
return "UNKNOWN/" + state;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void onServiceConnected(final IMediaBrowserServiceCallbacks callback,
|
||
|
final String root, final MediaSession.Token session, final Bundle extra) {
|
||
|
mHandler.post(new Runnable() {
|
||
|
@Override
|
||
|
public void run() {
|
||
|
// Check to make sure there hasn't been a disconnect or a different
|
||
|
// ServiceConnection.
|
||
|
if (!isCurrent(callback, "onConnect")) {
|
||
|
return;
|
||
|
}
|
||
|
// Don't allow them to call us twice.
|
||
|
if (mState != CONNECT_STATE_CONNECTING) {
|
||
|
Log.w(TAG, "onConnect from service while mState="
|
||
|
+ getStateLabel(mState) + "... ignoring");
|
||
|
return;
|
||
|
}
|
||
|
mRootId = root;
|
||
|
mMediaSessionToken = session;
|
||
|
mExtras = extra;
|
||
|
mState = CONNECT_STATE_CONNECTED;
|
||
|
|
||
|
if (DBG) {
|
||
|
Log.d(TAG, "ServiceCallbacks.onConnect...");
|
||
|
dump();
|
||
|
}
|
||
|
mCallback.onConnected();
|
||
|
|
||
|
// we may receive some subscriptions before we are connected, so re-subscribe
|
||
|
// everything now
|
||
|
for (Entry<String, Subscription> subscriptionEntry : mSubscriptions.entrySet()) {
|
||
|
String id = subscriptionEntry.getKey();
|
||
|
Subscription sub = subscriptionEntry.getValue();
|
||
|
List<SubscriptionCallback> callbackList = sub.getCallbacks();
|
||
|
List<Bundle> optionsList = sub.getOptionsList();
|
||
|
for (int i = 0; i < callbackList.size(); ++i) {
|
||
|
try {
|
||
|
mServiceBinder.addSubscription(id, callbackList.get(i).mToken,
|
||
|
optionsList.get(i), 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);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
private void onConnectionFailed(final IMediaBrowserServiceCallbacks callback) {
|
||
|
mHandler.post(new Runnable() {
|
||
|
@Override
|
||
|
public void run() {
|
||
|
Log.e(TAG, "onConnectFailed for " + mServiceComponent);
|
||
|
|
||
|
// Check to make sure there hasn't been a disconnect or a different
|
||
|
// ServiceConnection.
|
||
|
if (!isCurrent(callback, "onConnectFailed")) {
|
||
|
return;
|
||
|
}
|
||
|
// Don't allow them to call us twice.
|
||
|
if (mState != CONNECT_STATE_CONNECTING) {
|
||
|
Log.w(TAG, "onConnect from service while mState="
|
||
|
+ getStateLabel(mState) + "... ignoring");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Clean up
|
||
|
forceCloseConnection();
|
||
|
|
||
|
// Tell the app.
|
||
|
mCallback.onConnectionFailed();
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
private void onLoadChildren(final IMediaBrowserServiceCallbacks callback,
|
||
|
final String parentId, final ParceledListSlice list, final Bundle options) {
|
||
|
mHandler.post(new Runnable() {
|
||
|
@Override
|
||
|
public void run() {
|
||
|
// Check that there hasn't been a disconnect or a different
|
||
|
// ServiceConnection.
|
||
|
if (!isCurrent(callback, "onLoadChildren")) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (DBG) {
|
||
|
Log.d(TAG, "onLoadChildren for " + mServiceComponent + " id=" + parentId);
|
||
|
}
|
||
|
|
||
|
// Check that the subscription is still subscribed.
|
||
|
final Subscription subscription = mSubscriptions.get(parentId);
|
||
|
if (subscription != null) {
|
||
|
// Tell the app.
|
||
|
SubscriptionCallback subscriptionCallback =
|
||
|
subscription.getCallback(mContext, options);
|
||
|
if (subscriptionCallback != null) {
|
||
|
List<MediaItem> data = list == null ? null : list.getList();
|
||
|
if (options == null) {
|
||
|
if (data == null) {
|
||
|
subscriptionCallback.onError(parentId);
|
||
|
} else {
|
||
|
subscriptionCallback.onChildrenLoaded(parentId, data);
|
||
|
}
|
||
|
} else {
|
||
|
if (data == null) {
|
||
|
subscriptionCallback.onError(parentId, options);
|
||
|
} else {
|
||
|
subscriptionCallback.onChildrenLoaded(parentId, data, options);
|
||
|
}
|
||
|
}
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
if (DBG) {
|
||
|
Log.d(TAG, "onLoadChildren for id that isn't subscribed id=" + parentId);
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
private void onDisconnectRequested(ServiceCallbacks callback) {
|
||
|
mHandler.post(
|
||
|
() -> {
|
||
|
Log.i(TAG, "onDisconnectRequest for " + mServiceComponent);
|
||
|
|
||
|
if (!isCurrent(callback, "onDisconnectRequest")) {
|
||
|
return;
|
||
|
}
|
||
|
forceCloseConnection();
|
||
|
mCallback.onDisconnected();
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Return true if {@code callback} is the current ServiceCallbacks. Also logs if it's not.
|
||
|
*/
|
||
|
private boolean isCurrent(IMediaBrowserServiceCallbacks callback, String funcName) {
|
||
|
if (mServiceCallbacks != callback || mState == CONNECT_STATE_DISCONNECTING
|
||
|
|| mState == CONNECT_STATE_DISCONNECTED) {
|
||
|
if (mState != CONNECT_STATE_DISCONNECTING && mState != CONNECT_STATE_DISCONNECTED) {
|
||
|
Log.i(TAG, funcName + " for " + mServiceComponent + " with mServiceConnection="
|
||
|
+ mServiceCallbacks + " this=" + this);
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
private ServiceCallbacks getNewServiceCallbacks() {
|
||
|
return new ServiceCallbacks(this);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Log internal state.
|
||
|
* @hide
|
||
|
*/
|
||
|
void dump() {
|
||
|
Log.d(TAG, "MediaBrowser...");
|
||
|
Log.d(TAG, " mServiceComponent=" + mServiceComponent);
|
||
|
Log.d(TAG, " mCallback=" + mCallback);
|
||
|
Log.d(TAG, " mRootHints=" + mRootHints);
|
||
|
Log.d(TAG, " mState=" + getStateLabel(mState));
|
||
|
Log.d(TAG, " mServiceConnection=" + mServiceConnection);
|
||
|
Log.d(TAG, " mServiceBinder=" + mServiceBinder);
|
||
|
Log.d(TAG, " mServiceCallbacks=" + mServiceCallbacks);
|
||
|
Log.d(TAG, " mRootId=" + mRootId);
|
||
|
Log.d(TAG, " mMediaSessionToken=" + mMediaSessionToken);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* A class with information on a single media item for use in browsing/searching media.
|
||
|
* MediaItems are application dependent so we cannot guarantee that they contain the
|
||
|
* right values.
|
||
|
*/
|
||
|
public static class MediaItem implements Parcelable {
|
||
|
private final int mFlags;
|
||
|
private final MediaDescription mDescription;
|
||
|
|
||
|
/** @hide */
|
||
|
@Retention(RetentionPolicy.SOURCE)
|
||
|
@IntDef(flag = true, value = { FLAG_BROWSABLE, FLAG_PLAYABLE })
|
||
|
public @interface Flags { }
|
||
|
|
||
|
/**
|
||
|
* Flag: Indicates that the item has children of its own.
|
||
|
*/
|
||
|
public static final int FLAG_BROWSABLE = 1 << 0;
|
||
|
|
||
|
/**
|
||
|
* Flag: Indicates that the item is playable.
|
||
|
* <p>
|
||
|
* The id of this item may be passed to
|
||
|
* {@link android.media.session.MediaController.TransportControls
|
||
|
* #playFromMediaId(String, Bundle)} to start playing it.
|
||
|
* </p>
|
||
|
*/
|
||
|
public static final int FLAG_PLAYABLE = 1 << 1;
|
||
|
|
||
|
/**
|
||
|
* Create a new MediaItem for use in browsing media.
|
||
|
* @param description The description of the media, which must include a
|
||
|
* media id.
|
||
|
* @param flags The flags for this item.
|
||
|
*/
|
||
|
public MediaItem(@NonNull MediaDescription description, @Flags int flags) {
|
||
|
if (description == null) {
|
||
|
throw new IllegalArgumentException("description cannot be null");
|
||
|
}
|
||
|
if (TextUtils.isEmpty(description.getMediaId())) {
|
||
|
throw new IllegalArgumentException("description must have a non-empty media id");
|
||
|
}
|
||
|
mFlags = flags;
|
||
|
mDescription = description;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Private constructor.
|
||
|
*/
|
||
|
private MediaItem(Parcel in) {
|
||
|
mFlags = in.readInt();
|
||
|
mDescription = MediaDescription.CREATOR.createFromParcel(in);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public int describeContents() {
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void writeToParcel(Parcel out, int flags) {
|
||
|
out.writeInt(mFlags);
|
||
|
mDescription.writeToParcel(out, flags);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public String toString() {
|
||
|
final StringBuilder sb = new StringBuilder("MediaItem{");
|
||
|
sb.append("mFlags=").append(mFlags);
|
||
|
sb.append(", mDescription=").append(mDescription);
|
||
|
sb.append('}');
|
||
|
return sb.toString();
|
||
|
}
|
||
|
|
||
|
public static final @android.annotation.NonNull Parcelable.Creator<MediaItem> CREATOR =
|
||
|
new Parcelable.Creator<MediaItem>() {
|
||
|
@Override
|
||
|
public MediaItem createFromParcel(Parcel in) {
|
||
|
return new MediaItem(in);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public MediaItem[] newArray(int size) {
|
||
|
return new MediaItem[size];
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Gets the flags of the item.
|
||
|
*/
|
||
|
public @Flags int getFlags() {
|
||
|
return mFlags;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns whether this item is browsable.
|
||
|
* @see #FLAG_BROWSABLE
|
||
|
*/
|
||
|
public boolean isBrowsable() {
|
||
|
return (mFlags & FLAG_BROWSABLE) != 0;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns whether this item is playable.
|
||
|
* @see #FLAG_PLAYABLE
|
||
|
*/
|
||
|
public boolean isPlayable() {
|
||
|
return (mFlags & FLAG_PLAYABLE) != 0;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns the description of the media.
|
||
|
*/
|
||
|
public @NonNull MediaDescription getDescription() {
|
||
|
return mDescription;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns the media id in the {@link MediaDescription} for this item.
|
||
|
* @see android.media.MediaMetadata#METADATA_KEY_MEDIA_ID
|
||
|
*/
|
||
|
public @Nullable String getMediaId() {
|
||
|
return mDescription.getMediaId();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Callbacks for connection related events.
|
||
|
*/
|
||
|
public static class ConnectionCallback {
|
||
|
/**
|
||
|
* Invoked after {@link MediaBrowser#connect()} when the request has successfully completed.
|
||
|
*/
|
||
|
public void onConnected() {
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Invoked when the client is disconnected from the media browser.
|
||
|
*/
|
||
|
public void onConnectionSuspended() {
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Invoked when the connection to the media browser failed.
|
||
|
*/
|
||
|
public void onConnectionFailed() {
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Invoked after disconnecting by request of the {@link MediaBrowserService}.
|
||
|
*
|
||
|
* <p>The default implementation of this method calls {@link #onConnectionFailed()}.
|
||
|
*
|
||
|
* @hide
|
||
|
*/
|
||
|
// TODO: b/185136506 - Consider publishing this API in the next window for API changes, if
|
||
|
// the need arises.
|
||
|
public void onDisconnected() {
|
||
|
onConnectionFailed();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Callbacks for subscription related events.
|
||
|
*/
|
||
|
public abstract static class SubscriptionCallback {
|
||
|
Binder mToken;
|
||
|
|
||
|
public SubscriptionCallback() {
|
||
|
mToken = new Binder();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 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.
|
||
|
*/
|
||
|
public void onChildrenLoaded(@NonNull String parentId, @NonNull 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.
|
||
|
* @param options The bundle of service-specific arguments sent to the media
|
||
|
* browser service. The contents of this bundle may affect the
|
||
|
* information returned when browsing.
|
||
|
*/
|
||
|
public void onChildrenLoaded(@NonNull String parentId, @NonNull 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}
|
||
|
* called, because some errors may heal themselves.
|
||
|
* </p>
|
||
|
*
|
||
|
* @param parentId The media id of the parent media item whose children could
|
||
|
* 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 The bundle of service-specific arguments sent to the media
|
||
|
* browser service.
|
||
|
*/
|
||
|
public void onError(@NonNull String parentId, @NonNull Bundle options) {
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Callback for receiving the result of {@link #getItem}.
|
||
|
*/
|
||
|
public abstract static class ItemCallback {
|
||
|
/**
|
||
|
* Called when the item has been returned by the connected service.
|
||
|
*
|
||
|
* @param item The item that was returned or null if it doesn't exist.
|
||
|
*/
|
||
|
public void onItemLoaded(MediaItem item) {
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Called there was an error retrieving it or the connected service doesn't support
|
||
|
* {@link #getItem}.
|
||
|
*
|
||
|
* @param mediaId The media id of the media item which could not be loaded.
|
||
|
*/
|
||
|
public void onError(@NonNull String mediaId) {
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* ServiceConnection to the other app.
|
||
|
*/
|
||
|
private class MediaServiceConnection implements ServiceConnection {
|
||
|
@Override
|
||
|
public void onServiceConnected(final ComponentName name, final IBinder binder) {
|
||
|
postOrRun(new Runnable() {
|
||
|
@Override
|
||
|
public void run() {
|
||
|
if (DBG) {
|
||
|
Log.d(TAG, "MediaServiceConnection.onServiceConnected name=" + name
|
||
|
+ " binder=" + binder);
|
||
|
dump();
|
||
|
}
|
||
|
|
||
|
// Make sure we are still the current connection, and that they haven't called
|
||
|
// disconnect().
|
||
|
if (!isCurrent("onServiceConnected")) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Save their binder
|
||
|
mServiceBinder = IMediaBrowserService.Stub.asInterface(binder);
|
||
|
|
||
|
// We make a new mServiceCallbacks each time we connect so that we can drop
|
||
|
// responses from previous connections.
|
||
|
mServiceCallbacks = getNewServiceCallbacks();
|
||
|
mState = CONNECT_STATE_CONNECTING;
|
||
|
|
||
|
// Call connect, which is async. When we get a response from that we will
|
||
|
// say that we're connected.
|
||
|
try {
|
||
|
if (DBG) {
|
||
|
Log.d(TAG, "ServiceCallbacks.onConnect...");
|
||
|
dump();
|
||
|
}
|
||
|
mServiceBinder.connect(mContext.getPackageName(), mRootHints,
|
||
|
mServiceCallbacks);
|
||
|
} catch (RemoteException ex) {
|
||
|
// Connect failed, which isn't good. But the auto-reconnect on the service
|
||
|
// will take over and we will come back. We will also get the
|
||
|
// onServiceDisconnected, which has all the cleanup code. So let that do
|
||
|
// it.
|
||
|
Log.w(TAG, "RemoteException during connect for " + mServiceComponent);
|
||
|
if (DBG) {
|
||
|
Log.d(TAG, "ServiceCallbacks.onConnect...");
|
||
|
dump();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void onServiceDisconnected(final ComponentName name) {
|
||
|
postOrRun(new Runnable() {
|
||
|
@Override
|
||
|
public void run() {
|
||
|
if (DBG) {
|
||
|
Log.d(TAG, "MediaServiceConnection.onServiceDisconnected name=" + name
|
||
|
+ " this=" + this + " mServiceConnection=" + mServiceConnection);
|
||
|
dump();
|
||
|
}
|
||
|
|
||
|
// Make sure we are still the current connection, and that they haven't called
|
||
|
// disconnect().
|
||
|
if (!isCurrent("onServiceDisconnected")) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Clear out what we set in onServiceConnected
|
||
|
mServiceBinder = null;
|
||
|
mServiceCallbacks = null;
|
||
|
|
||
|
// And tell the app that it's suspended.
|
||
|
mState = CONNECT_STATE_SUSPENDED;
|
||
|
mCallback.onConnectionSuspended();
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
private void postOrRun(Runnable r) {
|
||
|
if (Thread.currentThread() == mHandler.getLooper().getThread()) {
|
||
|
r.run();
|
||
|
} else {
|
||
|
mHandler.post(r);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Return true if this is the current ServiceConnection. Also logs if it's not.
|
||
|
*/
|
||
|
private boolean isCurrent(String funcName) {
|
||
|
if (mServiceConnection != this || mState == CONNECT_STATE_DISCONNECTING
|
||
|
|| mState == CONNECT_STATE_DISCONNECTED) {
|
||
|
if (mState != CONNECT_STATE_DISCONNECTING && mState != CONNECT_STATE_DISCONNECTED) {
|
||
|
// Check mState, because otherwise this log is noisy.
|
||
|
Log.i(TAG, funcName + " for " + mServiceComponent + " with mServiceConnection="
|
||
|
+ mServiceConnection + " this=" + this);
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Callbacks from the service.
|
||
|
*/
|
||
|
private static class ServiceCallbacks extends IMediaBrowserServiceCallbacks.Stub {
|
||
|
private WeakReference<MediaBrowser> mMediaBrowser;
|
||
|
|
||
|
ServiceCallbacks(MediaBrowser mediaBrowser) {
|
||
|
mMediaBrowser = new WeakReference<MediaBrowser>(mediaBrowser);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* The other side has acknowledged our connection. The parameters to this function
|
||
|
* are the initial data as requested.
|
||
|
*/
|
||
|
@Override
|
||
|
public void onConnect(String root, MediaSession.Token session,
|
||
|
final Bundle extras) {
|
||
|
MediaBrowser mediaBrowser = mMediaBrowser.get();
|
||
|
if (mediaBrowser != null) {
|
||
|
mediaBrowser.onServiceConnected(this, root, session, extras);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* The other side does not like us. Tell the app via onConnectionFailed.
|
||
|
*/
|
||
|
@Override
|
||
|
public void onConnectFailed() {
|
||
|
MediaBrowser mediaBrowser = mMediaBrowser.get();
|
||
|
if (mediaBrowser != null) {
|
||
|
mediaBrowser.onConnectionFailed(this);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void onLoadChildren(String parentId, ParceledListSlice list, Bundle options) {
|
||
|
MediaBrowser mediaBrowser = mMediaBrowser.get();
|
||
|
if (mediaBrowser != null) {
|
||
|
mediaBrowser.onLoadChildren(this, parentId, list, options);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void onDisconnect() {
|
||
|
MediaBrowser mediaBrowser = mMediaBrowser.get();
|
||
|
if (mediaBrowser != null) {
|
||
|
mediaBrowser.onDisconnectRequested(this);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private static class Subscription {
|
||
|
private final List<SubscriptionCallback> mCallbacks;
|
||
|
private final List<Bundle> mOptionsList;
|
||
|
|
||
|
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 SubscriptionCallback getCallback(Context context, Bundle options) {
|
||
|
if (options != null) {
|
||
|
options.setClassLoader(context.getClassLoader());
|
||
|
}
|
||
|
for (int i = 0; i < mOptionsList.size(); ++i) {
|
||
|
if (MediaBrowserUtils.areSameOptions(mOptionsList.get(i), options)) {
|
||
|
return mCallbacks.get(i);
|
||
|
}
|
||
|
}
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
public void putCallback(Context context, Bundle options, SubscriptionCallback callback) {
|
||
|
if (options != null) {
|
||
|
options.setClassLoader(context.getClassLoader());
|
||
|
}
|
||
|
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);
|
||
|
}
|
||
|
}
|
||
|
}
|