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