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