Home | History | Annotate | Download | only in media
      1 /*
      2  * Copyright 2018 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package android.support.v4.media;
     17 
     18 import static androidx.annotation.RestrictTo.Scope.LIBRARY;
     19 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
     20 import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_ADD_SUBSCRIPTION;
     21 import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_CONNECT;
     22 import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_DISCONNECT;
     23 import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_GET_MEDIA_ITEM;
     24 import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_REGISTER_CALLBACK_MESSENGER;
     25 import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_REMOVE_SUBSCRIPTION;
     26 import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_SEARCH;
     27 import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_SEND_CUSTOM_ACTION;
     28 import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_UNREGISTER_CALLBACK_MESSENGER;
     29 import static androidx.media.MediaBrowserProtocol.CLIENT_VERSION_CURRENT;
     30 import static androidx.media.MediaBrowserProtocol.DATA_CALLBACK_TOKEN;
     31 import static androidx.media.MediaBrowserProtocol.DATA_CUSTOM_ACTION;
     32 import static androidx.media.MediaBrowserProtocol.DATA_CUSTOM_ACTION_EXTRAS;
     33 import static androidx.media.MediaBrowserProtocol.DATA_MEDIA_ITEM_ID;
     34 import static androidx.media.MediaBrowserProtocol.DATA_MEDIA_ITEM_LIST;
     35 import static androidx.media.MediaBrowserProtocol.DATA_MEDIA_SESSION_TOKEN;
     36 import static androidx.media.MediaBrowserProtocol.DATA_NOTIFY_CHILDREN_CHANGED_OPTIONS;
     37 import static androidx.media.MediaBrowserProtocol.DATA_OPTIONS;
     38 import static androidx.media.MediaBrowserProtocol.DATA_PACKAGE_NAME;
     39 import static androidx.media.MediaBrowserProtocol.DATA_RESULT_RECEIVER;
     40 import static androidx.media.MediaBrowserProtocol.DATA_ROOT_HINTS;
     41 import static androidx.media.MediaBrowserProtocol.DATA_SEARCH_EXTRAS;
     42 import static androidx.media.MediaBrowserProtocol.DATA_SEARCH_QUERY;
     43 import static androidx.media.MediaBrowserProtocol.EXTRA_CLIENT_VERSION;
     44 import static androidx.media.MediaBrowserProtocol.EXTRA_MESSENGER_BINDER;
     45 import static androidx.media.MediaBrowserProtocol.EXTRA_SERVICE_VERSION;
     46 import static androidx.media.MediaBrowserProtocol.EXTRA_SESSION_BINDER;
     47 import static androidx.media.MediaBrowserProtocol.SERVICE_MSG_ON_CONNECT;
     48 import static androidx.media.MediaBrowserProtocol.SERVICE_MSG_ON_CONNECT_FAILED;
     49 import static androidx.media.MediaBrowserProtocol.SERVICE_MSG_ON_LOAD_CHILDREN;
     50 import static androidx.media.MediaBrowserProtocol.SERVICE_VERSION_2;
     51 
     52 import android.content.ComponentName;
     53 import android.content.Context;
     54 import android.content.Intent;
     55 import android.content.ServiceConnection;
     56 import android.os.BadParcelableException;
     57 import android.os.Binder;
     58 import android.os.Build;
     59 import android.os.Bundle;
     60 import android.os.Handler;
     61 import android.os.IBinder;
     62 import android.os.Message;
     63 import android.os.Messenger;
     64 import android.os.Parcel;
     65 import android.os.Parcelable;
     66 import android.os.RemoteException;
     67 import android.support.v4.media.session.IMediaSession;
     68 import android.support.v4.media.session.MediaControllerCompat.TransportControls;
     69 import android.support.v4.media.session.MediaSessionCompat;
     70 import android.support.v4.os.ResultReceiver;
     71 import android.text.TextUtils;
     72 import android.util.Log;
     73 
     74 import androidx.annotation.IntDef;
     75 import androidx.annotation.NonNull;
     76 import androidx.annotation.Nullable;
     77 import androidx.annotation.RequiresApi;
     78 import androidx.annotation.RestrictTo;
     79 import androidx.collection.ArrayMap;
     80 import androidx.core.app.BundleCompat;
     81 import androidx.media.MediaBrowserCompatUtils;
     82 import androidx.media.MediaBrowserServiceCompat;
     83 
     84 import java.lang.annotation.Retention;
     85 import java.lang.annotation.RetentionPolicy;
     86 import java.lang.ref.WeakReference;
     87 import java.util.ArrayList;
     88 import java.util.Collections;
     89 import java.util.List;
     90 import java.util.Map;
     91 
     92 /**
     93  * Browses media content offered by a {@link MediaBrowserServiceCompat}.
     94  * <p>
     95  * This object is not thread-safe. All calls should happen on the thread on which the browser
     96  * was constructed.
     97  * </p><p>
     98  * All callback methods will be called from the thread on which the browser was constructed.
     99  * </p>
    100  *
    101  * <div class="special reference">
    102  * <h3>Developer Guides</h3>
    103  * <p>For information about building your media application, read the
    104  * <a href="{@docRoot}guide/topics/media-apps/index.html">Media Apps</a> developer guide.</p>
    105  * </div>
    106  */
    107 public final class MediaBrowserCompat {
    108     static final String TAG = "MediaBrowserCompat";
    109     static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
    110 
    111     /**
    112      * Used as an int extra field to denote the page number to subscribe.
    113      * The value of {@code EXTRA_PAGE} should be greater than or equal to 1.
    114      *
    115      * @see android.service.media.MediaBrowserService.BrowserRoot
    116      * @see #EXTRA_PAGE_SIZE
    117      */
    118     public static final String EXTRA_PAGE = "android.media.browse.extra.PAGE";
    119 
    120     /**
    121      * Used as an int extra field to denote the number of media items in a page.
    122      * The value of {@code EXTRA_PAGE_SIZE} should be greater than or equal to 1.
    123      *
    124      * @see android.service.media.MediaBrowserService.BrowserRoot
    125      * @see #EXTRA_PAGE
    126      */
    127     public static final String EXTRA_PAGE_SIZE = "android.media.browse.extra.PAGE_SIZE";
    128 
    129     /**
    130      * Used as a string extra field to denote the target {@link MediaItem}.
    131      *
    132      * @see #CUSTOM_ACTION_DOWNLOAD
    133      * @see #CUSTOM_ACTION_REMOVE_DOWNLOADED_FILE
    134      */
    135     public static final String EXTRA_MEDIA_ID = "android.media.browse.extra.MEDIA_ID";
    136 
    137     /**
    138      * Used as a float extra field to denote the current progress during download. The value of this
    139      * field must be a float number within [0.0, 1.0].
    140      *
    141      * @see #CUSTOM_ACTION_DOWNLOAD
    142      * @see CustomActionCallback#onProgressUpdate
    143      */
    144     public static final String EXTRA_DOWNLOAD_PROGRESS =
    145             "android.media.browse.extra.DOWNLOAD_PROGRESS";
    146 
    147     /**
    148      * Predefined custom action to ask the connected service to download a specific
    149      * {@link MediaItem} for offline playback. The id of the media item must be passed in an extra
    150      * bundle. The download progress might be delivered to the browser via
    151      * {@link CustomActionCallback#onProgressUpdate}.
    152      *
    153      * @see #EXTRA_MEDIA_ID
    154      * @see #EXTRA_DOWNLOAD_PROGRESS
    155      * @see #CUSTOM_ACTION_REMOVE_DOWNLOADED_FILE
    156      */
    157     public static final String CUSTOM_ACTION_DOWNLOAD = "android.support.v4.media.action.DOWNLOAD";
    158 
    159     /**
    160      * Predefined custom action to ask the connected service to remove the downloaded file of
    161      * {@link MediaItem} by the {@link #CUSTOM_ACTION_DOWNLOAD download} action. The id of the
    162      * media item must be passed in an extra bundle.
    163      *
    164      * @see #EXTRA_MEDIA_ID
    165      * @see #CUSTOM_ACTION_DOWNLOAD
    166      */
    167     public static final String CUSTOM_ACTION_REMOVE_DOWNLOADED_FILE =
    168             "android.support.v4.media.action.REMOVE_DOWNLOADED_FILE";
    169 
    170     private final MediaBrowserImpl mImpl;
    171 
    172     /**
    173      * Creates a media browser for the specified media browse service.
    174      *
    175      * @param context The context.
    176      * @param serviceComponent The component name of the media browse service.
    177      * @param callback The connection callback.
    178      * @param rootHints An optional bundle of service-specific arguments to send
    179      * to the media browse service when connecting and retrieving the root id
    180      * for browsing, or null if none. The contents of this bundle may affect
    181      * the information returned when browsing.
    182      * @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_RECENT
    183      * @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_OFFLINE
    184      * @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_SUGGESTED
    185      */
    186     public MediaBrowserCompat(Context context, ComponentName serviceComponent,
    187             ConnectionCallback callback, Bundle rootHints) {
    188         // To workaround an issue of {@link #unsubscribe(String, SubscriptionCallback)} on API 24
    189         // and 25 devices, use the support library version of implementation on those devices.
    190         if (Build.VERSION.SDK_INT >= 26) {
    191             mImpl = new MediaBrowserImplApi26(context, serviceComponent, callback, rootHints);
    192         } else if (Build.VERSION.SDK_INT >= 23) {
    193             mImpl = new MediaBrowserImplApi23(context, serviceComponent, callback, rootHints);
    194         } else if (Build.VERSION.SDK_INT >= 21) {
    195             mImpl = new MediaBrowserImplApi21(context, serviceComponent, callback, rootHints);
    196         } else {
    197             mImpl = new MediaBrowserImplBase(context, serviceComponent, callback, rootHints);
    198         }
    199     }
    200 
    201     /**
    202      * Connects to the media browse service.
    203      * <p>
    204      * The connection callback specified in the constructor will be invoked
    205      * when the connection completes or fails.
    206      * </p>
    207      */
    208     public void connect() {
    209         mImpl.connect();
    210     }
    211 
    212     /**
    213      * Disconnects from the media browse service.
    214      * After this, no more callbacks will be received.
    215      */
    216     public void disconnect() {
    217         mImpl.disconnect();
    218     }
    219 
    220     /**
    221      * Returns whether the browser is connected to the service.
    222      */
    223     public boolean isConnected() {
    224         return mImpl.isConnected();
    225     }
    226 
    227     /**
    228      * Gets the service component that the media browser is connected to.
    229      */
    230     public @NonNull
    231     ComponentName getServiceComponent() {
    232         return mImpl.getServiceComponent();
    233     }
    234 
    235     /**
    236      * Gets the root id.
    237      * <p>
    238      * Note that the root id may become invalid or change when when the
    239      * browser is disconnected.
    240      * </p>
    241      *
    242      * @throws IllegalStateException if not connected.
    243      */
    244     public @NonNull String getRoot() {
    245         return mImpl.getRoot();
    246     }
    247 
    248     /**
    249      * Gets any extras for the media service.
    250      *
    251      * @throws IllegalStateException if not connected.
    252      */
    253     public @Nullable
    254     Bundle getExtras() {
    255         return mImpl.getExtras();
    256     }
    257 
    258     /**
    259      * Gets the media session token associated with the media browser.
    260      * <p>
    261      * Note that the session token may become invalid or change when when the
    262      * browser is disconnected.
    263      * </p>
    264      *
    265      * @return The session token for the browser, never null.
    266      *
    267      * @throws IllegalStateException if not connected.
    268      */
    269     public @NonNull MediaSessionCompat.Token getSessionToken() {
    270         return mImpl.getSessionToken();
    271     }
    272 
    273     /**
    274      * Queries for information about the media items that are contained within
    275      * the specified id and subscribes to receive updates when they change.
    276      * <p>
    277      * The list of subscriptions is maintained even when not connected and is
    278      * restored after the reconnection. It is ok to subscribe while not connected
    279      * but the results will not be returned until the connection completes.
    280      * </p>
    281      * <p>
    282      * If the id is already subscribed with a different callback then the new
    283      * callback will replace the previous one and the child data will be
    284      * reloaded.
    285      * </p>
    286      *
    287      * @param parentId The id of the parent media item whose list of children
    288      *            will be subscribed.
    289      * @param callback The callback to receive the list of children.
    290      */
    291     public void subscribe(@NonNull String parentId, @NonNull SubscriptionCallback callback) {
    292         // Check arguments.
    293         if (TextUtils.isEmpty(parentId)) {
    294             throw new IllegalArgumentException("parentId is empty");
    295         }
    296         if (callback == null) {
    297             throw new IllegalArgumentException("callback is null");
    298         }
    299         mImpl.subscribe(parentId, null, callback);
    300     }
    301 
    302     /**
    303      * Queries with service-specific arguments for information about the media items
    304      * that are contained within the specified id and subscribes to receive updates
    305      * when they change.
    306      * <p>
    307      * The list of subscriptions is maintained even when not connected and is
    308      * restored after the reconnection. It is ok to subscribe while not connected
    309      * but the results will not be returned until the connection completes.
    310      * </p>
    311      * <p>
    312      * If the id is already subscribed with a different callback then the new
    313      * callback will replace the previous one and the child data will be
    314      * reloaded.
    315      * </p>
    316      *
    317      * @param parentId The id of the parent media item whose list of children
    318      *            will be subscribed.
    319      * @param options A bundle of service-specific arguments to send to the media
    320      *            browse service. The contents of this bundle may affect the
    321      *            information returned when browsing.
    322      * @param callback The callback to receive the list of children.
    323      */
    324     public void subscribe(@NonNull String parentId, @NonNull Bundle options,
    325             @NonNull SubscriptionCallback callback) {
    326         // Check arguments.
    327         if (TextUtils.isEmpty(parentId)) {
    328             throw new IllegalArgumentException("parentId is empty");
    329         }
    330         if (callback == null) {
    331             throw new IllegalArgumentException("callback is null");
    332         }
    333         if (options == null) {
    334             throw new IllegalArgumentException("options are null");
    335         }
    336         mImpl.subscribe(parentId, options, callback);
    337     }
    338 
    339     /**
    340      * Unsubscribes for changes to the children of the specified media id.
    341      * <p>
    342      * The query callback will no longer be invoked for results associated with
    343      * this id once this method returns.
    344      * </p>
    345      *
    346      * @param parentId The id of the parent media item whose list of children
    347      *            will be unsubscribed.
    348      */
    349     public void unsubscribe(@NonNull String parentId) {
    350         // Check arguments.
    351         if (TextUtils.isEmpty(parentId)) {
    352             throw new IllegalArgumentException("parentId is empty");
    353         }
    354         mImpl.unsubscribe(parentId, null);
    355     }
    356 
    357     /**
    358      * Unsubscribes for changes to the children of the specified media id.
    359      * <p>
    360      * The query callback will no longer be invoked for results associated with
    361      * this id once this method returns.
    362      * </p>
    363      *
    364      * @param parentId The id of the parent media item whose list of children
    365      *            will be unsubscribed.
    366      * @param callback A callback sent to the media browse service to subscribe.
    367      */
    368     public void unsubscribe(@NonNull String parentId, @NonNull SubscriptionCallback callback) {
    369         // Check arguments.
    370         if (TextUtils.isEmpty(parentId)) {
    371             throw new IllegalArgumentException("parentId is empty");
    372         }
    373         if (callback == null) {
    374             throw new IllegalArgumentException("callback is null");
    375         }
    376         mImpl.unsubscribe(parentId, callback);
    377     }
    378 
    379     /**
    380      * Retrieves a specific {@link MediaItem} from the connected service. Not
    381      * all services may support this, so falling back to subscribing to the
    382      * parent's id should be used when unavailable.
    383      *
    384      * @param mediaId The id of the item to retrieve.
    385      * @param cb The callback to receive the result on.
    386      */
    387     public void getItem(final @NonNull String mediaId, @NonNull final ItemCallback cb) {
    388         mImpl.getItem(mediaId, cb);
    389     }
    390 
    391     /**
    392      * Searches {@link MediaItem media items} from the connected service. Not all services may
    393      * support this, and {@link SearchCallback#onError} will be called if not implemented.
    394      *
    395      * @param query The search query that contains keywords separated by space. Should not be an
    396      *            empty string.
    397      * @param extras The bundle of service-specific arguments to send to the media browser service.
    398      *            The contents of this bundle may affect the search result.
    399      * @param callback The callback to receive the search result. Must be non-null.
    400      * @throws IllegalStateException if the browser is not connected to the media browser service.
    401      */
    402     public void search(@NonNull final String query, final Bundle extras,
    403             @NonNull SearchCallback callback) {
    404         if (TextUtils.isEmpty(query)) {
    405             throw new IllegalArgumentException("query cannot be empty");
    406         }
    407         if (callback == null) {
    408             throw new IllegalArgumentException("callback cannot be null");
    409         }
    410         mImpl.search(query, extras, callback);
    411     }
    412 
    413     /**
    414      * Sends a custom action to the connected service. If the service doesn't support the given
    415      * action, {@link CustomActionCallback#onError} will be called.
    416      *
    417      * @param action The custom action that will be sent to the connected service. Should not be an
    418      *            empty string.
    419      * @param extras The bundle of service-specific arguments to send to the media browser service.
    420      * @param callback The callback to receive the result of the custom action.
    421      * @see #CUSTOM_ACTION_DOWNLOAD
    422      * @see #CUSTOM_ACTION_REMOVE_DOWNLOADED_FILE
    423      */
    424     public void sendCustomAction(@NonNull String action, Bundle extras,
    425             @Nullable CustomActionCallback callback) {
    426         if (TextUtils.isEmpty(action)) {
    427             throw new IllegalArgumentException("action cannot be empty");
    428         }
    429         mImpl.sendCustomAction(action, extras, callback);
    430     }
    431 
    432     /**
    433      * Gets the options which is passed to {@link MediaBrowserServiceCompat#notifyChildrenChanged(
    434      * String, Bundle)} call that triggered {@link SubscriptionCallback#onChildrenLoaded}.
    435      * This should be called inside of {@link SubscriptionCallback#onChildrenLoaded}.
    436      *
    437      * @return A bundle which is passed to {@link MediaBrowserServiceCompat#notifyChildrenChanged(
    438      *         String, Bundle)}
    439      * @hide
    440      */
    441     @RestrictTo(LIBRARY)
    442     public @Nullable Bundle getNotifyChildrenChangedOptions() {
    443         return mImpl.getNotifyChildrenChangedOptions();
    444     }
    445 
    446     /**
    447      * A class with information on a single media item for use in browsing/searching media.
    448      * MediaItems are application dependent so we cannot guarantee that they contain the
    449      * right values.
    450      */
    451     public static class MediaItem implements Parcelable {
    452         private final int mFlags;
    453         private final MediaDescriptionCompat mDescription;
    454 
    455         /** @hide */
    456         @RestrictTo(LIBRARY_GROUP)
    457         @Retention(RetentionPolicy.SOURCE)
    458         @IntDef(flag=true, value = { FLAG_BROWSABLE, FLAG_PLAYABLE })
    459         public @interface Flags { }
    460 
    461         /**
    462          * Flag: Indicates that the item has children of its own.
    463          */
    464         public static final int FLAG_BROWSABLE = 1 << 0;
    465 
    466         /**
    467          * Flag: Indicates that the item is playable.
    468          * <p>
    469          * The id of this item may be passed to
    470          * {@link TransportControls#playFromMediaId(String, Bundle)}
    471          * to start playing it.
    472          * </p>
    473          */
    474         public static final int FLAG_PLAYABLE = 1 << 1;
    475 
    476         /**
    477          * Creates an instance from a framework {@link android.media.browse.MediaBrowser.MediaItem}
    478          * object.
    479          * <p>
    480          * This method is only supported on API 21+. On API 20 and below, it returns null.
    481          * </p>
    482          *
    483          * @param itemObj A {@link android.media.browse.MediaBrowser.MediaItem} object.
    484          * @return An equivalent {@link MediaItem} object, or null if none.
    485          */
    486         public static MediaItem fromMediaItem(Object itemObj) {
    487             if (itemObj == null || Build.VERSION.SDK_INT < 21) {
    488                 return null;
    489             }
    490             int flags = MediaBrowserCompatApi21.MediaItem.getFlags(itemObj);
    491             MediaDescriptionCompat description =
    492                     MediaDescriptionCompat.fromMediaDescription(
    493                             MediaBrowserCompatApi21.MediaItem.getDescription(itemObj));
    494             return new MediaItem(description, flags);
    495         }
    496 
    497         /**
    498          * Creates a list of {@link MediaItem} objects from a framework
    499          * {@link android.media.browse.MediaBrowser.MediaItem} object list.
    500          * <p>
    501          * This method is only supported on API 21+. On API 20 and below, it returns null.
    502          * </p>
    503          *
    504          * @param itemList A list of {@link android.media.browse.MediaBrowser.MediaItem} objects.
    505          * @return An equivalent list of {@link MediaItem} objects, or null if none.
    506          */
    507         public static List<MediaItem> fromMediaItemList(List<?> itemList) {
    508             if (itemList == null || Build.VERSION.SDK_INT < 21) {
    509                 return null;
    510             }
    511             List<MediaItem> items = new ArrayList<>(itemList.size());
    512             for (Object itemObj : itemList) {
    513                 items.add(fromMediaItem(itemObj));
    514             }
    515             return items;
    516         }
    517 
    518         /**
    519          * Create a new MediaItem for use in browsing media.
    520          * @param description The description of the media, which must include a
    521          *            media id.
    522          * @param flags The flags for this item.
    523          */
    524         public MediaItem(@NonNull MediaDescriptionCompat description, @Flags int flags) {
    525             if (description == null) {
    526                 throw new IllegalArgumentException("description cannot be null");
    527             }
    528             if (TextUtils.isEmpty(description.getMediaId())) {
    529                 throw new IllegalArgumentException("description must have a non-empty media id");
    530             }
    531             mFlags = flags;
    532             mDescription = description;
    533         }
    534 
    535         /**
    536          * Private constructor.
    537          */
    538         MediaItem(Parcel in) {
    539             mFlags = in.readInt();
    540             mDescription = MediaDescriptionCompat.CREATOR.createFromParcel(in);
    541         }
    542 
    543         @Override
    544         public int describeContents() {
    545             return 0;
    546         }
    547 
    548         @Override
    549         public void writeToParcel(Parcel out, int flags) {
    550             out.writeInt(mFlags);
    551             mDescription.writeToParcel(out, flags);
    552         }
    553 
    554         @Override
    555         public String toString() {
    556             final StringBuilder sb = new StringBuilder("MediaItem{");
    557             sb.append("mFlags=").append(mFlags);
    558             sb.append(", mDescription=").append(mDescription);
    559             sb.append('}');
    560             return sb.toString();
    561         }
    562 
    563         public static final Parcelable.Creator<MediaItem> CREATOR =
    564                 new Parcelable.Creator<MediaItem>() {
    565                     @Override
    566                     public MediaItem createFromParcel(Parcel in) {
    567                         return new MediaItem(in);
    568                     }
    569 
    570                     @Override
    571                     public MediaItem[] newArray(int size) {
    572                         return new MediaItem[size];
    573                     }
    574                 };
    575 
    576         /**
    577          * Gets the flags of the item.
    578          */
    579         public @Flags int getFlags() {
    580             return mFlags;
    581         }
    582 
    583         /**
    584          * Returns whether this item is browsable.
    585          * @see #FLAG_BROWSABLE
    586          */
    587         public boolean isBrowsable() {
    588             return (mFlags & FLAG_BROWSABLE) != 0;
    589         }
    590 
    591         /**
    592          * Returns whether this item is playable.
    593          * @see #FLAG_PLAYABLE
    594          */
    595         public boolean isPlayable() {
    596             return (mFlags & FLAG_PLAYABLE) != 0;
    597         }
    598 
    599         /**
    600          * Returns the description of the media.
    601          */
    602         public @NonNull MediaDescriptionCompat getDescription() {
    603             return mDescription;
    604         }
    605 
    606         /**
    607          * Returns the media id in the {@link MediaDescriptionCompat} for this item.
    608          * @see MediaMetadataCompat#METADATA_KEY_MEDIA_ID
    609          */
    610         public @Nullable String getMediaId() {
    611             return mDescription.getMediaId();
    612         }
    613     }
    614 
    615     /**
    616      * Callbacks for connection related events.
    617      */
    618     public static class ConnectionCallback {
    619         final Object mConnectionCallbackObj;
    620         ConnectionCallbackInternal mConnectionCallbackInternal;
    621 
    622         public ConnectionCallback() {
    623             if (Build.VERSION.SDK_INT >= 21) {
    624                 mConnectionCallbackObj =
    625                         MediaBrowserCompatApi21.createConnectionCallback(new StubApi21());
    626             } else {
    627                 mConnectionCallbackObj = null;
    628             }
    629         }
    630 
    631         /**
    632          * Invoked after {@link MediaBrowserCompat#connect()} when the request has successfully
    633          * completed.
    634          */
    635         public void onConnected() {
    636         }
    637 
    638         /**
    639          * Invoked when the client is disconnected from the media browser.
    640          */
    641         public void onConnectionSuspended() {
    642         }
    643 
    644         /**
    645          * Invoked when the connection to the media browser failed.
    646          */
    647         public void onConnectionFailed() {
    648         }
    649 
    650         void setInternalConnectionCallback(ConnectionCallbackInternal connectionCallbackInternal) {
    651             mConnectionCallbackInternal = connectionCallbackInternal;
    652         }
    653 
    654         interface ConnectionCallbackInternal {
    655             void onConnected();
    656             void onConnectionSuspended();
    657             void onConnectionFailed();
    658         }
    659 
    660         private class StubApi21 implements MediaBrowserCompatApi21.ConnectionCallback {
    661             StubApi21() {
    662             }
    663 
    664             @Override
    665             public void onConnected() {
    666                 if (mConnectionCallbackInternal != null) {
    667                     mConnectionCallbackInternal.onConnected();
    668                 }
    669                 ConnectionCallback.this.onConnected();
    670             }
    671 
    672             @Override
    673             public void onConnectionSuspended() {
    674                 if (mConnectionCallbackInternal != null) {
    675                     mConnectionCallbackInternal.onConnectionSuspended();
    676                 }
    677                 ConnectionCallback.this.onConnectionSuspended();
    678             }
    679 
    680             @Override
    681             public void onConnectionFailed() {
    682                 if (mConnectionCallbackInternal != null) {
    683                     mConnectionCallbackInternal.onConnectionFailed();
    684                 }
    685                 ConnectionCallback.this.onConnectionFailed();
    686             }
    687         }
    688     }
    689 
    690     /**
    691      * Callbacks for subscription related events.
    692      */
    693     public static abstract class SubscriptionCallback {
    694         private final Object mSubscriptionCallbackObj;
    695         private final IBinder mToken;
    696         WeakReference<Subscription> mSubscriptionRef;
    697 
    698         public SubscriptionCallback() {
    699             mToken = new Binder();
    700             if (Build.VERSION.SDK_INT >= 26) {
    701                 mSubscriptionCallbackObj =
    702                         MediaBrowserCompatApi26.createSubscriptionCallback(new StubApi26());
    703             } else if (Build.VERSION.SDK_INT >= 21) {
    704                 mSubscriptionCallbackObj =
    705                         MediaBrowserCompatApi21.createSubscriptionCallback(new StubApi21());
    706             } else {
    707                 mSubscriptionCallbackObj = null;
    708             }
    709         }
    710 
    711         /**
    712          * Called when the list of children is loaded or updated.
    713          *
    714          * @param parentId The media id of the parent media item.
    715          * @param children The children which were loaded.
    716          */
    717         public void onChildrenLoaded(@NonNull String parentId, @NonNull List<MediaItem> children) {
    718         }
    719 
    720         /**
    721          * Called when the list of children is loaded or updated.
    722          *
    723          * @param parentId The media id of the parent media item.
    724          * @param children The children which were loaded.
    725          * @param options A bundle of service-specific arguments to send to the media
    726          *            browse service. The contents of this bundle may affect the
    727          *            information returned when browsing.
    728          */
    729         public void onChildrenLoaded(@NonNull String parentId, @NonNull List<MediaItem> children,
    730                 @NonNull Bundle options) {
    731         }
    732 
    733         /**
    734          * Called when the id doesn't exist or other errors in subscribing.
    735          * <p>
    736          * If this is called, the subscription remains until {@link MediaBrowserCompat#unsubscribe}
    737          * called, because some errors may heal themselves.
    738          * </p>
    739          *
    740          * @param parentId The media id of the parent media item whose children could not be loaded.
    741          */
    742         public void onError(@NonNull String parentId) {
    743         }
    744 
    745         /**
    746          * Called when the id doesn't exist or other errors in subscribing.
    747          * <p>
    748          * If this is called, the subscription remains until {@link MediaBrowserCompat#unsubscribe}
    749          * called, because some errors may heal themselves.
    750          * </p>
    751          *
    752          * @param parentId The media id of the parent media item whose children could
    753          *            not be loaded.
    754          * @param options A bundle of service-specific arguments sent to the media
    755          *            browse service.
    756          */
    757         public void onError(@NonNull String parentId, @NonNull Bundle options) {
    758         }
    759 
    760         private void setSubscription(Subscription subscription) {
    761             mSubscriptionRef = new WeakReference<>(subscription);
    762         }
    763 
    764         private class StubApi21 implements MediaBrowserCompatApi21.SubscriptionCallback {
    765             StubApi21() {
    766             }
    767 
    768             @Override
    769             public void onChildrenLoaded(@NonNull String parentId, List<?> children) {
    770                 Subscription sub = mSubscriptionRef == null ? null : mSubscriptionRef.get();
    771                 if (sub == null) {
    772                     SubscriptionCallback.this.onChildrenLoaded(
    773                             parentId, MediaItem.fromMediaItemList(children));
    774                 } else {
    775                     List<MediaBrowserCompat.MediaItem> itemList =
    776                             MediaItem.fromMediaItemList(children);
    777                     final List<SubscriptionCallback> callbacks = sub.getCallbacks();
    778                     final List<Bundle> optionsList = sub.getOptionsList();
    779                     for (int i = 0; i < callbacks.size(); ++i) {
    780                         Bundle options = optionsList.get(i);
    781                         if (options == null) {
    782                             SubscriptionCallback.this.onChildrenLoaded(parentId, itemList);
    783                         } else {
    784                             SubscriptionCallback.this.onChildrenLoaded(
    785                                     parentId, applyOptions(itemList, options), options);
    786                         }
    787                     }
    788                 }
    789             }
    790 
    791             @Override
    792             public void onError(@NonNull String parentId) {
    793                 SubscriptionCallback.this.onError(parentId);
    794             }
    795 
    796             List<MediaBrowserCompat.MediaItem> applyOptions(List<MediaBrowserCompat.MediaItem> list,
    797                     final Bundle options) {
    798                 if (list == null) {
    799                     return null;
    800                 }
    801                 int page = options.getInt(MediaBrowserCompat.EXTRA_PAGE, -1);
    802                 int pageSize = options.getInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, -1);
    803                 if (page == -1 && pageSize == -1) {
    804                     return list;
    805                 }
    806                 int fromIndex = pageSize * page;
    807                 int toIndex = fromIndex + pageSize;
    808                 if (page < 0 || pageSize < 1 || fromIndex >= list.size()) {
    809                     return Collections.EMPTY_LIST;
    810                 }
    811                 if (toIndex > list.size()) {
    812                     toIndex = list.size();
    813                 }
    814                 return list.subList(fromIndex, toIndex);
    815             }
    816 
    817         }
    818 
    819         private class StubApi26 extends StubApi21
    820                 implements MediaBrowserCompatApi26.SubscriptionCallback {
    821             StubApi26() {
    822             }
    823 
    824             @Override
    825             public void onChildrenLoaded(@NonNull String parentId, List<?> children,
    826                     @NonNull Bundle options) {
    827                 SubscriptionCallback.this.onChildrenLoaded(
    828                         parentId, MediaItem.fromMediaItemList(children), options);
    829             }
    830 
    831             @Override
    832             public void onError(@NonNull String parentId, @NonNull Bundle options) {
    833                 SubscriptionCallback.this.onError(parentId, options);
    834             }
    835         }
    836     }
    837 
    838     /**
    839      * Callback for receiving the result of {@link #getItem}.
    840      */
    841     public static abstract class ItemCallback {
    842         final Object mItemCallbackObj;
    843 
    844         public ItemCallback() {
    845             if (Build.VERSION.SDK_INT >= 23) {
    846                 mItemCallbackObj = MediaBrowserCompatApi23.createItemCallback(new StubApi23());
    847             } else {
    848                 mItemCallbackObj = null;
    849             }
    850         }
    851 
    852         /**
    853          * Called when the item has been returned by the browser service.
    854          *
    855          * @param item The item that was returned or null if it doesn't exist.
    856          */
    857         public void onItemLoaded(MediaItem item) {
    858         }
    859 
    860         /**
    861          * Called when the item doesn't exist or there was an error retrieving it.
    862          *
    863          * @param itemId The media id of the media item which could not be loaded.
    864          */
    865         public void onError(@NonNull String itemId) {
    866         }
    867 
    868         private class StubApi23 implements MediaBrowserCompatApi23.ItemCallback {
    869             StubApi23() {
    870             }
    871 
    872             @Override
    873             public void onItemLoaded(Parcel itemParcel) {
    874                 if (itemParcel == null) {
    875                     ItemCallback.this.onItemLoaded(null);
    876                 } else {
    877                     itemParcel.setDataPosition(0);
    878                     MediaItem item =
    879                             MediaBrowserCompat.MediaItem.CREATOR.createFromParcel(itemParcel);
    880                     itemParcel.recycle();
    881                     ItemCallback.this.onItemLoaded(item);
    882                 }
    883             }
    884 
    885             @Override
    886             public void onError(@NonNull String itemId) {
    887                 ItemCallback.this.onError(itemId);
    888             }
    889         }
    890     }
    891 
    892     /**
    893      * Callback for receiving the result of {@link #search}.
    894      */
    895     public abstract static class SearchCallback {
    896         /**
    897          * Called when the {@link #search} finished successfully.
    898          *
    899          * @param query The search query sent for the search request to the connected service.
    900          * @param extras The bundle of service-specific arguments sent to the connected service.
    901          * @param items The list of media items which contains the search result.
    902          */
    903         public void onSearchResult(@NonNull String query, Bundle extras,
    904                 @NonNull List<MediaItem> items) {
    905         }
    906 
    907         /**
    908          * Called when an error happens while {@link #search} or the connected service doesn't
    909          * support {@link #search}.
    910          *
    911          * @param query The search query sent for the search request to the connected service.
    912          * @param extras The bundle of service-specific arguments sent to the connected service.
    913          */
    914         public void onError(@NonNull String query, Bundle extras) {
    915         }
    916     }
    917 
    918     /**
    919      * Callback for receiving the result of {@link #sendCustomAction}.
    920      */
    921     public abstract static class CustomActionCallback {
    922         /**
    923          * Called when an interim update was delivered from the connected service while performing
    924          * the custom action.
    925          *
    926          * @param action The custom action sent to the connected service.
    927          * @param extras The bundle of service-specific arguments sent to the connected service.
    928          * @param data The additional data delivered from the connected service.
    929          */
    930         public void onProgressUpdate(String action, Bundle extras, Bundle data) {
    931         }
    932 
    933         /**
    934          * Called when the custom action finished successfully.
    935          *
    936          * @param action The custom action sent to the connected service.
    937          * @param extras The bundle of service-specific arguments sent to the connected service.
    938          * @param resultData The additional data delivered from the connected service.
    939          */
    940         public void onResult(String action, Bundle extras, Bundle resultData) {
    941         }
    942 
    943         /**
    944          * Called when an error happens while performing the custom action or the connected service
    945          * doesn't support the requested custom action.
    946          *
    947          * @param action The custom action sent to the connected service.
    948          * @param extras The bundle of service-specific arguments sent to the connected service.
    949          * @param data The additional data delivered from the connected service.
    950          */
    951         public void onError(String action, Bundle extras, Bundle data) {
    952         }
    953     }
    954 
    955     interface MediaBrowserImpl {
    956         void connect();
    957         void disconnect();
    958         boolean isConnected();
    959         ComponentName getServiceComponent();
    960         @NonNull String getRoot();
    961         @Nullable Bundle getExtras();
    962         @NonNull MediaSessionCompat.Token getSessionToken();
    963         void subscribe(@NonNull String parentId, @Nullable Bundle options,
    964                 @NonNull SubscriptionCallback callback);
    965         void unsubscribe(@NonNull String parentId, SubscriptionCallback callback);
    966         void getItem(@NonNull String mediaId, @NonNull ItemCallback cb);
    967         void search(@NonNull String query, Bundle extras, @NonNull SearchCallback callback);
    968         void sendCustomAction(@NonNull String action, Bundle extras,
    969                 @Nullable CustomActionCallback callback);
    970         @Nullable Bundle getNotifyChildrenChangedOptions();
    971     }
    972 
    973     interface MediaBrowserServiceCallbackImpl {
    974         void onServiceConnected(Messenger callback, String root, MediaSessionCompat.Token session,
    975                 Bundle extra);
    976         void onConnectionFailed(Messenger callback);
    977         void onLoadChildren(Messenger callback, String parentId, List list, Bundle options,
    978                 Bundle notifyChildrenChangedOptions);
    979     }
    980 
    981     static class MediaBrowserImplBase
    982             implements MediaBrowserImpl, MediaBrowserServiceCallbackImpl {
    983         static final int CONNECT_STATE_DISCONNECTING = 0;
    984         static final int CONNECT_STATE_DISCONNECTED = 1;
    985         static final int CONNECT_STATE_CONNECTING = 2;
    986         static final int CONNECT_STATE_CONNECTED = 3;
    987         static final int CONNECT_STATE_SUSPENDED = 4;
    988 
    989         final Context mContext;
    990         final ComponentName mServiceComponent;
    991         final ConnectionCallback mCallback;
    992         final Bundle mRootHints;
    993         final CallbackHandler mHandler = new CallbackHandler(this);
    994         private final ArrayMap<String, Subscription> mSubscriptions = new ArrayMap<>();
    995 
    996         int mState = CONNECT_STATE_DISCONNECTED;
    997         MediaServiceConnection mServiceConnection;
    998         ServiceBinderWrapper mServiceBinderWrapper;
    999         Messenger mCallbacksMessenger;
   1000         private String mRootId;
   1001         private MediaSessionCompat.Token mMediaSessionToken;
   1002         private Bundle mExtras;
   1003         private Bundle mNotifyChildrenChangedOptions;
   1004 
   1005         public MediaBrowserImplBase(Context context, ComponentName serviceComponent,
   1006                 ConnectionCallback callback, Bundle rootHints) {
   1007             if (context == null) {
   1008                 throw new IllegalArgumentException("context must not be null");
   1009             }
   1010             if (serviceComponent == null) {
   1011                 throw new IllegalArgumentException("service component must not be null");
   1012             }
   1013             if (callback == null) {
   1014                 throw new IllegalArgumentException("connection callback must not be null");
   1015             }
   1016             mContext = context;
   1017             mServiceComponent = serviceComponent;
   1018             mCallback = callback;
   1019             mRootHints = rootHints == null ? null : new Bundle(rootHints);
   1020         }
   1021 
   1022         @Override
   1023         public void connect() {
   1024             if (mState != CONNECT_STATE_DISCONNECTING && mState != CONNECT_STATE_DISCONNECTED) {
   1025                 throw new IllegalStateException("connect() called while neigther disconnecting nor "
   1026                         + "disconnected (state=" + getStateLabel(mState) + ")");
   1027             }
   1028 
   1029             mState = CONNECT_STATE_CONNECTING;
   1030             mHandler.post(new Runnable() {
   1031                 @Override
   1032                 public void run() {
   1033                     // mState could be changed by the Runnable of disconnect()
   1034                     if (mState == CONNECT_STATE_DISCONNECTING) {
   1035                         return;
   1036                     }
   1037                     mState = CONNECT_STATE_CONNECTING;
   1038                     // TODO: remove this extra check.
   1039                     if (DEBUG) {
   1040                         if (mServiceConnection != null) {
   1041                             throw new RuntimeException("mServiceConnection should be null. Instead "
   1042                                     + "it is " + mServiceConnection);
   1043                         }
   1044                     }
   1045                     if (mServiceBinderWrapper != null) {
   1046                         throw new RuntimeException("mServiceBinderWrapper should be null. Instead "
   1047                                 + "it is " + mServiceBinderWrapper);
   1048                     }
   1049                     if (mCallbacksMessenger != null) {
   1050                         throw new RuntimeException("mCallbacksMessenger should be null. Instead "
   1051                                 + "it is " + mCallbacksMessenger);
   1052                     }
   1053 
   1054                     final Intent intent = new Intent(MediaBrowserServiceCompat.SERVICE_INTERFACE);
   1055                     intent.setComponent(mServiceComponent);
   1056 
   1057                     mServiceConnection = new MediaServiceConnection();
   1058                     boolean bound = false;
   1059                     try {
   1060                         bound = mContext.bindService(intent, mServiceConnection,
   1061                                 Context.BIND_AUTO_CREATE);
   1062                     } catch (Exception ex) {
   1063                         Log.e(TAG, "Failed binding to service " + mServiceComponent);
   1064                     }
   1065 
   1066                     if (!bound) {
   1067                         // Tell them that it didn't work.
   1068                         forceCloseConnection();
   1069                         mCallback.onConnectionFailed();
   1070                     }
   1071 
   1072                     if (DEBUG) {
   1073                         Log.d(TAG, "connect...");
   1074                         dump();
   1075                     }
   1076                 }
   1077             });
   1078         }
   1079 
   1080         @Override
   1081         public void disconnect() {
   1082             // It's ok to call this any state, because allowing this lets apps not have
   1083             // to check isConnected() unnecessarily. They won't appreciate the extra
   1084             // assertions for this. We do everything we can here to go back to a sane state.
   1085             mState = CONNECT_STATE_DISCONNECTING;
   1086             mHandler.post(new Runnable() {
   1087                 @Override
   1088                 public void run() {
   1089                     // connect() could be called before this. Then we will disconnect and reconnect.
   1090                     if (mCallbacksMessenger != null) {
   1091                         try {
   1092                             mServiceBinderWrapper.disconnect(mCallbacksMessenger);
   1093                         } catch (RemoteException ex) {
   1094                             // We are disconnecting anyway. Log, just for posterity but it's not
   1095                             // a big problem.
   1096                             Log.w(TAG, "RemoteException during connect for " + mServiceComponent);
   1097                         }
   1098                     }
   1099                     int state = mState;
   1100                     forceCloseConnection();
   1101                     // If the state was not CONNECT_STATE_DISCONNECTING, keep the state so that
   1102                     // the operation came after disconnect() can be handled properly.
   1103                     if (state != CONNECT_STATE_DISCONNECTING) {
   1104                         mState = state;
   1105                     }
   1106                     if (DEBUG) {
   1107                         Log.d(TAG, "disconnect...");
   1108                         dump();
   1109                     }
   1110                 }
   1111             });
   1112         }
   1113 
   1114         /**
   1115          * Null out the variables and unbind from the service. This doesn't include
   1116          * calling disconnect on the service, because we only try to do that in the
   1117          * clean shutdown cases.
   1118          * <p>
   1119          * Everywhere that calls this EXCEPT for disconnect() should follow it with
   1120          * a call to mCallback.onConnectionFailed(). Disconnect doesn't do that callback
   1121          * for a clean shutdown, but everywhere else is a dirty shutdown and should
   1122          * notify the app.
   1123          */
   1124         void forceCloseConnection() {
   1125             if (mServiceConnection != null) {
   1126                 mContext.unbindService(mServiceConnection);
   1127             }
   1128             mState = CONNECT_STATE_DISCONNECTED;
   1129             mServiceConnection = null;
   1130             mServiceBinderWrapper = null;
   1131             mCallbacksMessenger = null;
   1132             mHandler.setCallbacksMessenger(null);
   1133             mRootId = null;
   1134             mMediaSessionToken = null;
   1135         }
   1136 
   1137         @Override
   1138         public boolean isConnected() {
   1139             return mState == CONNECT_STATE_CONNECTED;
   1140         }
   1141 
   1142         @Override
   1143         public @NonNull ComponentName getServiceComponent() {
   1144             if (!isConnected()) {
   1145                 throw new IllegalStateException("getServiceComponent() called while not connected" +
   1146                         " (state=" + mState + ")");
   1147             }
   1148             return mServiceComponent;
   1149         }
   1150 
   1151         @Override
   1152         public @NonNull String getRoot() {
   1153             if (!isConnected()) {
   1154                 throw new IllegalStateException("getRoot() called while not connected"
   1155                         + "(state=" + getStateLabel(mState) + ")");
   1156             }
   1157             return mRootId;
   1158         }
   1159 
   1160         @Override
   1161         public @Nullable Bundle getExtras() {
   1162             if (!isConnected()) {
   1163                 throw new IllegalStateException("getExtras() called while not connected (state="
   1164                         + getStateLabel(mState) + ")");
   1165             }
   1166             return mExtras;
   1167         }
   1168 
   1169         @Override
   1170         public @NonNull MediaSessionCompat.Token getSessionToken() {
   1171             if (!isConnected()) {
   1172                 throw new IllegalStateException("getSessionToken() called while not connected"
   1173                         + "(state=" + mState + ")");
   1174             }
   1175             return mMediaSessionToken;
   1176         }
   1177 
   1178         @Override
   1179         public void subscribe(@NonNull String parentId, Bundle options,
   1180                 @NonNull SubscriptionCallback callback) {
   1181             // Update or create the subscription.
   1182             Subscription sub = mSubscriptions.get(parentId);
   1183             if (sub == null) {
   1184                 sub = new Subscription();
   1185                 mSubscriptions.put(parentId, sub);
   1186             }
   1187             Bundle copiedOptions = options == null ? null : new Bundle(options);
   1188             sub.putCallback(mContext, copiedOptions, callback);
   1189 
   1190             // If we are connected, tell the service that we are watching. If we aren't
   1191             // connected, the service will be told when we connect.
   1192             if (isConnected()) {
   1193                 try {
   1194                     mServiceBinderWrapper.addSubscription(parentId, callback.mToken, copiedOptions,
   1195                             mCallbacksMessenger);
   1196                 } catch (RemoteException e) {
   1197                     // Process is crashing. We will disconnect, and upon reconnect we will
   1198                     // automatically reregister. So nothing to do here.
   1199                     Log.d(TAG, "addSubscription failed with RemoteException parentId=" + parentId);
   1200                 }
   1201             }
   1202         }
   1203 
   1204         @Override
   1205         public void unsubscribe(@NonNull String parentId, SubscriptionCallback callback) {
   1206             Subscription sub = mSubscriptions.get(parentId);
   1207             if (sub == null) {
   1208                 return;
   1209             }
   1210 
   1211             // Tell the service if necessary.
   1212             try {
   1213                 if (callback == null) {
   1214                     if (isConnected()) {
   1215                         mServiceBinderWrapper.removeSubscription(parentId, null,
   1216                                 mCallbacksMessenger);
   1217                     }
   1218                 } else {
   1219                     final List<SubscriptionCallback> callbacks = sub.getCallbacks();
   1220                     final List<Bundle> optionsList = sub.getOptionsList();
   1221                     for (int i = callbacks.size() - 1; i >= 0; --i) {
   1222                         if (callbacks.get(i) == callback) {
   1223                             if (isConnected()) {
   1224                                 mServiceBinderWrapper.removeSubscription(
   1225                                         parentId, callback.mToken, mCallbacksMessenger);
   1226                             }
   1227                             callbacks.remove(i);
   1228                             optionsList.remove(i);
   1229                         }
   1230                     }
   1231                 }
   1232             } catch (RemoteException ex) {
   1233                 // Process is crashing. We will disconnect, and upon reconnect we will
   1234                 // automatically reregister. So nothing to do here.
   1235                 Log.d(TAG, "removeSubscription failed with RemoteException parentId=" + parentId);
   1236             }
   1237 
   1238             if (sub.isEmpty() || callback == null) {
   1239                 mSubscriptions.remove(parentId);
   1240             }
   1241         }
   1242 
   1243         @Override
   1244         public void getItem(@NonNull final String mediaId, @NonNull final ItemCallback cb) {
   1245             if (TextUtils.isEmpty(mediaId)) {
   1246                 throw new IllegalArgumentException("mediaId is empty");
   1247             }
   1248             if (cb == null) {
   1249                 throw new IllegalArgumentException("cb is null");
   1250             }
   1251             if (!isConnected()) {
   1252                 Log.i(TAG, "Not connected, unable to retrieve the MediaItem.");
   1253                 mHandler.post(new Runnable() {
   1254                     @Override
   1255                     public void run() {
   1256                         cb.onError(mediaId);
   1257                     }
   1258                 });
   1259                 return;
   1260             }
   1261             ResultReceiver receiver = new ItemReceiver(mediaId, cb, mHandler);
   1262             try {
   1263                 mServiceBinderWrapper.getMediaItem(mediaId, receiver, mCallbacksMessenger);
   1264             } catch (RemoteException e) {
   1265                 Log.i(TAG, "Remote error getting media item: " + mediaId);
   1266                 mHandler.post(new Runnable() {
   1267                     @Override
   1268                     public void run() {
   1269                         cb.onError(mediaId);
   1270                     }
   1271                 });
   1272             }
   1273         }
   1274 
   1275         @Override
   1276         public void search(@NonNull final String query, final Bundle extras,
   1277                 @NonNull final SearchCallback callback) {
   1278             if (!isConnected()) {
   1279                 throw new IllegalStateException("search() called while not connected"
   1280                         + " (state=" + getStateLabel(mState) + ")");
   1281             }
   1282 
   1283             ResultReceiver receiver = new SearchResultReceiver(query, extras, callback, mHandler);
   1284             try {
   1285                 mServiceBinderWrapper.search(query, extras, receiver, mCallbacksMessenger);
   1286             } catch (RemoteException e) {
   1287                 Log.i(TAG, "Remote error searching items with query: " + query, e);
   1288                 mHandler.post(new Runnable() {
   1289                     @Override
   1290                     public void run() {
   1291                         callback.onError(query, extras);
   1292                     }
   1293                 });
   1294             }
   1295         }
   1296 
   1297         @Override
   1298         public void sendCustomAction(@NonNull final String action, final Bundle extras,
   1299                 @Nullable final CustomActionCallback callback) {
   1300             if (!isConnected()) {
   1301                 throw new IllegalStateException("Cannot send a custom action (" + action + ") with "
   1302                         + "extras " + extras + " because the browser is not connected to the "
   1303                         + "service.");
   1304             }
   1305 
   1306             ResultReceiver receiver = new CustomActionResultReceiver(action, extras, callback,
   1307                     mHandler);
   1308             try {
   1309                 mServiceBinderWrapper.sendCustomAction(action, extras, receiver,
   1310                         mCallbacksMessenger);
   1311             } catch (RemoteException e) {
   1312                 Log.i(TAG, "Remote error sending a custom action: action=" + action + ", extras="
   1313                         + extras, e);
   1314                 if (callback != null) {
   1315                     mHandler.post(new Runnable() {
   1316                         @Override
   1317                         public void run() {
   1318                             callback.onError(action, extras, null);
   1319                         }
   1320                     });
   1321                 }
   1322             }
   1323         }
   1324 
   1325         @Override
   1326         public void onServiceConnected(final Messenger callback, final String root,
   1327                 final MediaSessionCompat.Token session, final Bundle extra) {
   1328             // Check to make sure there hasn't been a disconnect or a different ServiceConnection.
   1329             if (!isCurrent(callback, "onConnect")) {
   1330                 return;
   1331             }
   1332             // Don't allow them to call us twice.
   1333             if (mState != CONNECT_STATE_CONNECTING) {
   1334                 Log.w(TAG, "onConnect from service while mState=" + getStateLabel(mState)
   1335                         + "... ignoring");
   1336                 return;
   1337             }
   1338             mRootId = root;
   1339             mMediaSessionToken = session;
   1340             mExtras = extra;
   1341             mState = CONNECT_STATE_CONNECTED;
   1342 
   1343             if (DEBUG) {
   1344                 Log.d(TAG, "ServiceCallbacks.onConnect...");
   1345                 dump();
   1346             }
   1347             mCallback.onConnected();
   1348 
   1349             // we may receive some subscriptions before we are connected, so re-subscribe
   1350             // everything now
   1351             try {
   1352                 for (Map.Entry<String, Subscription> subscriptionEntry
   1353                         : mSubscriptions.entrySet()) {
   1354                     String id = subscriptionEntry.getKey();
   1355                     Subscription sub = subscriptionEntry.getValue();
   1356                     List<SubscriptionCallback> callbackList = sub.getCallbacks();
   1357                     List<Bundle> optionsList = sub.getOptionsList();
   1358                     for (int i = 0; i < callbackList.size(); ++i) {
   1359                         mServiceBinderWrapper.addSubscription(id, callbackList.get(i).mToken,
   1360                                 optionsList.get(i), mCallbacksMessenger);
   1361                     }
   1362                 }
   1363             } catch (RemoteException ex) {
   1364                 // Process is crashing. We will disconnect, and upon reconnect we will
   1365                 // automatically reregister. So nothing to do here.
   1366                 Log.d(TAG, "addSubscription failed with RemoteException.");
   1367             }
   1368         }
   1369 
   1370         @Override
   1371         public void onConnectionFailed(final Messenger callback) {
   1372             Log.e(TAG, "onConnectFailed for " + mServiceComponent);
   1373 
   1374             // Check to make sure there hasn't been a disconnect or a different ServiceConnection.
   1375             if (!isCurrent(callback, "onConnectFailed")) {
   1376                 return;
   1377             }
   1378             // Don't allow them to call us twice.
   1379             if (mState != CONNECT_STATE_CONNECTING) {
   1380                 Log.w(TAG, "onConnect from service while mState=" + getStateLabel(mState)
   1381                         + "... ignoring");
   1382                 return;
   1383             }
   1384 
   1385             // Clean up
   1386             forceCloseConnection();
   1387 
   1388             // Tell the app.
   1389             mCallback.onConnectionFailed();
   1390         }
   1391 
   1392         @Override
   1393         public void onLoadChildren(final Messenger callback, final String parentId,
   1394                 final List list, final Bundle options, final Bundle notifyChildrenChangedOptions) {
   1395             // Check that there hasn't been a disconnect or a different ServiceConnection.
   1396             if (!isCurrent(callback, "onLoadChildren")) {
   1397                 return;
   1398             }
   1399 
   1400             if (DEBUG) {
   1401                 Log.d(TAG, "onLoadChildren for " + mServiceComponent + " id=" + parentId);
   1402             }
   1403 
   1404             // Check that the subscription is still subscribed.
   1405             final Subscription subscription = mSubscriptions.get(parentId);
   1406             if (subscription == null) {
   1407                 if (DEBUG) {
   1408                     Log.d(TAG, "onLoadChildren for id that isn't subscribed id=" + parentId);
   1409                 }
   1410                 return;
   1411             }
   1412 
   1413             // Tell the app.
   1414             SubscriptionCallback subscriptionCallback = subscription.getCallback(mContext, options);
   1415             if (subscriptionCallback != null) {
   1416                 if (options == null) {
   1417                     if (list == null) {
   1418                         subscriptionCallback.onError(parentId);
   1419                     } else {
   1420                         mNotifyChildrenChangedOptions = notifyChildrenChangedOptions;
   1421                         subscriptionCallback.onChildrenLoaded(parentId, list);
   1422                         mNotifyChildrenChangedOptions = null;
   1423                     }
   1424                 } else {
   1425                     if (list == null) {
   1426                         subscriptionCallback.onError(parentId, options);
   1427                     } else {
   1428                         mNotifyChildrenChangedOptions = notifyChildrenChangedOptions;
   1429                         subscriptionCallback.onChildrenLoaded(parentId, list, options);
   1430                         mNotifyChildrenChangedOptions = null;
   1431                     }
   1432                 }
   1433             }
   1434         }
   1435 
   1436         @Override
   1437         public Bundle getNotifyChildrenChangedOptions() {
   1438             return mNotifyChildrenChangedOptions;
   1439         }
   1440 
   1441         /**
   1442          * For debugging.
   1443          */
   1444         private static String getStateLabel(int state) {
   1445             switch (state) {
   1446                 case CONNECT_STATE_DISCONNECTING:
   1447                     return "CONNECT_STATE_DISCONNECTING";
   1448                 case CONNECT_STATE_DISCONNECTED:
   1449                     return "CONNECT_STATE_DISCONNECTED";
   1450                 case CONNECT_STATE_CONNECTING:
   1451                     return "CONNECT_STATE_CONNECTING";
   1452                 case CONNECT_STATE_CONNECTED:
   1453                     return "CONNECT_STATE_CONNECTED";
   1454                 case CONNECT_STATE_SUSPENDED:
   1455                     return "CONNECT_STATE_SUSPENDED";
   1456                 default:
   1457                     return "UNKNOWN/" + state;
   1458             }
   1459         }
   1460 
   1461         /**
   1462          * Return true if {@code callback} is the current ServiceCallbacks. Also logs if it's not.
   1463          */
   1464         @SuppressWarnings("ReferenceEquality")
   1465         private boolean isCurrent(Messenger callback, String funcName) {
   1466             if (mCallbacksMessenger != callback || mState == CONNECT_STATE_DISCONNECTING
   1467                     || mState == CONNECT_STATE_DISCONNECTED) {
   1468                 if (mState != CONNECT_STATE_DISCONNECTING && mState != CONNECT_STATE_DISCONNECTED) {
   1469                     Log.i(TAG, funcName + " for " + mServiceComponent + " with mCallbacksMessenger="
   1470                             + mCallbacksMessenger + " this=" + this);
   1471                 }
   1472                 return false;
   1473             }
   1474             return true;
   1475         }
   1476 
   1477         /**
   1478          * Log internal state.
   1479          */
   1480         void dump() {
   1481             Log.d(TAG, "MediaBrowserCompat...");
   1482             Log.d(TAG, "  mServiceComponent=" + mServiceComponent);
   1483             Log.d(TAG, "  mCallback=" + mCallback);
   1484             Log.d(TAG, "  mRootHints=" + mRootHints);
   1485             Log.d(TAG, "  mState=" + getStateLabel(mState));
   1486             Log.d(TAG, "  mServiceConnection=" + mServiceConnection);
   1487             Log.d(TAG, "  mServiceBinderWrapper=" + mServiceBinderWrapper);
   1488             Log.d(TAG, "  mCallbacksMessenger=" + mCallbacksMessenger);
   1489             Log.d(TAG, "  mRootId=" + mRootId);
   1490             Log.d(TAG, "  mMediaSessionToken=" + mMediaSessionToken);
   1491         }
   1492 
   1493         /**
   1494          * ServiceConnection to the other app.
   1495          */
   1496         private class MediaServiceConnection implements ServiceConnection {
   1497             MediaServiceConnection() {
   1498             }
   1499 
   1500             @Override
   1501             public void onServiceConnected(final ComponentName name, final IBinder binder) {
   1502                 postOrRun(new Runnable() {
   1503                     @Override
   1504                     public void run() {
   1505                         if (DEBUG) {
   1506                             Log.d(TAG, "MediaServiceConnection.onServiceConnected name=" + name
   1507                                     + " binder=" + binder);
   1508                             dump();
   1509                         }
   1510 
   1511                         // Make sure we are still the current connection, and that they haven't
   1512                         // called disconnect().
   1513                         if (!isCurrent("onServiceConnected")) {
   1514                             return;
   1515                         }
   1516 
   1517                         // Save their binder
   1518                         mServiceBinderWrapper = new ServiceBinderWrapper(binder, mRootHints);
   1519 
   1520                         // We make a new mServiceCallbacks each time we connect so that we can drop
   1521                         // responses from previous connections.
   1522                         mCallbacksMessenger = new Messenger(mHandler);
   1523                         mHandler.setCallbacksMessenger(mCallbacksMessenger);
   1524 
   1525                         mState = CONNECT_STATE_CONNECTING;
   1526 
   1527                         // Call connect, which is async. When we get a response from that we will
   1528                         // say that we're connected.
   1529                         try {
   1530                             if (DEBUG) {
   1531                                 Log.d(TAG, "ServiceCallbacks.onConnect...");
   1532                                 dump();
   1533                             }
   1534                             mServiceBinderWrapper.connect(mContext, mCallbacksMessenger);
   1535                         } catch (RemoteException ex) {
   1536                             // Connect failed, which isn't good. But the auto-reconnect on the
   1537                             // service will take over and we will come back. We will also get the
   1538                             // onServiceDisconnected, which has all the cleanup code. So let that
   1539                             // do it.
   1540                             Log.w(TAG, "RemoteException during connect for " + mServiceComponent);
   1541                             if (DEBUG) {
   1542                                 Log.d(TAG, "ServiceCallbacks.onConnect...");
   1543                                 dump();
   1544                             }
   1545                         }
   1546                     }
   1547                 });
   1548             }
   1549 
   1550             @Override
   1551             public void onServiceDisconnected(final ComponentName name) {
   1552                 postOrRun(new Runnable() {
   1553                     @Override
   1554                     public void run() {
   1555                         if (DEBUG) {
   1556                             Log.d(TAG, "MediaServiceConnection.onServiceDisconnected name=" + name
   1557                                     + " this=" + this + " mServiceConnection=" +
   1558                                     mServiceConnection);
   1559                             dump();
   1560                         }
   1561 
   1562                         // Make sure we are still the current connection, and that they haven't
   1563                         // called disconnect().
   1564                         if (!isCurrent("onServiceDisconnected")) {
   1565                             return;
   1566                         }
   1567 
   1568                         // Clear out what we set in onServiceConnected
   1569                         mServiceBinderWrapper = null;
   1570                         mCallbacksMessenger = null;
   1571                         mHandler.setCallbacksMessenger(null);
   1572 
   1573                         // And tell the app that it's suspended.
   1574                         mState = CONNECT_STATE_SUSPENDED;
   1575                         mCallback.onConnectionSuspended();
   1576                     }
   1577                 });
   1578             }
   1579 
   1580             private void postOrRun(Runnable r) {
   1581                 if (Thread.currentThread() == mHandler.getLooper().getThread()) {
   1582                     r.run();
   1583                 } else {
   1584                     mHandler.post(r);
   1585                 }
   1586             }
   1587 
   1588             /**
   1589              * Return true if this is the current ServiceConnection. Also logs if it's not.
   1590              */
   1591             boolean isCurrent(String funcName) {
   1592                 if (mServiceConnection != this || mState == CONNECT_STATE_DISCONNECTING
   1593                         || mState == CONNECT_STATE_DISCONNECTED) {
   1594                     if (mState != CONNECT_STATE_DISCONNECTING
   1595                             && mState != CONNECT_STATE_DISCONNECTED) {
   1596                         // Check mState, because otherwise this log is noisy.
   1597                         Log.i(TAG, funcName + " for " + mServiceComponent +
   1598                                 " with mServiceConnection=" + mServiceConnection + " this=" + this);
   1599                     }
   1600                     return false;
   1601                 }
   1602                 return true;
   1603             }
   1604         }
   1605     }
   1606 
   1607     @RequiresApi(21)
   1608     static class MediaBrowserImplApi21 implements MediaBrowserImpl, MediaBrowserServiceCallbackImpl,
   1609             ConnectionCallback.ConnectionCallbackInternal {
   1610         final Context mContext;
   1611         protected final Object mBrowserObj;
   1612         protected final Bundle mRootHints;
   1613         protected final CallbackHandler mHandler = new CallbackHandler(this);
   1614         private final ArrayMap<String, Subscription> mSubscriptions = new ArrayMap<>();
   1615 
   1616         protected int mServiceVersion;
   1617         protected ServiceBinderWrapper mServiceBinderWrapper;
   1618         protected Messenger mCallbacksMessenger;
   1619         private MediaSessionCompat.Token mMediaSessionToken;
   1620         private Bundle mNotifyChildrenChangedOptions;
   1621 
   1622         MediaBrowserImplApi21(Context context, ComponentName serviceComponent,
   1623                 ConnectionCallback callback, Bundle rootHints) {
   1624             mContext = context;
   1625             if (rootHints == null) {
   1626                 rootHints = new Bundle();
   1627             }
   1628             rootHints.putInt(EXTRA_CLIENT_VERSION, CLIENT_VERSION_CURRENT);
   1629             mRootHints = new Bundle(rootHints);
   1630             callback.setInternalConnectionCallback(this);
   1631             mBrowserObj = MediaBrowserCompatApi21.createBrowser(context, serviceComponent,
   1632                     callback.mConnectionCallbackObj, mRootHints);
   1633         }
   1634 
   1635         @Override
   1636         public void connect() {
   1637             MediaBrowserCompatApi21.connect(mBrowserObj);
   1638         }
   1639 
   1640         @Override
   1641         public void disconnect() {
   1642             if (mServiceBinderWrapper != null && mCallbacksMessenger != null) {
   1643                 try {
   1644                     mServiceBinderWrapper.unregisterCallbackMessenger(mCallbacksMessenger);
   1645                 } catch (RemoteException e) {
   1646                     Log.i(TAG, "Remote error unregistering client messenger." );
   1647                 }
   1648             }
   1649             MediaBrowserCompatApi21.disconnect(mBrowserObj);
   1650         }
   1651 
   1652         @Override
   1653         public boolean isConnected() {
   1654             return MediaBrowserCompatApi21.isConnected(mBrowserObj);
   1655         }
   1656 
   1657         @Override
   1658         public ComponentName getServiceComponent() {
   1659             return MediaBrowserCompatApi21.getServiceComponent(mBrowserObj);
   1660         }
   1661 
   1662         @NonNull
   1663         @Override
   1664         public String getRoot() {
   1665             return MediaBrowserCompatApi21.getRoot(mBrowserObj);
   1666         }
   1667 
   1668         @Nullable
   1669         @Override
   1670         public Bundle getExtras() {
   1671             return MediaBrowserCompatApi21.getExtras(mBrowserObj);
   1672         }
   1673 
   1674         @NonNull
   1675         @Override
   1676         public MediaSessionCompat.Token getSessionToken() {
   1677             if (mMediaSessionToken == null) {
   1678                 mMediaSessionToken = MediaSessionCompat.Token.fromToken(
   1679                         MediaBrowserCompatApi21.getSessionToken(mBrowserObj));
   1680             }
   1681             return mMediaSessionToken;
   1682         }
   1683 
   1684         @Override
   1685         public void subscribe(@NonNull final String parentId, final Bundle options,
   1686                 @NonNull final SubscriptionCallback callback) {
   1687             // Update or create the subscription.
   1688             Subscription sub = mSubscriptions.get(parentId);
   1689             if (sub == null) {
   1690                 sub = new Subscription();
   1691                 mSubscriptions.put(parentId, sub);
   1692             }
   1693             callback.setSubscription(sub);
   1694             Bundle copiedOptions = options == null ? null : new Bundle(options);
   1695             sub.putCallback(mContext, copiedOptions, callback);
   1696 
   1697             if (mServiceBinderWrapper == null) {
   1698                 // TODO: When MediaBrowser is connected to framework's MediaBrowserService,
   1699                 // subscribe with options won't work properly.
   1700                 MediaBrowserCompatApi21.subscribe(
   1701                         mBrowserObj, parentId, callback.mSubscriptionCallbackObj);
   1702             } else {
   1703                 try {
   1704                     mServiceBinderWrapper.addSubscription(
   1705                             parentId, callback.mToken, copiedOptions, mCallbacksMessenger);
   1706                 } catch (RemoteException e) {
   1707                     // Process is crashing. We will disconnect, and upon reconnect we will
   1708                     // automatically reregister. So nothing to do here.
   1709                     Log.i(TAG, "Remote error subscribing media item: " + parentId);
   1710                 }
   1711             }
   1712         }
   1713 
   1714         @Override
   1715         public void unsubscribe(@NonNull String parentId, SubscriptionCallback callback) {
   1716             Subscription sub = mSubscriptions.get(parentId);
   1717             if (sub == null) {
   1718                 return;
   1719             }
   1720 
   1721             if (mServiceBinderWrapper == null) {
   1722                 if (callback == null) {
   1723                     MediaBrowserCompatApi21.unsubscribe(mBrowserObj, parentId);
   1724                 } else {
   1725                     final List<SubscriptionCallback> callbacks = sub.getCallbacks();
   1726                     final List<Bundle> optionsList = sub.getOptionsList();
   1727                     for (int i = callbacks.size() - 1; i >= 0; --i) {
   1728                         if (callbacks.get(i) == callback) {
   1729                             callbacks.remove(i);
   1730                             optionsList.remove(i);
   1731                         }
   1732                     }
   1733                     if (callbacks.size() == 0) {
   1734                         MediaBrowserCompatApi21.unsubscribe(mBrowserObj, parentId);
   1735                     }
   1736                 }
   1737             } else {
   1738                 // Tell the service if necessary.
   1739                 try {
   1740                     if (callback == null) {
   1741                         mServiceBinderWrapper.removeSubscription(parentId, null,
   1742                                 mCallbacksMessenger);
   1743                     } else {
   1744                         final List<SubscriptionCallback> callbacks = sub.getCallbacks();
   1745                         final List<Bundle> optionsList = sub.getOptionsList();
   1746                         for (int i = callbacks.size() - 1; i >= 0; --i) {
   1747                             if (callbacks.get(i) == callback) {
   1748                                 mServiceBinderWrapper.removeSubscription(
   1749                                         parentId, callback.mToken, mCallbacksMessenger);
   1750                                 callbacks.remove(i);
   1751                                 optionsList.remove(i);
   1752                             }
   1753                         }
   1754                     }
   1755                 } catch (RemoteException ex) {
   1756                     // Process is crashing. We will disconnect, and upon reconnect we will
   1757                     // automatically reregister. So nothing to do here.
   1758                     Log.d(TAG, "removeSubscription failed with RemoteException parentId="
   1759                             + parentId);
   1760                 }
   1761             }
   1762 
   1763             if (sub.isEmpty() || callback == null) {
   1764                 mSubscriptions.remove(parentId);
   1765             }
   1766         }
   1767 
   1768         @Override
   1769         public void getItem(@NonNull final String mediaId, @NonNull final ItemCallback cb) {
   1770             if (TextUtils.isEmpty(mediaId)) {
   1771                 throw new IllegalArgumentException("mediaId is empty");
   1772             }
   1773             if (cb == null) {
   1774                 throw new IllegalArgumentException("cb is null");
   1775             }
   1776             if (!MediaBrowserCompatApi21.isConnected(mBrowserObj)) {
   1777                 Log.i(TAG, "Not connected, unable to retrieve the MediaItem.");
   1778                 mHandler.post(new Runnable() {
   1779                     @Override
   1780                     public void run() {
   1781                         cb.onError(mediaId);
   1782                     }
   1783                 });
   1784                 return;
   1785             }
   1786             if (mServiceBinderWrapper == null) {
   1787                 mHandler.post(new Runnable() {
   1788                     @Override
   1789                     public void run() {
   1790                         // Default framework implementation.
   1791                         cb.onError(mediaId);
   1792                     }
   1793                 });
   1794                 return;
   1795             }
   1796             ResultReceiver receiver = new ItemReceiver(mediaId, cb, mHandler);
   1797             try {
   1798                 mServiceBinderWrapper.getMediaItem(mediaId, receiver, mCallbacksMessenger);
   1799             } catch (RemoteException e) {
   1800                 Log.i(TAG, "Remote error getting media item: " + mediaId);
   1801                 mHandler.post(new Runnable() {
   1802                     @Override
   1803                     public void run() {
   1804                         cb.onError(mediaId);
   1805                     }
   1806                 });
   1807             }
   1808         }
   1809 
   1810         @Override
   1811         public void search(@NonNull final String query, final Bundle extras,
   1812                 @NonNull final SearchCallback callback) {
   1813             if (!isConnected()) {
   1814                 throw new IllegalStateException("search() called while not connected");
   1815             }
   1816             if (mServiceBinderWrapper == null) {
   1817                 Log.i(TAG, "The connected service doesn't support search.");
   1818                 mHandler.post(new Runnable() {
   1819                     @Override
   1820                     public void run() {
   1821                         // Default framework implementation.
   1822                         callback.onError(query, extras);
   1823                     }
   1824                 });
   1825                 return;
   1826             }
   1827 
   1828             ResultReceiver receiver = new SearchResultReceiver(query, extras, callback, mHandler);
   1829             try {
   1830                 mServiceBinderWrapper.search(query, extras, receiver, mCallbacksMessenger);
   1831             } catch (RemoteException e) {
   1832                 Log.i(TAG, "Remote error searching items with query: " + query, e);
   1833                 mHandler.post(new Runnable() {
   1834                     @Override
   1835                     public void run() {
   1836                         callback.onError(query, extras);
   1837                     }
   1838                 });
   1839             }
   1840         }
   1841 
   1842         @Override
   1843         public void sendCustomAction(@NonNull final String action, final Bundle extras,
   1844                 @Nullable final CustomActionCallback callback) {
   1845             if (!isConnected()) {
   1846                 throw new IllegalStateException("Cannot send a custom action (" + action + ") with "
   1847                         + "extras " + extras + " because the browser is not connected to the "
   1848                         + "service.");
   1849             }
   1850             if (mServiceBinderWrapper == null) {
   1851                 Log.i(TAG, "The connected service doesn't support sendCustomAction.");
   1852                 if (callback != null) {
   1853                     mHandler.post(new Runnable() {
   1854                         @Override
   1855                         public void run() {
   1856                             callback.onError(action, extras, null);
   1857                         }
   1858                     });
   1859                 }
   1860             }
   1861 
   1862             ResultReceiver receiver = new CustomActionResultReceiver(action, extras, callback,
   1863                     mHandler);
   1864             try {
   1865                 mServiceBinderWrapper.sendCustomAction(action, extras, receiver,
   1866                         mCallbacksMessenger);
   1867             } catch (RemoteException e) {
   1868                 Log.i(TAG, "Remote error sending a custom action: action=" + action + ", extras="
   1869                         + extras, e);
   1870                 if (callback != null) {
   1871                     mHandler.post(new Runnable() {
   1872                         @Override
   1873                         public void run() {
   1874                             callback.onError(action, extras, null);
   1875                         }
   1876                     });
   1877                 }
   1878             }
   1879         }
   1880 
   1881         @Override
   1882         public void onConnected() {
   1883             Bundle extras = MediaBrowserCompatApi21.getExtras(mBrowserObj);
   1884             if (extras == null) {
   1885                 return;
   1886             }
   1887             mServiceVersion = extras.getInt(EXTRA_SERVICE_VERSION, 0);
   1888             IBinder serviceBinder = BundleCompat.getBinder(extras, EXTRA_MESSENGER_BINDER);
   1889             if (serviceBinder != null) {
   1890                 mServiceBinderWrapper = new ServiceBinderWrapper(serviceBinder, mRootHints);
   1891                 mCallbacksMessenger = new Messenger(mHandler);
   1892                 mHandler.setCallbacksMessenger(mCallbacksMessenger);
   1893                 try {
   1894                     mServiceBinderWrapper.registerCallbackMessenger(mContext, mCallbacksMessenger);
   1895                 } catch (RemoteException e) {
   1896                     Log.i(TAG, "Remote error registering client messenger." );
   1897                 }
   1898             }
   1899             IMediaSession sessionToken = IMediaSession.Stub.asInterface(
   1900                     BundleCompat.getBinder(extras, EXTRA_SESSION_BINDER));
   1901             if (sessionToken != null) {
   1902                 mMediaSessionToken = MediaSessionCompat.Token.fromToken(
   1903                         MediaBrowserCompatApi21.getSessionToken(mBrowserObj), sessionToken);
   1904             }
   1905         }
   1906 
   1907         @Override
   1908         public void onConnectionSuspended() {
   1909             mServiceBinderWrapper = null;
   1910             mCallbacksMessenger = null;
   1911             mMediaSessionToken = null;
   1912             mHandler.setCallbacksMessenger(null);
   1913         }
   1914 
   1915         @Override
   1916         public void onConnectionFailed() {
   1917             // Do noting
   1918         }
   1919 
   1920         @Override
   1921         public void onServiceConnected(final Messenger callback, final String root,
   1922                 final MediaSessionCompat.Token session, final Bundle extra) {
   1923             // This method will not be called.
   1924         }
   1925 
   1926         @Override
   1927         public void onConnectionFailed(Messenger callback) {
   1928             // This method will not be called.
   1929         }
   1930 
   1931         @Override
   1932         @SuppressWarnings("ReferenceEquality")
   1933         public void onLoadChildren(Messenger callback, String parentId, List list, Bundle options,
   1934                 Bundle notifyChildrenChangedOptions) {
   1935             if (mCallbacksMessenger != callback) {
   1936                 return;
   1937             }
   1938 
   1939             // Check that the subscription is still subscribed.
   1940             Subscription subscription = mSubscriptions.get(parentId);
   1941             if (subscription == null) {
   1942                 if (DEBUG) {
   1943                     Log.d(TAG, "onLoadChildren for id that isn't subscribed id=" + parentId);
   1944                 }
   1945                 return;
   1946             }
   1947 
   1948             // Tell the app.
   1949             SubscriptionCallback subscriptionCallback = subscription.getCallback(mContext, options);
   1950             if (subscriptionCallback != null) {
   1951                 if (options == null) {
   1952                     if (list == null) {
   1953                         subscriptionCallback.onError(parentId);
   1954                     } else {
   1955                         mNotifyChildrenChangedOptions = notifyChildrenChangedOptions;
   1956                         subscriptionCallback.onChildrenLoaded(parentId, list);
   1957                         mNotifyChildrenChangedOptions = null;
   1958                     }
   1959                 } else {
   1960                     if (list == null) {
   1961                         subscriptionCallback.onError(parentId, options);
   1962                     } else {
   1963                         mNotifyChildrenChangedOptions = notifyChildrenChangedOptions;
   1964                         subscriptionCallback.onChildrenLoaded(parentId, list, options);
   1965                         mNotifyChildrenChangedOptions = null;
   1966                     }
   1967                 }
   1968             }
   1969         }
   1970 
   1971         @Override
   1972         public Bundle getNotifyChildrenChangedOptions() {
   1973             return mNotifyChildrenChangedOptions;
   1974         }
   1975     }
   1976 
   1977     @RequiresApi(23)
   1978     static class MediaBrowserImplApi23 extends MediaBrowserImplApi21 {
   1979         MediaBrowserImplApi23(Context context, ComponentName serviceComponent,
   1980                 ConnectionCallback callback, Bundle rootHints) {
   1981             super(context, serviceComponent, callback, rootHints);
   1982         }
   1983 
   1984         @Override
   1985         public void getItem(@NonNull final String mediaId, @NonNull final ItemCallback cb) {
   1986             if (mServiceBinderWrapper == null) {
   1987                 MediaBrowserCompatApi23.getItem(mBrowserObj, mediaId, cb.mItemCallbackObj);
   1988             } else {
   1989                 super.getItem(mediaId, cb);
   1990             }
   1991         }
   1992     }
   1993 
   1994     @RequiresApi(26)
   1995     static class MediaBrowserImplApi26 extends MediaBrowserImplApi23 {
   1996         MediaBrowserImplApi26(Context context, ComponentName serviceComponent,
   1997                 ConnectionCallback callback, Bundle rootHints) {
   1998             super(context, serviceComponent, callback, rootHints);
   1999         }
   2000 
   2001         @Override
   2002         public void subscribe(@NonNull String parentId, @Nullable Bundle options,
   2003                 @NonNull SubscriptionCallback callback) {
   2004             // From service v2, we use compat code when subscribing.
   2005             // This is to prevent ClassNotFoundException when options has Parcelable in it.
   2006             if (mServiceBinderWrapper == null || mServiceVersion < SERVICE_VERSION_2) {
   2007                 if (options == null) {
   2008                     MediaBrowserCompatApi21.subscribe(
   2009                             mBrowserObj, parentId, callback.mSubscriptionCallbackObj);
   2010                 } else {
   2011                     MediaBrowserCompatApi26.subscribe(
   2012                             mBrowserObj, parentId, options, callback.mSubscriptionCallbackObj);
   2013                 }
   2014             } else {
   2015                 super.subscribe(parentId, options, callback);
   2016             }
   2017         }
   2018 
   2019         @Override
   2020         public void unsubscribe(@NonNull String parentId, SubscriptionCallback callback) {
   2021             // From service v2, we use compat code when subscribing.
   2022             // This is to prevent ClassNotFoundException when options has Parcelable in it.
   2023             if (mServiceBinderWrapper == null || mServiceVersion < SERVICE_VERSION_2) {
   2024                 if (callback == null) {
   2025                     MediaBrowserCompatApi21.unsubscribe(mBrowserObj, parentId);
   2026                 } else {
   2027                     MediaBrowserCompatApi26.unsubscribe(mBrowserObj, parentId,
   2028                             callback.mSubscriptionCallbackObj);
   2029                 }
   2030             } else {
   2031                 super.unsubscribe(parentId, callback);
   2032             }
   2033         }
   2034     }
   2035 
   2036     private static class Subscription {
   2037         private final List<SubscriptionCallback> mCallbacks;
   2038         private final List<Bundle> mOptionsList;
   2039 
   2040         public Subscription() {
   2041             mCallbacks = new ArrayList<>();
   2042             mOptionsList = new ArrayList<>();
   2043         }
   2044 
   2045         public boolean isEmpty() {
   2046             return mCallbacks.isEmpty();
   2047         }
   2048 
   2049         public List<Bundle> getOptionsList() {
   2050             return mOptionsList;
   2051         }
   2052 
   2053         public List<SubscriptionCallback> getCallbacks() {
   2054             return mCallbacks;
   2055         }
   2056 
   2057         public SubscriptionCallback getCallback(Context context, Bundle options) {
   2058             if (options != null) {
   2059                 options.setClassLoader(context.getClassLoader());
   2060             }
   2061             for (int i = 0; i < mOptionsList.size(); ++i) {
   2062                 if (MediaBrowserCompatUtils.areSameOptions(mOptionsList.get(i), options)) {
   2063                     return mCallbacks.get(i);
   2064                 }
   2065             }
   2066             return null;
   2067         }
   2068 
   2069         public void putCallback(Context context, Bundle options, SubscriptionCallback callback) {
   2070             if (options != null) {
   2071                 options.setClassLoader(context.getClassLoader());
   2072             }
   2073             for (int i = 0; i < mOptionsList.size(); ++i) {
   2074                 if (MediaBrowserCompatUtils.areSameOptions(mOptionsList.get(i), options)) {
   2075                     mCallbacks.set(i, callback);
   2076                     return;
   2077                 }
   2078             }
   2079             mCallbacks.add(callback);
   2080             mOptionsList.add(options);
   2081         }
   2082     }
   2083 
   2084     private static class CallbackHandler extends Handler {
   2085         private final WeakReference<MediaBrowserServiceCallbackImpl> mCallbackImplRef;
   2086         private WeakReference<Messenger> mCallbacksMessengerRef;
   2087 
   2088         CallbackHandler(MediaBrowserServiceCallbackImpl callbackImpl) {
   2089             super();
   2090             mCallbackImplRef = new WeakReference<>(callbackImpl);
   2091         }
   2092 
   2093         @Override
   2094         public void handleMessage(Message msg) {
   2095             if (mCallbacksMessengerRef == null || mCallbacksMessengerRef.get() == null ||
   2096                     mCallbackImplRef.get() == null) {
   2097                 return;
   2098             }
   2099             Bundle data = msg.getData();
   2100             data.setClassLoader(MediaSessionCompat.class.getClassLoader());
   2101             MediaBrowserServiceCallbackImpl serviceCallback = mCallbackImplRef.get();
   2102             Messenger callbacksMessenger = mCallbacksMessengerRef.get();
   2103             try {
   2104                 switch (msg.what) {
   2105                     case SERVICE_MSG_ON_CONNECT:
   2106                         serviceCallback.onServiceConnected(callbacksMessenger,
   2107                                 data.getString(DATA_MEDIA_ITEM_ID),
   2108                                 (MediaSessionCompat.Token) data.getParcelable(
   2109                                         DATA_MEDIA_SESSION_TOKEN),
   2110                                 data.getBundle(DATA_ROOT_HINTS));
   2111                         break;
   2112                     case SERVICE_MSG_ON_CONNECT_FAILED:
   2113                         serviceCallback.onConnectionFailed(callbacksMessenger);
   2114                         break;
   2115                     case SERVICE_MSG_ON_LOAD_CHILDREN:
   2116                         serviceCallback.onLoadChildren(callbacksMessenger,
   2117                                 data.getString(DATA_MEDIA_ITEM_ID),
   2118                                 data.getParcelableArrayList(DATA_MEDIA_ITEM_LIST),
   2119                                 data.getBundle(DATA_OPTIONS),
   2120                                 data.getBundle(DATA_NOTIFY_CHILDREN_CHANGED_OPTIONS));
   2121                         break;
   2122                     default:
   2123                         Log.w(TAG, "Unhandled message: " + msg
   2124                                 + "\n  Client version: " + CLIENT_VERSION_CURRENT
   2125                                 + "\n  Service version: " + msg.arg1);
   2126                 }
   2127             } catch (BadParcelableException e) {
   2128                 // Do not print the exception here, since it is already done by the Parcel class.
   2129                 Log.e(TAG, "Could not unparcel the data.");
   2130                 // If an error happened while connecting, disconnect from the service.
   2131                 if (msg.what == SERVICE_MSG_ON_CONNECT) {
   2132                     serviceCallback.onConnectionFailed(callbacksMessenger);
   2133                 }
   2134             }
   2135         }
   2136 
   2137         void setCallbacksMessenger(Messenger callbacksMessenger) {
   2138             mCallbacksMessengerRef = new WeakReference<>(callbacksMessenger);
   2139         }
   2140     }
   2141 
   2142     private static class ServiceBinderWrapper {
   2143         private Messenger mMessenger;
   2144         private Bundle mRootHints;
   2145 
   2146         public ServiceBinderWrapper(IBinder target, Bundle rootHints) {
   2147             mMessenger = new Messenger(target);
   2148             mRootHints = rootHints;
   2149         }
   2150 
   2151         void connect(Context context, Messenger callbacksMessenger)
   2152                 throws RemoteException {
   2153             Bundle data = new Bundle();
   2154             data.putString(DATA_PACKAGE_NAME, context.getPackageName());
   2155             data.putBundle(DATA_ROOT_HINTS, mRootHints);
   2156             sendRequest(CLIENT_MSG_CONNECT, data, callbacksMessenger);
   2157         }
   2158 
   2159         void disconnect(Messenger callbacksMessenger) throws RemoteException {
   2160             sendRequest(CLIENT_MSG_DISCONNECT, null, callbacksMessenger);
   2161         }
   2162 
   2163         void addSubscription(String parentId, IBinder callbackToken, Bundle options,
   2164                 Messenger callbacksMessenger)
   2165                 throws RemoteException {
   2166             Bundle data = new Bundle();
   2167             data.putString(DATA_MEDIA_ITEM_ID, parentId);
   2168             BundleCompat.putBinder(data, DATA_CALLBACK_TOKEN, callbackToken);
   2169             data.putBundle(DATA_OPTIONS, options);
   2170             sendRequest(CLIENT_MSG_ADD_SUBSCRIPTION, data, callbacksMessenger);
   2171         }
   2172 
   2173         void removeSubscription(String parentId, IBinder callbackToken,
   2174                 Messenger callbacksMessenger)
   2175                 throws RemoteException {
   2176             Bundle data = new Bundle();
   2177             data.putString(DATA_MEDIA_ITEM_ID, parentId);
   2178             BundleCompat.putBinder(data, DATA_CALLBACK_TOKEN, callbackToken);
   2179             sendRequest(CLIENT_MSG_REMOVE_SUBSCRIPTION, data, callbacksMessenger);
   2180         }
   2181 
   2182         void getMediaItem(String mediaId, ResultReceiver receiver, Messenger callbacksMessenger)
   2183                 throws RemoteException {
   2184             Bundle data = new Bundle();
   2185             data.putString(DATA_MEDIA_ITEM_ID, mediaId);
   2186             data.putParcelable(DATA_RESULT_RECEIVER, receiver);
   2187             sendRequest(CLIENT_MSG_GET_MEDIA_ITEM, data, callbacksMessenger);
   2188         }
   2189 
   2190         void registerCallbackMessenger(Context context, Messenger callbackMessenger)
   2191                 throws RemoteException {
   2192             Bundle data = new Bundle();
   2193             data.putString(DATA_PACKAGE_NAME, context.getPackageName());
   2194             data.putBundle(DATA_ROOT_HINTS, mRootHints);
   2195             sendRequest(CLIENT_MSG_REGISTER_CALLBACK_MESSENGER, data, callbackMessenger);
   2196         }
   2197 
   2198         void unregisterCallbackMessenger(Messenger callbackMessenger) throws RemoteException {
   2199             sendRequest(CLIENT_MSG_UNREGISTER_CALLBACK_MESSENGER, null, callbackMessenger);
   2200         }
   2201 
   2202         void search(String query, Bundle extras, ResultReceiver receiver,
   2203                 Messenger callbacksMessenger) throws RemoteException {
   2204             Bundle data = new Bundle();
   2205             data.putString(DATA_SEARCH_QUERY, query);
   2206             data.putBundle(DATA_SEARCH_EXTRAS, extras);
   2207             data.putParcelable(DATA_RESULT_RECEIVER, receiver);
   2208             sendRequest(CLIENT_MSG_SEARCH, data, callbacksMessenger);
   2209         }
   2210 
   2211         void sendCustomAction(String action, Bundle extras, ResultReceiver receiver,
   2212                 Messenger callbacksMessenger) throws RemoteException {
   2213             Bundle data = new Bundle();
   2214             data.putString(DATA_CUSTOM_ACTION, action);
   2215             data.putBundle(DATA_CUSTOM_ACTION_EXTRAS, extras);
   2216             data.putParcelable(DATA_RESULT_RECEIVER, receiver);
   2217             sendRequest(CLIENT_MSG_SEND_CUSTOM_ACTION, data, callbacksMessenger);
   2218         }
   2219 
   2220         private void sendRequest(int what, Bundle data, Messenger cbMessenger)
   2221                 throws RemoteException {
   2222             Message msg = Message.obtain();
   2223             msg.what = what;
   2224             msg.arg1 = CLIENT_VERSION_CURRENT;
   2225             msg.setData(data);
   2226             msg.replyTo = cbMessenger;
   2227             mMessenger.send(msg);
   2228         }
   2229     }
   2230 
   2231     private  static class ItemReceiver extends ResultReceiver {
   2232         private final String mMediaId;
   2233         private final ItemCallback mCallback;
   2234 
   2235         ItemReceiver(String mediaId, ItemCallback callback, Handler handler) {
   2236             super(handler);
   2237             mMediaId = mediaId;
   2238             mCallback = callback;
   2239         }
   2240 
   2241         @Override
   2242         protected void onReceiveResult(int resultCode, Bundle resultData) {
   2243             if (resultData != null) {
   2244                 resultData.setClassLoader(MediaBrowserCompat.class.getClassLoader());
   2245             }
   2246             if (resultCode != MediaBrowserServiceCompat.RESULT_OK || resultData == null
   2247                     || !resultData.containsKey(MediaBrowserServiceCompat.KEY_MEDIA_ITEM)) {
   2248                 mCallback.onError(mMediaId);
   2249                 return;
   2250             }
   2251             Parcelable item = resultData.getParcelable(MediaBrowserServiceCompat.KEY_MEDIA_ITEM);
   2252             if (item == null || item instanceof MediaItem) {
   2253                 mCallback.onItemLoaded((MediaItem) item);
   2254             } else {
   2255                 mCallback.onError(mMediaId);
   2256             }
   2257         }
   2258     }
   2259 
   2260     private static class SearchResultReceiver extends ResultReceiver {
   2261         private final String mQuery;
   2262         private final Bundle mExtras;
   2263         private final SearchCallback mCallback;
   2264 
   2265         SearchResultReceiver(String query, Bundle extras, SearchCallback callback,
   2266                 Handler handler) {
   2267             super(handler);
   2268             mQuery = query;
   2269             mExtras = extras;
   2270             mCallback = callback;
   2271         }
   2272 
   2273         @Override
   2274         protected void onReceiveResult(int resultCode, Bundle resultData) {
   2275             if (resultData != null) {
   2276                 resultData.setClassLoader(MediaBrowserCompat.class.getClassLoader());
   2277             }
   2278             if (resultCode != MediaBrowserServiceCompat.RESULT_OK || resultData == null
   2279                     || !resultData.containsKey(MediaBrowserServiceCompat.KEY_SEARCH_RESULTS)) {
   2280                 mCallback.onError(mQuery, mExtras);
   2281                 return;
   2282             }
   2283             Parcelable[] items = resultData.getParcelableArray(
   2284                     MediaBrowserServiceCompat.KEY_SEARCH_RESULTS);
   2285             List<MediaItem> results = null;
   2286             if (items != null) {
   2287                 results = new ArrayList<>();
   2288                 for (Parcelable item : items) {
   2289                     results.add((MediaItem) item);
   2290                 }
   2291             }
   2292             mCallback.onSearchResult(mQuery, mExtras, results);
   2293         }
   2294     }
   2295 
   2296     private static class CustomActionResultReceiver extends ResultReceiver {
   2297         private final String mAction;
   2298         private final Bundle mExtras;
   2299         private final CustomActionCallback mCallback;
   2300 
   2301         CustomActionResultReceiver(String action, Bundle extras, CustomActionCallback callback,
   2302                 Handler handler) {
   2303             super(handler);
   2304             mAction = action;
   2305             mExtras = extras;
   2306             mCallback = callback;
   2307         }
   2308 
   2309         @Override
   2310         protected void onReceiveResult(int resultCode, Bundle resultData) {
   2311             if (mCallback == null) {
   2312                 return;
   2313             }
   2314             switch (resultCode) {
   2315                 case MediaBrowserServiceCompat.RESULT_PROGRESS_UPDATE:
   2316                     mCallback.onProgressUpdate(mAction, mExtras, resultData);
   2317                     break;
   2318                 case MediaBrowserServiceCompat.RESULT_OK:
   2319                     mCallback.onResult(mAction, mExtras, resultData);
   2320                     break;
   2321                 case MediaBrowserServiceCompat.RESULT_ERROR:
   2322                     mCallback.onError(mAction, mExtras, resultData);
   2323                     break;
   2324                 default:
   2325                     Log.w(TAG, "Unknown result code: " + resultCode + " (extras=" + mExtras
   2326                             + ", resultData=" + resultData + ")");
   2327                     break;
   2328             }
   2329         }
   2330     }
   2331 }
   2332