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