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 */
16package android.support.v4.media;
17
18import static androidx.annotation.RestrictTo.Scope.LIBRARY;
19import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
20import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_ADD_SUBSCRIPTION;
21import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_CONNECT;
22import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_DISCONNECT;
23import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_GET_MEDIA_ITEM;
24import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_REGISTER_CALLBACK_MESSENGER;
25import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_REMOVE_SUBSCRIPTION;
26import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_SEARCH;
27import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_SEND_CUSTOM_ACTION;
28import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_UNREGISTER_CALLBACK_MESSENGER;
29import static androidx.media.MediaBrowserProtocol.CLIENT_VERSION_CURRENT;
30import static androidx.media.MediaBrowserProtocol.DATA_CALLBACK_TOKEN;
31import static androidx.media.MediaBrowserProtocol.DATA_CUSTOM_ACTION;
32import static androidx.media.MediaBrowserProtocol.DATA_CUSTOM_ACTION_EXTRAS;
33import static androidx.media.MediaBrowserProtocol.DATA_MEDIA_ITEM_ID;
34import static androidx.media.MediaBrowserProtocol.DATA_MEDIA_ITEM_LIST;
35import static androidx.media.MediaBrowserProtocol.DATA_MEDIA_SESSION_TOKEN;
36import static androidx.media.MediaBrowserProtocol.DATA_NOTIFY_CHILDREN_CHANGED_OPTIONS;
37import static androidx.media.MediaBrowserProtocol.DATA_OPTIONS;
38import static androidx.media.MediaBrowserProtocol.DATA_PACKAGE_NAME;
39import static androidx.media.MediaBrowserProtocol.DATA_RESULT_RECEIVER;
40import static androidx.media.MediaBrowserProtocol.DATA_ROOT_HINTS;
41import static androidx.media.MediaBrowserProtocol.DATA_SEARCH_EXTRAS;
42import static androidx.media.MediaBrowserProtocol.DATA_SEARCH_QUERY;
43import static androidx.media.MediaBrowserProtocol.EXTRA_CLIENT_VERSION;
44import static androidx.media.MediaBrowserProtocol.EXTRA_MESSENGER_BINDER;
45import static androidx.media.MediaBrowserProtocol.EXTRA_SERVICE_VERSION;
46import static androidx.media.MediaBrowserProtocol.EXTRA_SESSION_BINDER;
47import static androidx.media.MediaBrowserProtocol.SERVICE_MSG_ON_CONNECT;
48import static androidx.media.MediaBrowserProtocol.SERVICE_MSG_ON_CONNECT_FAILED;
49import static androidx.media.MediaBrowserProtocol.SERVICE_MSG_ON_LOAD_CHILDREN;
50import static androidx.media.MediaBrowserProtocol.SERVICE_VERSION_2;
51
52import android.content.ComponentName;
53import android.content.Context;
54import android.content.Intent;
55import android.content.ServiceConnection;
56import android.os.BadParcelableException;
57import android.os.Binder;
58import android.os.Build;
59import android.os.Bundle;
60import android.os.Handler;
61import android.os.IBinder;
62import android.os.Message;
63import android.os.Messenger;
64import android.os.Parcel;
65import android.os.Parcelable;
66import android.os.RemoteException;
67import android.support.v4.media.session.IMediaSession;
68import android.support.v4.media.session.MediaControllerCompat.TransportControls;
69import android.support.v4.media.session.MediaSessionCompat;
70import android.support.v4.os.ResultReceiver;
71import android.text.TextUtils;
72import android.util.Log;
73
74import androidx.annotation.IntDef;
75import androidx.annotation.NonNull;
76import androidx.annotation.Nullable;
77import androidx.annotation.RequiresApi;
78import androidx.annotation.RestrictTo;
79import androidx.collection.ArrayMap;
80import androidx.core.app.BundleCompat;
81import androidx.media.MediaBrowserCompatUtils;
82import androidx.media.MediaBrowserServiceCompat;
83
84import java.lang.annotation.Retention;
85import java.lang.annotation.RetentionPolicy;
86import java.lang.ref.WeakReference;
87import java.util.ArrayList;
88import java.util.Collections;
89import java.util.List;
90import 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 */
107public 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