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