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