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