blob: 7adf7d786347b8e516373a5869218f1ec7bf5dd9 [file] [log] [blame]
/*
* Copyright (C) 2015 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.support.v4.media;
import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_ADD_SUBSCRIPTION;
import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_CONNECT;
import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_DISCONNECT;
import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_GET_MEDIA_ITEM;
import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_REGISTER_CALLBACK_MESSENGER;
import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_REMOVE_SUBSCRIPTION;
import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_SEARCH;
import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_SEND_CUSTOM_ACTION;
import static android.support.v4.media.MediaBrowserProtocol
.CLIENT_MSG_UNREGISTER_CALLBACK_MESSENGER;
import static android.support.v4.media.MediaBrowserProtocol.CLIENT_VERSION_CURRENT;
import static android.support.v4.media.MediaBrowserProtocol.DATA_CALLBACK_TOKEN;
import static android.support.v4.media.MediaBrowserProtocol.DATA_CUSTOM_ACTION;
import static android.support.v4.media.MediaBrowserProtocol.DATA_CUSTOM_ACTION_EXTRAS;
import static android.support.v4.media.MediaBrowserProtocol.DATA_MEDIA_ITEM_ID;
import static android.support.v4.media.MediaBrowserProtocol.DATA_MEDIA_ITEM_LIST;
import static android.support.v4.media.MediaBrowserProtocol.DATA_MEDIA_SESSION_TOKEN;
import static android.support.v4.media.MediaBrowserProtocol.DATA_OPTIONS;
import static android.support.v4.media.MediaBrowserProtocol.DATA_PACKAGE_NAME;
import static android.support.v4.media.MediaBrowserProtocol.DATA_RESULT_RECEIVER;
import static android.support.v4.media.MediaBrowserProtocol.DATA_ROOT_HINTS;
import static android.support.v4.media.MediaBrowserProtocol.DATA_SEARCH_EXTRAS;
import static android.support.v4.media.MediaBrowserProtocol.DATA_SEARCH_QUERY;
import static android.support.v4.media.MediaBrowserProtocol.EXTRA_CLIENT_VERSION;
import static android.support.v4.media.MediaBrowserProtocol.EXTRA_MESSENGER_BINDER;
import static android.support.v4.media.MediaBrowserProtocol.EXTRA_SESSION_BINDER;
import static android.support.v4.media.MediaBrowserProtocol.SERVICE_MSG_ON_CONNECT;
import static android.support.v4.media.MediaBrowserProtocol.SERVICE_MSG_ON_CONNECT_FAILED;
import static android.support.v4.media.MediaBrowserProtocol.SERVICE_MSG_ON_LOAD_CHILDREN;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.BadParcelableException;
import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.RemoteException;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.support.annotation.RestrictTo;
import android.support.v4.app.BundleCompat;
import android.support.v4.media.session.IMediaSession;
import android.support.v4.media.session.MediaControllerCompat.TransportControls;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.os.ResultReceiver;
import android.support.v4.util.ArrayMap;
import android.text.TextUtils;
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.Collections;
import java.util.List;
import java.util.Map;
/**
* Browses media content offered by a {@link MediaBrowserServiceCompat}.
* <p>
* This object is not thread-safe. All calls should happen on the thread on which the browser
* was constructed.
* </p><p>
* All callback methods will be called from the thread on which the browser was constructed.
* </p>
*
* <div class="special reference">
* <h3>Developer Guides</h3>
* <p>For information about building your media application, read the
* <a href="{@docRoot}guide/topics/media-apps/index.html">Media Apps</a> developer guide.</p>
* </div>
*/
public final class MediaBrowserCompat {
static final String TAG = "MediaBrowserCompat";
static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
/**
* 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";
/**
* Used as a string extra field to denote the target {@link MediaItem}.
*
* @see #CUSTOM_ACTION_DOWNLOAD
* @see #CUSTOM_ACTION_REMOVE_DOWNLOADED_FILE
*/
public static final String EXTRA_MEDIA_ID = "android.media.browse.extra.MEDIA_ID";
/**
* Used as a float extra field to denote the current progress during download. The value of this
* field must be a float number within [0.0, 1.0].
*
* @see #CUSTOM_ACTION_DOWNLOAD
* @see CustomActionCallback#onProgressUpdate
*/
public static final String EXTRA_DOWNLOAD_PROGRESS =
"android.media.browse.extra.DOWNLOAD_PROGRESS";
/**
* Predefined custom action to ask the connected service to download a specific
* {@link MediaItem} for offline playback. The id of the media item must be passed in an extra
* bundle. The download progress might be delivered to the browser via
* {@link CustomActionCallback#onProgressUpdate}.
*
* @see #EXTRA_MEDIA_ID
* @see #EXTRA_DOWNLOAD_PROGRESS
* @see #CUSTOM_ACTION_REMOVE_DOWNLOADED_FILE
*/
public static final String CUSTOM_ACTION_DOWNLOAD = "android.support.v4.media.action.DOWNLOAD";
/**
* Predefined custom action to ask the connected service to remove the downloaded file of
* {@link MediaItem} by the {@link #CUSTOM_ACTION_DOWNLOAD download} action. The id of the
* media item must be passed in an extra bundle.
*
* @see #EXTRA_MEDIA_ID
* @see #CUSTOM_ACTION_DOWNLOAD
*/
public static final String CUSTOM_ACTION_REMOVE_DOWNLOADED_FILE =
"android.support.v4.media.action.REMOVE_DOWNLOADED_FILE";
private final MediaBrowserImpl mImpl;
/**
* Creates a media browser for the specified media browse service.
*
* @param context The context.
* @param serviceComponent The component name of the media browse service.
* @param callback The connection callback.
* @param rootHints An optional bundle of service-specific arguments to send
* to the media browse service when connecting and retrieving the root id
* for browsing, or null if none. The contents of this bundle may affect
* the information returned when browsing.
* @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_RECENT
* @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_OFFLINE
* @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_SUGGESTED
*/
public MediaBrowserCompat(Context context, ComponentName serviceComponent,
ConnectionCallback callback, Bundle rootHints) {
// To workaround an issue of {@link #unsubscribe(String, SubscriptionCallback)} on API 24
// and 25 devices, use the support library version of implementation on those devices.
if (Build.VERSION.SDK_INT >= 26) {
mImpl = new MediaBrowserImplApi26(context, serviceComponent, callback, rootHints);
} else if (Build.VERSION.SDK_INT >= 23) {
mImpl = new MediaBrowserImplApi23(context, serviceComponent, callback, rootHints);
} else if (Build.VERSION.SDK_INT >= 21) {
mImpl = new MediaBrowserImplApi21(context, serviceComponent, callback, rootHints);
} else {
mImpl = new MediaBrowserImplBase(context, serviceComponent, callback, rootHints);
}
}
/**
* Connects to the media browse service.
* <p>
* The connection callback specified in the constructor will be invoked
* when the connection completes or fails.
* </p>
*/
public void connect() {
mImpl.connect();
}
/**
* Disconnects from the media browse service.
* After this, no more callbacks will be received.
*/
public void disconnect() {
mImpl.disconnect();
}
/**
* Returns whether the browser is connected to the service.
*/
public boolean isConnected() {
return mImpl.isConnected();
}
/**
* Gets the service component that the media browser is connected to.
*/
public @NonNull
ComponentName getServiceComponent() {
return mImpl.getServiceComponent();
}
/**
* Gets the root id.
* <p>
* Note that the root id may become invalid or change when when the
* browser is disconnected.
* </p>
*
* @throws IllegalStateException if not connected.
*/
public @NonNull String getRoot() {
return mImpl.getRoot();
}
/**
* Gets any extras for the media service.
*
* @throws IllegalStateException if not connected.
*/
public @Nullable
Bundle getExtras() {
return mImpl.getExtras();
}
/**
* Gets the media session token associated with the media browser.
* <p>
* Note that the session token may become invalid or change when when the
* browser is disconnected.
* </p>
*
* @return The session token for the browser, never null.
*
* @throws IllegalStateException if not connected.
*/
public @NonNull MediaSessionCompat.Token getSessionToken() {
return mImpl.getSessionToken();
}
/**
* 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) {
// Check arguments.
if (TextUtils.isEmpty(parentId)) {
throw new IllegalArgumentException("parentId is empty");
}
if (callback == null) {
throw new IllegalArgumentException("callback is null");
}
mImpl.subscribe(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 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) {
// Check arguments.
if (TextUtils.isEmpty(parentId)) {
throw new IllegalArgumentException("parentId is empty");
}
if (callback == null) {
throw new IllegalArgumentException("callback is null");
}
if (options == null) {
throw new IllegalArgumentException("options are null");
}
mImpl.subscribe(parentId, 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) {
// Check arguments.
if (TextUtils.isEmpty(parentId)) {
throw new IllegalArgumentException("parentId is empty");
}
mImpl.unsubscribe(parentId, null);
}
/**
* 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 callback A callback sent to the media browse service to subscribe.
*/
public void unsubscribe(@NonNull String parentId, @NonNull SubscriptionCallback callback) {
// Check arguments.
if (TextUtils.isEmpty(parentId)) {
throw new IllegalArgumentException("parentId is empty");
}
if (callback == null) {
throw new IllegalArgumentException("callback is null");
}
mImpl.unsubscribe(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) {
mImpl.getItem(mediaId, cb);
}
/**
* Searches {@link MediaItem media items} from the connected service. Not all services may
* support this, and {@link SearchCallback#onError} will be called if not implemented.
*
* @param query The search query that contains keywords separated by space. Should not be an
* empty string.
* @param extras The bundle of service-specific arguments to send to the media browser service.
* The contents of this bundle may affect the search result.
* @param callback The callback to receive the search result. Must be non-null.
* @throws IllegalStateException if the browser is not connected to the media browser service.
*/
public void search(@NonNull final String query, final Bundle extras,
@NonNull SearchCallback callback) {
if (TextUtils.isEmpty(query)) {
throw new IllegalArgumentException("query cannot be empty");
}
if (callback == null) {
throw new IllegalArgumentException("callback cannot be null");
}
mImpl.search(query, extras, callback);
}
/**
* Sends a custom action to the connected service. If the service doesn't support the given
* action, {@link CustomActionCallback#onError} will be called.
*
* @param action The custom action that will be sent to the connected service. Should not be an
* empty string.
* @param extras The bundle of service-specific arguments to send to the media browser service.
* @param callback The callback to receive the result of the custom action.
* @see #CUSTOM_ACTION_DOWNLOAD
* @see #CUSTOM_ACTION_REMOVE_DOWNLOADED_FILE
*/
public void sendCustomAction(@NonNull String action, Bundle extras,
@Nullable CustomActionCallback callback) {
if (TextUtils.isEmpty(action)) {
throw new IllegalArgumentException("action cannot be empty");
}
mImpl.sendCustomAction(action, extras, callback);
}
/**
* 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 MediaDescriptionCompat mDescription;
/** @hide */
@RestrictTo(LIBRARY_GROUP)
@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 TransportControls#playFromMediaId(String, Bundle)}
* to start playing it.
* </p>
*/
public static final int FLAG_PLAYABLE = 1 << 1;
/**
* Creates an instance from a framework {@link android.media.browse.MediaBrowser.MediaItem}
* object.
* <p>
* This method is only supported on API 21+. On API 20 and below, it returns null.
* </p>
*
* @param itemObj A {@link android.media.browse.MediaBrowser.MediaItem} object.
* @return An equivalent {@link MediaItem} object, or null if none.
*/
public static MediaItem fromMediaItem(Object itemObj) {
if (itemObj == null || Build.VERSION.SDK_INT < 21) {
return null;
}
int flags = MediaBrowserCompatApi21.MediaItem.getFlags(itemObj);
MediaDescriptionCompat description =
MediaDescriptionCompat.fromMediaDescription(
MediaBrowserCompatApi21.MediaItem.getDescription(itemObj));
return new MediaItem(description, flags);
}
/**
* Creates a list of {@link MediaItem} objects from a framework
* {@link android.media.browse.MediaBrowser.MediaItem} object list.
* <p>
* This method is only supported on API 21+. On API 20 and below, it returns null.
* </p>
*
* @param itemList A list of {@link android.media.browse.MediaBrowser.MediaItem} objects.
* @return An equivalent list of {@link MediaItem} objects, or null if none.
*/
public static List<MediaItem> fromMediaItemList(List<?> itemList) {
if (itemList == null || Build.VERSION.SDK_INT < 21) {
return null;
}
List<MediaItem> items = new ArrayList<>(itemList.size());
for (Object itemObj : itemList) {
items.add(fromMediaItem(itemObj));
}
return items;
}
/**
* 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 MediaDescriptionCompat 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.
*/
MediaItem(Parcel in) {
mFlags = in.readInt();
mDescription = MediaDescriptionCompat.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 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 MediaDescriptionCompat getDescription() {
return mDescription;
}
/**
* Returns the media id in the {@link MediaDescriptionCompat} for this item.
* @see MediaMetadataCompat#METADATA_KEY_MEDIA_ID
*/
public @Nullable String getMediaId() {
return mDescription.getMediaId();
}
}
/**
* Callbacks for connection related events.
*/
public static class ConnectionCallback {
final Object mConnectionCallbackObj;
ConnectionCallbackInternal mConnectionCallbackInternal;
public ConnectionCallback() {
if (Build.VERSION.SDK_INT >= 21) {
mConnectionCallbackObj =
MediaBrowserCompatApi21.createConnectionCallback(new StubApi21());
} else {
mConnectionCallbackObj = null;
}
}
/**
* Invoked after {@link MediaBrowserCompat#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() {
}
void setInternalConnectionCallback(ConnectionCallbackInternal connectionCallbackInternal) {
mConnectionCallbackInternal = connectionCallbackInternal;
}
interface ConnectionCallbackInternal {
void onConnected();
void onConnectionSuspended();
void onConnectionFailed();
}
private class StubApi21 implements MediaBrowserCompatApi21.ConnectionCallback {
StubApi21() {
}
@Override
public void onConnected() {
if (mConnectionCallbackInternal != null) {
mConnectionCallbackInternal.onConnected();
}
ConnectionCallback.this.onConnected();
}
@Override
public void onConnectionSuspended() {
if (mConnectionCallbackInternal != null) {
mConnectionCallbackInternal.onConnectionSuspended();
}
ConnectionCallback.this.onConnectionSuspended();
}
@Override
public void onConnectionFailed() {
if (mConnectionCallbackInternal != null) {
mConnectionCallbackInternal.onConnectionFailed();
}
ConnectionCallback.this.onConnectionFailed();
}
}
}
/**
* Callbacks for subscription related events.
*/
public static abstract class SubscriptionCallback {
private final Object mSubscriptionCallbackObj;
private final IBinder mToken;
WeakReference<Subscription> mSubscriptionRef;
public SubscriptionCallback() {
mToken = new Binder();
if (Build.VERSION.SDK_INT >= 26) {
mSubscriptionCallbackObj =
MediaBrowserCompatApi26.createSubscriptionCallback(new StubApi26());
} else if (Build.VERSION.SDK_INT >= 21) {
mSubscriptionCallbackObj =
MediaBrowserCompatApi21.createSubscriptionCallback(new StubApi21());
} else {
mSubscriptionCallbackObj = null;
}
}
/**
* 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 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, @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 MediaBrowserCompat#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 MediaBrowserCompat#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) {
}
private void setSubscription(Subscription subscription) {
mSubscriptionRef = new WeakReference<>(subscription);
}
private class StubApi21 implements MediaBrowserCompatApi21.SubscriptionCallback {
StubApi21() {
}
@Override
public void onChildrenLoaded(@NonNull String parentId, List<?> children) {
Subscription sub = mSubscriptionRef == null ? null : mSubscriptionRef.get();
if (sub == null) {
SubscriptionCallback.this.onChildrenLoaded(
parentId, MediaItem.fromMediaItemList(children));
} else {
List<MediaBrowserCompat.MediaItem> itemList =
MediaItem.fromMediaItemList(children);
final List<SubscriptionCallback> callbacks = sub.getCallbacks();
final List<Bundle> optionsList = sub.getOptionsList();
for (int i = 0; i < callbacks.size(); ++i) {
Bundle options = optionsList.get(i);
if (options == null) {
SubscriptionCallback.this.onChildrenLoaded(parentId, itemList);
} else {
SubscriptionCallback.this.onChildrenLoaded(
parentId, applyOptions(itemList, options), options);
}
}
}
}
@Override
public void onError(@NonNull String parentId) {
SubscriptionCallback.this.onError(parentId);
}
List<MediaBrowserCompat.MediaItem> applyOptions(List<MediaBrowserCompat.MediaItem> list,
final Bundle options) {
if (list == null) {
return null;
}
int page = options.getInt(MediaBrowserCompat.EXTRA_PAGE, -1);
int pageSize = options.getInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, -1);
if (page == -1 && pageSize == -1) {
return list;
}
int fromIndex = pageSize * page;
int toIndex = fromIndex + pageSize;
if (page < 0 || pageSize < 1 || fromIndex >= list.size()) {
return Collections.EMPTY_LIST;
}
if (toIndex > list.size()) {
toIndex = list.size();
}
return list.subList(fromIndex, toIndex);
}
}
private class StubApi26 extends StubApi21
implements MediaBrowserCompatApi26.SubscriptionCallback {
StubApi26() {
}
@Override
public void onChildrenLoaded(@NonNull String parentId, List<?> children,
@NonNull Bundle options) {
SubscriptionCallback.this.onChildrenLoaded(
parentId, MediaItem.fromMediaItemList(children), options);
}
@Override
public void onError(@NonNull String parentId, @NonNull Bundle options) {
SubscriptionCallback.this.onError(parentId, options);
}
}
}
/**
* Callback for receiving the result of {@link #getItem}.
*/
public static abstract class ItemCallback {
final Object mItemCallbackObj;
public ItemCallback() {
if (Build.VERSION.SDK_INT >= 23) {
mItemCallbackObj = MediaBrowserCompatApi23.createItemCallback(new StubApi23());
} else {
mItemCallbackObj = null;
}
}
/**
* Called when the item has been returned by the browser service.
*
* @param item The item that was returned or null if it doesn't exist.
*/
public void onItemLoaded(MediaItem item) {
}
/**
* Called when the item doesn't exist or there was an error retrieving it.
*
* @param itemId The media id of the media item which could not be loaded.
*/
public void onError(@NonNull String itemId) {
}
private class StubApi23 implements MediaBrowserCompatApi23.ItemCallback {
StubApi23() {
}
@Override
public void onItemLoaded(Parcel itemParcel) {
if (itemParcel == null) {
ItemCallback.this.onItemLoaded(null);
} else {
itemParcel.setDataPosition(0);
MediaItem item =
MediaBrowserCompat.MediaItem.CREATOR.createFromParcel(itemParcel);
itemParcel.recycle();
ItemCallback.this.onItemLoaded(item);
}
}
@Override
public void onError(@NonNull String itemId) {
ItemCallback.this.onError(itemId);
}
}
}
/**
* Callback for receiving the result of {@link #search}.
*/
public abstract static class SearchCallback {
/**
* Called when the {@link #search} finished successfully.
*
* @param query The search query sent for the search request to the connected service.
* @param extras The bundle of service-specific arguments sent to the connected service.
* @param items The list of media items which contains the search result.
*/
public void onSearchResult(@NonNull String query, Bundle extras,
@NonNull List<MediaItem> items) {
}
/**
* Called when an error happens while {@link #search} or the connected service doesn't
* support {@link #search}.
*
* @param query The search query sent for the search request to the connected service.
* @param extras The bundle of service-specific arguments sent to the connected service.
*/
public void onError(@NonNull String query, Bundle extras) {
}
}
/**
* Callback for receiving the result of {@link #sendCustomAction}.
*/
public abstract static class CustomActionCallback {
/**
* Called when an interim update was delivered from the connected service while performing
* the custom action.
*
* @param action The custom action sent to the connected service.
* @param extras The bundle of service-specific arguments sent to the connected service.
* @param data The additional data delivered from the connected service.
*/
public void onProgressUpdate(String action, Bundle extras, Bundle data) {
}
/**
* Called when the custom action finished successfully.
*
* @param action The custom action sent to the connected service.
* @param extras The bundle of service-specific arguments sent to the connected service.
* @param resultData The additional data delivered from the connected service.
*/
public void onResult(String action, Bundle extras, Bundle resultData) {
}
/**
* Called when an error happens while performing the custom action or the connected service
* doesn't support the requested custom action.
*
* @param action The custom action sent to the connected service.
* @param extras The bundle of service-specific arguments sent to the connected service.
* @param data The additional data delivered from the connected service.
*/
public void onError(String action, Bundle extras, Bundle data) {
}
}
interface MediaBrowserImpl {
void connect();
void disconnect();
boolean isConnected();
ComponentName getServiceComponent();
@NonNull String getRoot();
@Nullable Bundle getExtras();
@NonNull MediaSessionCompat.Token getSessionToken();
void subscribe(@NonNull String parentId, @Nullable Bundle options,
@NonNull SubscriptionCallback callback);
void unsubscribe(@NonNull String parentId, SubscriptionCallback callback);
void getItem(@NonNull String mediaId, @NonNull ItemCallback cb);
void search(@NonNull String query, Bundle extras, @NonNull SearchCallback callback);
void sendCustomAction(@NonNull String action, Bundle extras,
@Nullable CustomActionCallback callback);
}
interface MediaBrowserServiceCallbackImpl {
void onServiceConnected(Messenger callback, String root, MediaSessionCompat.Token session,
Bundle extra);
void onConnectionFailed(Messenger callback);
void onLoadChildren(Messenger callback, String parentId, List list, Bundle options);
}
static class MediaBrowserImplBase
implements MediaBrowserImpl, MediaBrowserServiceCallbackImpl {
static final int CONNECT_STATE_DISCONNECTING = 0;
static final int CONNECT_STATE_DISCONNECTED = 1;
static final int CONNECT_STATE_CONNECTING = 2;
static final int CONNECT_STATE_CONNECTED = 3;
static final int CONNECT_STATE_SUSPENDED = 4;
final Context mContext;
final ComponentName mServiceComponent;
final ConnectionCallback mCallback;
final Bundle mRootHints;
final CallbackHandler mHandler = new CallbackHandler(this);
private final ArrayMap<String, Subscription> mSubscriptions = new ArrayMap<>();
int mState = CONNECT_STATE_DISCONNECTED;
MediaServiceConnection mServiceConnection;
ServiceBinderWrapper mServiceBinderWrapper;
Messenger mCallbacksMessenger;
private String mRootId;
private MediaSessionCompat.Token mMediaSessionToken;
private Bundle mExtras;
public MediaBrowserImplBase(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);
}
@Override
public void connect() {
if (mState != CONNECT_STATE_DISCONNECTING && mState != CONNECT_STATE_DISCONNECTED) {
throw new IllegalStateException("connect() called while neigther disconnecting nor "
+ "disconnected (state=" + getStateLabel(mState) + ")");
}
mState = CONNECT_STATE_CONNECTING;
mHandler.post(new Runnable() {
@Override
public void run() {
// mState could be changed by the Runnable of disconnect()
if (mState == CONNECT_STATE_DISCONNECTING) {
return;
}
mState = CONNECT_STATE_CONNECTING;
// TODO: remove this extra check.
if (DEBUG) {
if (mServiceConnection != null) {
throw new RuntimeException("mServiceConnection should be null. Instead "
+ "it is " + mServiceConnection);
}
}
if (mServiceBinderWrapper != null) {
throw new RuntimeException("mServiceBinderWrapper should be null. Instead "
+ "it is " + mServiceBinderWrapper);
}
if (mCallbacksMessenger != null) {
throw new RuntimeException("mCallbacksMessenger should be null. Instead "
+ "it is " + mCallbacksMessenger);
}
final Intent intent = new Intent(MediaBrowserServiceCompat.SERVICE_INTERFACE);
intent.setComponent(mServiceComponent);
mServiceConnection = new MediaServiceConnection();
boolean bound = false;
try {
bound = mContext.bindService(intent, mServiceConnection,
Context.BIND_AUTO_CREATE);
} catch (Exception ex) {
Log.e(TAG, "Failed binding to service " + mServiceComponent);
}
if (!bound) {
// Tell them that it didn't work.
forceCloseConnection();
mCallback.onConnectionFailed();
}
if (DEBUG) {
Log.d(TAG, "connect...");
dump();
}
}
});
}
@Override
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 sane 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 (mCallbacksMessenger != null) {
try {
mServiceBinderWrapper.disconnect(mCallbacksMessenger);
} 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 (DEBUG) {
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.
*/
void forceCloseConnection() {
if (mServiceConnection != null) {
mContext.unbindService(mServiceConnection);
}
mState = CONNECT_STATE_DISCONNECTED;
mServiceConnection = null;
mServiceBinderWrapper = null;
mCallbacksMessenger = null;
mHandler.setCallbacksMessenger(null);
mRootId = null;
mMediaSessionToken = null;
}
@Override
public boolean isConnected() {
return mState == CONNECT_STATE_CONNECTED;
}
@Override
public @NonNull ComponentName getServiceComponent() {
if (!isConnected()) {
throw new IllegalStateException("getServiceComponent() called while not connected" +
" (state=" + mState + ")");
}
return mServiceComponent;
}
@Override
public @NonNull String getRoot() {
if (!isConnected()) {
throw new IllegalStateException("getRoot() called while not connected"
+ "(state=" + getStateLabel(mState) + ")");
}
return mRootId;
}
@Override
public @Nullable Bundle getExtras() {
if (!isConnected()) {
throw new IllegalStateException("getExtras() called while not connected (state="
+ getStateLabel(mState) + ")");
}
return mExtras;
}
@Override
public @NonNull MediaSessionCompat.Token getSessionToken() {
if (!isConnected()) {
throw new IllegalStateException("getSessionToken() called while not connected"
+ "(state=" + mState + ")");
}
return mMediaSessionToken;
}
@Override
public void subscribe(@NonNull String parentId, Bundle options,
@NonNull SubscriptionCallback callback) {
// Update or create the subscription.
Subscription sub = mSubscriptions.get(parentId);
if (sub == null) {
sub = new Subscription();
mSubscriptions.put(parentId, sub);
}
Bundle copiedOptions = options == null ? null : new Bundle(options);
sub.putCallback(mContext, copiedOptions, 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 {
mServiceBinderWrapper.addSubscription(parentId, callback.mToken, copiedOptions,
mCallbacksMessenger);
} catch (RemoteException e) {
// 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);
}
}
}
@Override
public void unsubscribe(@NonNull String parentId, SubscriptionCallback callback) {
Subscription sub = mSubscriptions.get(parentId);
if (sub == null) {
return;
}
// Tell the service if necessary.
try {
if (callback == null) {
if (isConnected()) {
mServiceBinderWrapper.removeSubscription(parentId, null,
mCallbacksMessenger);
}
} 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()) {
mServiceBinderWrapper.removeSubscription(
parentId, callback.mToken, mCallbacksMessenger);
}
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);
}
}
@Override
public void getItem(@NonNull final String mediaId, @NonNull final ItemCallback cb) {
if (TextUtils.isEmpty(mediaId)) {
throw new IllegalArgumentException("mediaId is empty");
}
if (cb == null) {
throw new IllegalArgumentException("cb is null");
}
if (!isConnected()) {
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 ItemReceiver(mediaId, cb, mHandler);
try {
mServiceBinderWrapper.getMediaItem(mediaId, receiver, mCallbacksMessenger);
} catch (RemoteException e) {
Log.i(TAG, "Remote error getting media item: " + mediaId);
mHandler.post(new Runnable() {
@Override
public void run() {
cb.onError(mediaId);
}
});
}
}
@Override
public void search(@NonNull final String query, final Bundle extras,
@NonNull final SearchCallback callback) {
if (!isConnected()) {
throw new IllegalStateException("search() called while not connected"
+ " (state=" + getStateLabel(mState) + ")");
}
ResultReceiver receiver = new SearchResultReceiver(query, extras, callback, mHandler);
try {
mServiceBinderWrapper.search(query, extras, receiver, mCallbacksMessenger);
} catch (RemoteException e) {
Log.i(TAG, "Remote error searching items with query: " + query, e);
mHandler.post(new Runnable() {
@Override
public void run() {
callback.onError(query, extras);
}
});
}
}
@Override
public void sendCustomAction(@NonNull final String action, final Bundle extras,
@Nullable final CustomActionCallback callback) {
if (!isConnected()) {
throw new IllegalStateException("Cannot send a custom action (" + action + ") with "
+ "extras " + extras + " because the browser is not connected to the "
+ "service.");
}
ResultReceiver receiver = new CustomActionResultReceiver(action, extras, callback,
mHandler);
try {
mServiceBinderWrapper.sendCustomAction(action, extras, receiver,
mCallbacksMessenger);
} catch (RemoteException e) {
Log.i(TAG, "Remote error sending a custom action: action=" + action + ", extras="
+ extras, e);
if (callback != null) {
mHandler.post(new Runnable() {
@Override
public void run() {
callback.onError(action, extras, null);
}
});
}
}
}
@Override
public void onServiceConnected(final Messenger callback, final String root,
final MediaSessionCompat.Token session, final Bundle extra) {
// 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 (DEBUG) {
Log.d(TAG, "ServiceCallbacks.onConnect...");
dump();
}
mCallback.onConnected();
// we may receive some subscriptions before we are connected, so re-subscribe
// everything now
try {
for (Map.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) {
mServiceBinderWrapper.addSubscription(id, callbackList.get(i).mToken,
optionsList.get(i), mCallbacksMessenger);
}
}
} 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.");
}
}
@Override
public void onConnectionFailed(final Messenger callback) {
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();
}
@Override
public void onLoadChildren(final Messenger callback, final String parentId,
final List list, final Bundle options) {
// Check that there hasn't been a disconnect or a different ServiceConnection.
if (!isCurrent(callback, "onLoadChildren")) {
return;
}
if (DEBUG) {
Log.d(TAG, "onLoadChildren for " + mServiceComponent + " id=" + parentId);
}
// Check that the subscription is still subscribed.
final Subscription subscription = mSubscriptions.get(parentId);
if (subscription == null) {
if (DEBUG) {
Log.d(TAG, "onLoadChildren for id that isn't subscribed id=" + parentId);
}
return;
}
// Tell the app.
SubscriptionCallback subscriptionCallback = subscription.getCallback(mContext, options);
if (subscriptionCallback != null) {
if (options == null) {
if (list == null) {
subscriptionCallback.onError(parentId);
} else {
subscriptionCallback.onChildrenLoaded(parentId, list);
}
} else {
if (list == null) {
subscriptionCallback.onError(parentId, options);
} else {
subscriptionCallback.onChildrenLoaded(parentId, list, options);
}
}
}
}
/**
* 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;
}
}
/**
* Return true if {@code callback} is the current ServiceCallbacks. Also logs if it's not.
*/
@SuppressWarnings("ReferenceEquality")
private boolean isCurrent(Messenger callback, String funcName) {
if (mCallbacksMessenger != 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 mCallbacksMessenger="
+ mCallbacksMessenger + " this=" + this);
}
return false;
}
return true;
}
/**
* Log internal state.
*/
void dump() {
Log.d(TAG, "MediaBrowserCompat...");
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, " mServiceBinderWrapper=" + mServiceBinderWrapper);
Log.d(TAG, " mCallbacksMessenger=" + mCallbacksMessenger);
Log.d(TAG, " mRootId=" + mRootId);
Log.d(TAG, " mMediaSessionToken=" + mMediaSessionToken);
}
/**
* ServiceConnection to the other app.
*/
private class MediaServiceConnection implements ServiceConnection {
MediaServiceConnection() {
}
@Override
public void onServiceConnected(final ComponentName name, final IBinder binder) {
postOrRun(new Runnable() {
@Override
public void run() {
if (DEBUG) {
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
mServiceBinderWrapper = new ServiceBinderWrapper(binder, mRootHints);
// We make a new mServiceCallbacks each time we connect so that we can drop
// responses from previous connections.
mCallbacksMessenger = new Messenger(mHandler);
mHandler.setCallbacksMessenger(mCallbacksMessenger);
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 (DEBUG) {
Log.d(TAG, "ServiceCallbacks.onConnect...");
dump();
}
mServiceBinderWrapper.connect(mContext, mCallbacksMessenger);
} 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 (DEBUG) {
Log.d(TAG, "ServiceCallbacks.onConnect...");
dump();
}
}
}
});
}
@Override
public void onServiceDisconnected(final ComponentName name) {
postOrRun(new Runnable() {
@Override
public void run() {
if (DEBUG) {
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
mServiceBinderWrapper = null;
mCallbacksMessenger = null;
mHandler.setCallbacksMessenger(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.
*/
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;
}
}
}
@RequiresApi(21)
static class MediaBrowserImplApi21 implements MediaBrowserImpl, MediaBrowserServiceCallbackImpl,
ConnectionCallback.ConnectionCallbackInternal {
final Context mContext;
protected final Object mBrowserObj;
protected final Bundle mRootHints;
protected final CallbackHandler mHandler = new CallbackHandler(this);
private final ArrayMap<String, Subscription> mSubscriptions = new ArrayMap<>();
protected ServiceBinderWrapper mServiceBinderWrapper;
protected Messenger mCallbacksMessenger;
private MediaSessionCompat.Token mMediaSessionToken;
MediaBrowserImplApi21(Context context, ComponentName serviceComponent,
ConnectionCallback callback, Bundle rootHints) {
mContext = context;
if (rootHints == null) {
rootHints = new Bundle();
}
rootHints.putInt(EXTRA_CLIENT_VERSION, CLIENT_VERSION_CURRENT);
mRootHints = new Bundle(rootHints);
callback.setInternalConnectionCallback(this);
mBrowserObj = MediaBrowserCompatApi21.createBrowser(context, serviceComponent,
callback.mConnectionCallbackObj, mRootHints);
}
@Override
public void connect() {
MediaBrowserCompatApi21.connect(mBrowserObj);
}
@Override
public void disconnect() {
if (mServiceBinderWrapper != null && mCallbacksMessenger != null) {
try {
mServiceBinderWrapper.unregisterCallbackMessenger(mCallbacksMessenger);
} catch (RemoteException e) {
Log.i(TAG, "Remote error unregistering client messenger." );
}
}
MediaBrowserCompatApi21.disconnect(mBrowserObj);
}
@Override
public boolean isConnected() {
return MediaBrowserCompatApi21.isConnected(mBrowserObj);
}
@Override
public ComponentName getServiceComponent() {
return MediaBrowserCompatApi21.getServiceComponent(mBrowserObj);
}
@NonNull
@Override
public String getRoot() {
return MediaBrowserCompatApi21.getRoot(mBrowserObj);
}
@Nullable
@Override
public Bundle getExtras() {
return MediaBrowserCompatApi21.getExtras(mBrowserObj);
}
@NonNull
@Override
public MediaSessionCompat.Token getSessionToken() {
if (mMediaSessionToken == null) {
mMediaSessionToken = MediaSessionCompat.Token.fromToken(
MediaBrowserCompatApi21.getSessionToken(mBrowserObj));
}
return mMediaSessionToken;
}
@Override
public void subscribe(@NonNull final String parentId, final Bundle options,
@NonNull final SubscriptionCallback callback) {
// Update or create the subscription.
Subscription sub = mSubscriptions.get(parentId);
if (sub == null) {
sub = new Subscription();
mSubscriptions.put(parentId, sub);
}
callback.setSubscription(sub);
Bundle copiedOptions = options == null ? null : new Bundle(options);
sub.putCallback(mContext, copiedOptions, callback);
if (mServiceBinderWrapper == null) {
// TODO: When MediaBrowser is connected to framework's MediaBrowserService,
// subscribe with options won't work properly.
MediaBrowserCompatApi21.subscribe(
mBrowserObj, parentId, callback.mSubscriptionCallbackObj);
} else {
try {
mServiceBinderWrapper.addSubscription(
parentId, callback.mToken, copiedOptions, mCallbacksMessenger);
} catch (RemoteException e) {
// Process is crashing. We will disconnect, and upon reconnect we will
// automatically reregister. So nothing to do here.
Log.i(TAG, "Remote error subscribing media item: " + parentId);
}
}
}
@Override
public void unsubscribe(@NonNull String parentId, SubscriptionCallback callback) {
Subscription sub = mSubscriptions.get(parentId);
if (sub == null) {
return;
}
if (mServiceBinderWrapper == null) {
if (callback == null) {
MediaBrowserCompatApi21.unsubscribe(mBrowserObj, parentId);
} 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) {
callbacks.remove(i);
optionsList.remove(i);
}
}
if (callbacks.size() == 0) {
MediaBrowserCompatApi21.unsubscribe(mBrowserObj, parentId);
}
}
} else {
// Tell the service if necessary.
try {
if (callback == null) {
mServiceBinderWrapper.removeSubscription(parentId, null,
mCallbacksMessenger);
} 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) {
mServiceBinderWrapper.removeSubscription(
parentId, callback.mToken, mCallbacksMessenger);
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);
}
}
@Override
public void getItem(@NonNull final String mediaId, @NonNull final ItemCallback cb) {
if (TextUtils.isEmpty(mediaId)) {
throw new IllegalArgumentException("mediaId is empty");
}
if (cb == null) {
throw new IllegalArgumentException("cb is null");
}
if (!MediaBrowserCompatApi21.isConnected(mBrowserObj)) {
Log.i(TAG, "Not connected, unable to retrieve the MediaItem.");
mHandler.post(new Runnable() {
@Override
public void run() {
cb.onError(mediaId);
}
});
return;
}
if (mServiceBinderWrapper == null) {
mHandler.post(new Runnable() {
@Override
public void run() {
// Default framework implementation.
cb.onError(mediaId);
}
});
return;
}
ResultReceiver receiver = new ItemReceiver(mediaId, cb, mHandler);
try {
mServiceBinderWrapper.getMediaItem(mediaId, receiver, mCallbacksMessenger);
} catch (RemoteException e) {
Log.i(TAG, "Remote error getting media item: " + mediaId);
mHandler.post(new Runnable() {
@Override
public void run() {
cb.onError(mediaId);
}
});
}
}
@Override
public void search(@NonNull final String query, final Bundle extras,
@NonNull final SearchCallback callback) {
if (!isConnected()) {
throw new IllegalStateException("search() called while not connected");
}
if (mServiceBinderWrapper == null) {
Log.i(TAG, "The connected service doesn't support search.");
mHandler.post(new Runnable() {
@Override
public void run() {
// Default framework implementation.
callback.onError(query, extras);
}
});
return;
}
ResultReceiver receiver = new SearchResultReceiver(query, extras, callback, mHandler);
try {
mServiceBinderWrapper.search(query, extras, receiver, mCallbacksMessenger);
} catch (RemoteException e) {
Log.i(TAG, "Remote error searching items with query: " + query, e);
mHandler.post(new Runnable() {
@Override
public void run() {
callback.onError(query, extras);
}
});
}
}
@Override
public void sendCustomAction(@NonNull final String action, final Bundle extras,
@Nullable final CustomActionCallback callback) {
if (!isConnected()) {
throw new IllegalStateException("Cannot send a custom action (" + action + ") with "
+ "extras " + extras + " because the browser is not connected to the "
+ "service.");
}
if (mServiceBinderWrapper == null) {
Log.i(TAG, "The connected service doesn't support sendCustomAction.");
if (callback != null) {
mHandler.post(new Runnable() {
@Override
public void run() {
callback.onError(action, extras, null);
}
});
}
}
ResultReceiver receiver = new CustomActionResultReceiver(action, extras, callback,
mHandler);
try {
mServiceBinderWrapper.sendCustomAction(action, extras, receiver,
mCallbacksMessenger);
} catch (RemoteException e) {
Log.i(TAG, "Remote error sending a custom action: action=" + action + ", extras="
+ extras, e);
if (callback != null) {
mHandler.post(new Runnable() {
@Override
public void run() {
callback.onError(action, extras, null);
}
});
}
}
}
@Override
public void onConnected() {
Bundle extras = MediaBrowserCompatApi21.getExtras(mBrowserObj);
if (extras == null) {
return;
}
IBinder serviceBinder = BundleCompat.getBinder(extras, EXTRA_MESSENGER_BINDER);
if (serviceBinder != null) {
mServiceBinderWrapper = new ServiceBinderWrapper(serviceBinder, mRootHints);
mCallbacksMessenger = new Messenger(mHandler);
mHandler.setCallbacksMessenger(mCallbacksMessenger);
try {
mServiceBinderWrapper.registerCallbackMessenger(mCallbacksMessenger);
} catch (RemoteException e) {
Log.i(TAG, "Remote error registering client messenger." );
}
}
IMediaSession sessionToken = IMediaSession.Stub.asInterface(
BundleCompat.getBinder(extras, EXTRA_SESSION_BINDER));
if (sessionToken != null) {
mMediaSessionToken = MediaSessionCompat.Token.fromToken(
MediaBrowserCompatApi21.getSessionToken(mBrowserObj), sessionToken);
}
}
@Override
public void onConnectionSuspended() {
mServiceBinderWrapper = null;
mCallbacksMessenger = null;
mMediaSessionToken = null;
mHandler.setCallbacksMessenger(null);
}
@Override
public void onConnectionFailed() {
// Do noting
}
@Override
public void onServiceConnected(final Messenger callback, final String root,
final MediaSessionCompat.Token session, final Bundle extra) {
// This method will not be called.
}
@Override
public void onConnectionFailed(Messenger callback) {
// This method will not be called.
}
@Override
@SuppressWarnings("ReferenceEquality")
public void onLoadChildren(Messenger callback, String parentId, List list, Bundle options) {
if (mCallbacksMessenger != callback) {
return;
}
// Check that the subscription is still subscribed.
Subscription subscription = mSubscriptions.get(parentId);
if (subscription == null) {
if (DEBUG) {
Log.d(TAG, "onLoadChildren for id that isn't subscribed id=" + parentId);
}
return;
}
// Tell the app.
SubscriptionCallback subscriptionCallback = subscription.getCallback(mContext, options);
if (subscriptionCallback != null) {
if (options == null) {
if (list == null) {
subscriptionCallback.onError(parentId);
} else {
subscriptionCallback.onChildrenLoaded(parentId, list);
}
} else {
if (list == null) {
subscriptionCallback.onError(parentId, options);
} else {
subscriptionCallback.onChildrenLoaded(parentId, list, options);
}
}
}
}
}
@RequiresApi(23)
static class MediaBrowserImplApi23 extends MediaBrowserImplApi21 {
MediaBrowserImplApi23(Context context, ComponentName serviceComponent,
ConnectionCallback callback, Bundle rootHints) {
super(context, serviceComponent, callback, rootHints);
}
@Override
public void getItem(@NonNull final String mediaId, @NonNull final ItemCallback cb) {
if (mServiceBinderWrapper == null) {
MediaBrowserCompatApi23.getItem(mBrowserObj, mediaId, cb.mItemCallbackObj);
} else {
super.getItem(mediaId, cb);
}
}
}
@RequiresApi(26)
static class MediaBrowserImplApi26 extends MediaBrowserImplApi23 {
MediaBrowserImplApi26(Context context, ComponentName serviceComponent,
ConnectionCallback callback, Bundle rootHints) {
super(context, serviceComponent, callback, rootHints);
}
@Override
public void subscribe(@NonNull String parentId, @Nullable Bundle options,
@NonNull SubscriptionCallback callback) {
if (mServiceBinderWrapper == null) {
if (options == null) {
MediaBrowserCompatApi21.subscribe(
mBrowserObj, parentId, callback.mSubscriptionCallbackObj);
} else {
MediaBrowserCompatApi26.subscribe(
mBrowserObj, parentId, options, callback.mSubscriptionCallbackObj);
}
} else {
super.subscribe(parentId, options, callback);
}
}
@Override
public void unsubscribe(@NonNull String parentId, SubscriptionCallback callback) {
if (mServiceBinderWrapper == null) {
if (callback == null) {
MediaBrowserCompatApi21.unsubscribe(mBrowserObj, parentId);
} else {
MediaBrowserCompatApi26.unsubscribe(mBrowserObj, parentId,
callback.mSubscriptionCallbackObj);
}
} else {
super.unsubscribe(parentId, callback);
}
}
}
private static class Subscription {
private final List<SubscriptionCallback> mCallbacks;
private final List<Bundle> mOptionsList;
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 SubscriptionCallback getCallback(Context context, Bundle options) {
if (options != null) {
options.setClassLoader(context.getClassLoader());
}
for (int i = 0; i < mOptionsList.size(); ++i) {
if (MediaBrowserCompatUtils.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 (MediaBrowserCompatUtils.areSameOptions(mOptionsList.get(i), options)) {
mCallbacks.set(i, callback);
return;
}
}
mCallbacks.add(callback);
mOptionsList.add(options);
}
}
private static class CallbackHandler extends Handler {
private final WeakReference<MediaBrowserServiceCallbackImpl> mCallbackImplRef;
private WeakReference<Messenger> mCallbacksMessengerRef;
CallbackHandler(MediaBrowserServiceCallbackImpl callbackImpl) {
super();
mCallbackImplRef = new WeakReference<>(callbackImpl);
}
@Override
public void handleMessage(Message msg) {
if (mCallbacksMessengerRef == null || mCallbacksMessengerRef.get() == null ||
mCallbackImplRef.get() == null) {
return;
}
Bundle data = msg.getData();
data.setClassLoader(MediaSessionCompat.class.getClassLoader());
MediaBrowserServiceCallbackImpl serviceCallback = mCallbackImplRef.get();
Messenger callbacksMessenger = mCallbacksMessengerRef.get();
try {
switch (msg.what) {
case SERVICE_MSG_ON_CONNECT:
serviceCallback.onServiceConnected(callbacksMessenger,
data.getString(DATA_MEDIA_ITEM_ID),
(MediaSessionCompat.Token) data.getParcelable(
DATA_MEDIA_SESSION_TOKEN),
data.getBundle(DATA_ROOT_HINTS));
break;
case SERVICE_MSG_ON_CONNECT_FAILED:
serviceCallback.onConnectionFailed(callbacksMessenger);
break;
case SERVICE_MSG_ON_LOAD_CHILDREN:
serviceCallback.onLoadChildren(callbacksMessenger,
data.getString(DATA_MEDIA_ITEM_ID),
data.getParcelableArrayList(DATA_MEDIA_ITEM_LIST),
data.getBundle(DATA_OPTIONS));
break;
default:
Log.w(TAG, "Unhandled message: " + msg
+ "\n Client version: " + CLIENT_VERSION_CURRENT
+ "\n Service version: " + msg.arg1);
}
} catch (BadParcelableException e) {
// Do not print the exception here, since it is already done by the Parcel class.
Log.e(TAG, "Could not unparcel the data.");
// If an error happened while connecting, disconnect from the service.
if (msg.what == SERVICE_MSG_ON_CONNECT) {
serviceCallback.onConnectionFailed(callbacksMessenger);
}
}
}
void setCallbacksMessenger(Messenger callbacksMessenger) {
mCallbacksMessengerRef = new WeakReference<>(callbacksMessenger);
}
}
private static class ServiceBinderWrapper {
private Messenger mMessenger;
private Bundle mRootHints;
public ServiceBinderWrapper(IBinder target, Bundle rootHints) {
mMessenger = new Messenger(target);
mRootHints = rootHints;
}
void connect(Context context, Messenger callbacksMessenger)
throws RemoteException {
Bundle data = new Bundle();
data.putString(DATA_PACKAGE_NAME, context.getPackageName());
data.putBundle(DATA_ROOT_HINTS, mRootHints);
sendRequest(CLIENT_MSG_CONNECT, data, callbacksMessenger);
}
void disconnect(Messenger callbacksMessenger) throws RemoteException {
sendRequest(CLIENT_MSG_DISCONNECT, null, callbacksMessenger);
}
void addSubscription(String parentId, IBinder callbackToken, Bundle options,
Messenger callbacksMessenger)
throws RemoteException {
Bundle data = new Bundle();
data.putString(DATA_MEDIA_ITEM_ID, parentId);
BundleCompat.putBinder(data, DATA_CALLBACK_TOKEN, callbackToken);
data.putBundle(DATA_OPTIONS, options);
sendRequest(CLIENT_MSG_ADD_SUBSCRIPTION, data, callbacksMessenger);
}
void removeSubscription(String parentId, IBinder callbackToken,
Messenger callbacksMessenger)
throws RemoteException {
Bundle data = new Bundle();
data.putString(DATA_MEDIA_ITEM_ID, parentId);
BundleCompat.putBinder(data, DATA_CALLBACK_TOKEN, callbackToken);
sendRequest(CLIENT_MSG_REMOVE_SUBSCRIPTION, data, callbacksMessenger);
}
void getMediaItem(String mediaId, ResultReceiver receiver, Messenger callbacksMessenger)
throws RemoteException {
Bundle data = new Bundle();
data.putString(DATA_MEDIA_ITEM_ID, mediaId);
data.putParcelable(DATA_RESULT_RECEIVER, receiver);
sendRequest(CLIENT_MSG_GET_MEDIA_ITEM, data, callbacksMessenger);
}
void registerCallbackMessenger(Messenger callbackMessenger) throws RemoteException {
Bundle data = new Bundle();
data.putBundle(DATA_ROOT_HINTS, mRootHints);
sendRequest(CLIENT_MSG_REGISTER_CALLBACK_MESSENGER, data, callbackMessenger);
}
void unregisterCallbackMessenger(Messenger callbackMessenger) throws RemoteException {
sendRequest(CLIENT_MSG_UNREGISTER_CALLBACK_MESSENGER, null, callbackMessenger);
}
void search(String query, Bundle extras, ResultReceiver receiver,
Messenger callbacksMessenger) throws RemoteException {
Bundle data = new Bundle();
data.putString(DATA_SEARCH_QUERY, query);
data.putBundle(DATA_SEARCH_EXTRAS, extras);
data.putParcelable(DATA_RESULT_RECEIVER, receiver);
sendRequest(CLIENT_MSG_SEARCH, data, callbacksMessenger);
}
void sendCustomAction(String action, Bundle extras, ResultReceiver receiver,
Messenger callbacksMessenger) throws RemoteException {
Bundle data = new Bundle();
data.putString(DATA_CUSTOM_ACTION, action);
data.putBundle(DATA_CUSTOM_ACTION_EXTRAS, extras);
data.putParcelable(DATA_RESULT_RECEIVER, receiver);
sendRequest(CLIENT_MSG_SEND_CUSTOM_ACTION, data, callbacksMessenger);
}
private void sendRequest(int what, Bundle data, Messenger cbMessenger)
throws RemoteException {
Message msg = Message.obtain();
msg.what = what;
msg.arg1 = CLIENT_VERSION_CURRENT;
msg.setData(data);
msg.replyTo = cbMessenger;
mMessenger.send(msg);
}
}
private static class ItemReceiver extends ResultReceiver {
private final String mMediaId;
private final ItemCallback mCallback;
ItemReceiver(String mediaId, ItemCallback callback, Handler handler) {
super(handler);
mMediaId = mediaId;
mCallback = callback;
}
@Override
protected void onReceiveResult(int resultCode, Bundle resultData) {
if (resultData != null) {
resultData.setClassLoader(MediaBrowserCompat.class.getClassLoader());
}
if (resultCode != MediaBrowserServiceCompat.RESULT_OK || resultData == null
|| !resultData.containsKey(MediaBrowserServiceCompat.KEY_MEDIA_ITEM)) {
mCallback.onError(mMediaId);
return;
}
Parcelable item = resultData.getParcelable(MediaBrowserServiceCompat.KEY_MEDIA_ITEM);
if (item == null || item instanceof MediaItem) {
mCallback.onItemLoaded((MediaItem) item);
} else {
mCallback.onError(mMediaId);
}
}
}
private static class SearchResultReceiver extends ResultReceiver {
private final String mQuery;
private final Bundle mExtras;
private final SearchCallback mCallback;
SearchResultReceiver(String query, Bundle extras, SearchCallback callback,
Handler handler) {
super(handler);
mQuery = query;
mExtras = extras;
mCallback = callback;
}
@Override
protected void onReceiveResult(int resultCode, Bundle resultData) {
if (resultData != null) {
resultData.setClassLoader(MediaBrowserCompat.class.getClassLoader());
}
if (resultCode != MediaBrowserServiceCompat.RESULT_OK || resultData == null
|| !resultData.containsKey(MediaBrowserServiceCompat.KEY_SEARCH_RESULTS)) {
mCallback.onError(mQuery, mExtras);
return;
}
Parcelable[] items = resultData.getParcelableArray(
MediaBrowserServiceCompat.KEY_SEARCH_RESULTS);
List<MediaItem> results = null;
if (items != null) {
results = new ArrayList<>();
for (Parcelable item : items) {
results.add((MediaItem) item);
}
}
mCallback.onSearchResult(mQuery, mExtras, results);
}
}
private static class CustomActionResultReceiver extends ResultReceiver {
private final String mAction;
private final Bundle mExtras;
private final CustomActionCallback mCallback;
CustomActionResultReceiver(String action, Bundle extras, CustomActionCallback callback,
Handler handler) {
super(handler);
mAction = action;
mExtras = extras;
mCallback = callback;
}
@Override
protected void onReceiveResult(int resultCode, Bundle resultData) {
if (mCallback == null) {
return;
}
switch (resultCode) {
case MediaBrowserServiceCompat.RESULT_PROGRESS_UPDATE:
mCallback.onProgressUpdate(mAction, mExtras, resultData);
break;
case MediaBrowserServiceCompat.RESULT_OK:
mCallback.onResult(mAction, mExtras, resultData);
break;
case MediaBrowserServiceCompat.RESULT_ERROR:
mCallback.onError(mAction, mExtras, resultData);
break;
default:
Log.w(TAG, "Unknown result code: " + resultCode + " (extras=" + mExtras
+ ", resultData=" + resultData + ")");
break;
}
}
}
}