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