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