/* * 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.BuildCompat; 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}. *

* This object is not thread-safe. All calls should happen on the thread on which the browser * was constructed. *

* All callback methods will be called from the thread on which the browser was constructed. *

* *
*

Developer Guides

*

For information about building your media application, read the * Media Apps developer guide.

*
*/ 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 (BuildCompat.isAtLeastO()) { mImpl = new MediaBrowserImplApi24(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. *

* The connection callback specified in the constructor will be invoked * when the connection completes or fails. *

*/ 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. *

* Note that the root id may become invalid or change when when the * browser is disconnected. *

* * @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. *

* Note that the session token may become invalid or change when when the * browser is disconnected. *

* * @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. *

* 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. *

*

* 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. *

* * @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. *

* 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. *

*

* 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. *

* * @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. *

* The query callback will no longer be invoked for results associated with * this id once this method returns. *

* * @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. *

* The query callback will no longer be invoked for results associated with * this id once this method returns. *

* * @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. *

* The id of this item may be passed to * {@link TransportControls#playFromMediaId(String, Bundle)} * to start playing it. *

*/ public static final int FLAG_PLAYABLE = 1 << 1; /** * Creates an instance from a framework {@link android.media.browse.MediaBrowser.MediaItem} * object. *

* This method is only supported on API 21+. On API 20 and below, it returns null. *

* * @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. *

* This method is only supported on API 21+. On API 20 and below, it returns null. *

* * @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 fromMediaItemList(List itemList) { if (itemList == null || Build.VERSION.SDK_INT < 21) { return null; } List 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 CREATOR = new Parcelable.Creator() { @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 mSubscriptionRef; public SubscriptionCallback() { if (BuildCompat.isAtLeastO()) { mSubscriptionCallbackObj = MediaBrowserCompatApi24.createSubscriptionCallback(new StubApi24()); mToken = null; } else if (Build.VERSION.SDK_INT >= 21) { mSubscriptionCallbackObj = MediaBrowserCompatApi21.createSubscriptionCallback(new StubApi21()); mToken = new Binder(); } else { mSubscriptionCallbackObj = null; 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 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 children, @NonNull Bundle options) { } /** * Called when the id doesn't exist or other errors in subscribing. *

* If this is called, the subscription remains until {@link MediaBrowserCompat#unsubscribe} * called, because some errors may heal themselves. *

* * @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. *

* If this is called, the subscription remains until {@link MediaBrowserCompat#unsubscribe} * called, because some errors may heal themselves. *

* * @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 itemList = MediaItem.fromMediaItemList(children); final List callbacks = sub.getCallbacks(); final List 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 applyOptions(List 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 StubApi24 extends StubApi21 implements MediaBrowserCompatApi24.SubscriptionCallback { StubApi24() { } @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 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, Bundle options, @NonNull SubscriptionCallback callback); void unsubscribe(@NonNull String parentId, SubscriptionCallback callback); void getItem(final @NonNull String mediaId, @NonNull final ItemCallback cb); void search(@NonNull String query, Bundle extras, @NonNull SearchCallback callback); void sendCustomAction(String action, Bundle extras, final 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 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. *

* 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(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 callbacks = sub.getCallbacks(); final List 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); 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 subscriptionEntry : mSubscriptions.entrySet()) { String id = subscriptionEntry.getKey(); Subscription sub = subscriptionEntry.getValue(); List callbackList = sub.getCallbacks(); List 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(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 { protected final Object mBrowserObj; protected final Bundle mRootHints; protected final CallbackHandler mHandler = new CallbackHandler(this); private final ArrayMap mSubscriptions = new ArrayMap<>(); protected ServiceBinderWrapper mServiceBinderWrapper; protected Messenger mCallbacksMessenger; private MediaSessionCompat.Token mMediaSessionToken; public MediaBrowserImplApi21(Context context, ComponentName serviceComponent, ConnectionCallback callback, Bundle rootHints) { 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(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 callbacks = sub.getCallbacks(); final List 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 callbacks = sub.getCallbacks(); final List 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(final String action, final Bundle extras, 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."); 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); 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(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 { public 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); } } } // TODO: Rename to MediaBrowserImplApi26 once O is released @RequiresApi(26) static class MediaBrowserImplApi24 extends MediaBrowserImplApi23 { public MediaBrowserImplApi24(Context context, ComponentName serviceComponent, ConnectionCallback callback, Bundle rootHints) { super(context, serviceComponent, callback, rootHints); } @Override public void subscribe(@NonNull String parentId, @NonNull Bundle options, @NonNull SubscriptionCallback callback) { if (options == null) { MediaBrowserCompatApi21.subscribe( mBrowserObj, parentId, callback.mSubscriptionCallbackObj); } else { MediaBrowserCompatApi24.subscribe( mBrowserObj, parentId, options, callback.mSubscriptionCallbackObj); } } @Override public void unsubscribe(@NonNull String parentId, SubscriptionCallback callback) { if (callback == null) { MediaBrowserCompatApi21.unsubscribe(mBrowserObj, parentId); } else { MediaBrowserCompatApi24.unsubscribe(mBrowserObj, parentId, callback.mSubscriptionCallbackObj); } } } private static class Subscription { private final List mCallbacks; private final List mOptionsList; public Subscription() { mCallbacks = new ArrayList<>(); mOptionsList = new ArrayList<>(); } public boolean isEmpty() { return mCallbacks.isEmpty(); } public List getOptionsList() { return mOptionsList; } public List getCallbacks() { return mCallbacks; } public SubscriptionCallback getCallback(Bundle options) { for (int i = 0; i < mOptionsList.size(); ++i) { if (MediaBrowserCompatUtils.areSameOptions(mOptionsList.get(i), options)) { return mCallbacks.get(i); } } return null; } public void putCallback(Bundle options, SubscriptionCallback callback) { 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 mCallbackImplRef; private WeakReference 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 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; } } } }