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