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