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