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