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