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