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