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