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