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