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