• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2014 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 
17 package android.media.browse;
18 
19 import android.annotation.IntDef;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.content.ComponentName;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.content.ServiceConnection;
26 import android.content.pm.ParceledListSlice;
27 import android.media.MediaDescription;
28 import android.media.session.MediaSession;
29 import android.os.Binder;
30 import android.os.Bundle;
31 import android.os.Handler;
32 import android.os.IBinder;
33 import android.os.Parcel;
34 import android.os.Parcelable;
35 import android.os.RemoteException;
36 import android.os.ResultReceiver;
37 import android.service.media.IMediaBrowserService;
38 import android.service.media.IMediaBrowserServiceCallbacks;
39 import android.service.media.MediaBrowserService;
40 import android.text.TextUtils;
41 import android.util.ArrayMap;
42 import android.util.Log;
43 
44 import java.lang.annotation.Retention;
45 import java.lang.annotation.RetentionPolicy;
46 import java.lang.ref.WeakReference;
47 import java.util.ArrayList;
48 import java.util.List;
49 import java.util.Map.Entry;
50 
51 /**
52  * Browses media content offered by a link MediaBrowserService.
53  * <p>
54  * This object is not thread-safe. All calls should happen on the thread on which the browser
55  * was constructed.
56  * </p>
57  * <h3>Standard Extra Data</h3>
58  *
59  * <p>These are the current standard fields that can be used as extra data via
60  * {@link #subscribe(String, Bundle, SubscriptionCallback)},
61  * {@link #unsubscribe(String, SubscriptionCallback)}, and
62  * {@link SubscriptionCallback#onChildrenLoaded(String, List, Bundle)}.
63  *
64  * <ul>
65  *     <li> {@link #EXTRA_PAGE}
66  *     <li> {@link #EXTRA_PAGE_SIZE}
67  * </ul>
68  */
69 public final class MediaBrowser {
70     private static final String TAG = "MediaBrowser";
71     private static final boolean DBG = false;
72 
73     /**
74      * Used as an int extra field to denote the page number to subscribe.
75      * The value of {@code EXTRA_PAGE} should be greater than or equal to 0.
76      *
77      * @see #EXTRA_PAGE_SIZE
78      */
79     public static final String EXTRA_PAGE = "android.media.browse.extra.PAGE";
80 
81     /**
82      * Used as an int extra field to denote the number of media items in a page.
83      * The value of {@code EXTRA_PAGE_SIZE} should be greater than or equal to 1.
84      *
85      * @see #EXTRA_PAGE
86      */
87     public static final String EXTRA_PAGE_SIZE = "android.media.browse.extra.PAGE_SIZE";
88 
89     private static final int CONNECT_STATE_DISCONNECTING = 0;
90     private static final int CONNECT_STATE_DISCONNECTED = 1;
91     private static final int CONNECT_STATE_CONNECTING = 2;
92     private static final int CONNECT_STATE_CONNECTED = 3;
93     private static final int CONNECT_STATE_SUSPENDED = 4;
94 
95     private final Context mContext;
96     private final ComponentName mServiceComponent;
97     private final ConnectionCallback mCallback;
98     private final Bundle mRootHints;
99     private final Handler mHandler = new Handler();
100     private final ArrayMap<String, Subscription> mSubscriptions = new ArrayMap<>();
101 
102     private volatile int mState = CONNECT_STATE_DISCONNECTED;
103     private volatile String mRootId;
104     private volatile MediaSession.Token mMediaSessionToken;
105     private volatile Bundle mExtras;
106 
107     private MediaServiceConnection mServiceConnection;
108     private IMediaBrowserService mServiceBinder;
109     private IMediaBrowserServiceCallbacks mServiceCallbacks;
110 
111     /**
112      * Creates a media browser for the specified media browser service.
113      *
114      * @param context The context.
115      * @param serviceComponent The component name of the media browser service.
116      * @param callback The connection callback.
117      * @param rootHints An optional bundle of service-specific arguments to send
118      * to the media browser service when connecting and retrieving the root id
119      * for browsing, or null if none. The contents of this bundle may affect
120      * the information returned when browsing.
121      * @see android.service.media.MediaBrowserService.BrowserRoot#EXTRA_RECENT
122      * @see android.service.media.MediaBrowserService.BrowserRoot#EXTRA_OFFLINE
123      * @see android.service.media.MediaBrowserService.BrowserRoot#EXTRA_SUGGESTED
124      */
MediaBrowser(Context context, ComponentName serviceComponent, ConnectionCallback callback, Bundle rootHints)125     public MediaBrowser(Context context, ComponentName serviceComponent,
126             ConnectionCallback callback, Bundle rootHints) {
127         if (context == null) {
128             throw new IllegalArgumentException("context must not be null");
129         }
130         if (serviceComponent == null) {
131             throw new IllegalArgumentException("service component must not be null");
132         }
133         if (callback == null) {
134             throw new IllegalArgumentException("connection callback must not be null");
135         }
136         mContext = context;
137         mServiceComponent = serviceComponent;
138         mCallback = callback;
139         mRootHints = rootHints == null ? null : new Bundle(rootHints);
140     }
141 
142     /**
143      * Connects to the media browser service.
144      * <p>
145      * The connection callback specified in the constructor will be invoked
146      * when the connection completes or fails.
147      * </p>
148      */
connect()149     public void connect() {
150         if (mState != CONNECT_STATE_DISCONNECTING && mState != CONNECT_STATE_DISCONNECTED) {
151             throw new IllegalStateException("connect() called while neither disconnecting nor "
152                     + "disconnected (state=" + getStateLabel(mState) + ")");
153         }
154 
155         mState = CONNECT_STATE_CONNECTING;
156         mHandler.post(new Runnable() {
157             @Override
158             public void run() {
159                 if (mState == CONNECT_STATE_DISCONNECTING) {
160                     return;
161                 }
162                 mState = CONNECT_STATE_CONNECTING;
163                 // TODO: remove this extra check.
164                 if (DBG) {
165                     if (mServiceConnection != null) {
166                         throw new RuntimeException("mServiceConnection should be null. Instead it"
167                                 + " is " + mServiceConnection);
168                     }
169                 }
170                 if (mServiceBinder != null) {
171                     throw new RuntimeException("mServiceBinder should be null. Instead it is "
172                             + mServiceBinder);
173                 }
174                 if (mServiceCallbacks != null) {
175                     throw new RuntimeException("mServiceCallbacks should be null. Instead it is "
176                             + mServiceCallbacks);
177                 }
178 
179                 final Intent intent = new Intent(MediaBrowserService.SERVICE_INTERFACE);
180                 intent.setComponent(mServiceComponent);
181 
182                 mServiceConnection = new MediaServiceConnection();
183 
184                 boolean bound = false;
185                 try {
186                     bound = mContext.bindService(intent, mServiceConnection,
187                             Context.BIND_AUTO_CREATE | Context.BIND_INCLUDE_CAPABILITIES);
188                 } catch (Exception ex) {
189                     Log.e(TAG, "Failed binding to service " + mServiceComponent);
190                 }
191 
192                 if (!bound) {
193                     // Tell them that it didn't work.
194                     forceCloseConnection();
195                     mCallback.onConnectionFailed();
196                 }
197 
198                 if (DBG) {
199                     Log.d(TAG, "connect...");
200                     dump();
201                 }
202             }
203         });
204     }
205 
206     /**
207      * Disconnects from the media browser service.
208      * After this, no more callbacks will be received.
209      */
disconnect()210     public void disconnect() {
211         // It's ok to call this any state, because allowing this lets apps not have
212         // to check isConnected() unnecessarily. They won't appreciate the extra
213         // assertions for this. We do everything we can here to go back to a valid state.
214         mState = CONNECT_STATE_DISCONNECTING;
215         mHandler.post(new Runnable() {
216             @Override
217             public void run() {
218                 // connect() could be called before this. Then we will disconnect and reconnect.
219                 if (mServiceCallbacks != null) {
220                     try {
221                         mServiceBinder.disconnect(mServiceCallbacks);
222                     } catch (RemoteException ex) {
223                         // We are disconnecting anyway. Log, just for posterity but it's not
224                         // a big problem.
225                         Log.w(TAG, "RemoteException during connect for " + mServiceComponent);
226                     }
227                 }
228                 int state = mState;
229                 forceCloseConnection();
230                 // If the state was not CONNECT_STATE_DISCONNECTING, keep the state so that
231                 // the operation came after disconnect() can be handled properly.
232                 if (state != CONNECT_STATE_DISCONNECTING) {
233                     mState = state;
234                 }
235                 if (DBG) {
236                     Log.d(TAG, "disconnect...");
237                     dump();
238                 }
239             }
240         });
241     }
242 
243     /**
244      * Null out the variables and unbind from the service. This doesn't include
245      * calling disconnect on the service, because we only try to do that in the
246      * clean shutdown cases.
247      * <p>
248      * Everywhere that calls this EXCEPT for disconnect() should follow it with
249      * a call to mCallback.onConnectionFailed(). Disconnect doesn't do that callback
250      * for a clean shutdown, but everywhere else is a dirty shutdown and should
251      * notify the app.
252      * <p>
253      * Also, mState should be updated properly. Mostly it should be CONNECT_STATE_DIACONNECTED
254      * except for disconnect().
255      */
forceCloseConnection()256     private void forceCloseConnection() {
257         if (mServiceConnection != null) {
258             try {
259                 mContext.unbindService(mServiceConnection);
260             } catch (IllegalArgumentException e) {
261                 if (DBG) {
262                     Log.d(TAG, "unbindService failed", e);
263                 }
264             }
265         }
266         mState = CONNECT_STATE_DISCONNECTED;
267         mServiceConnection = null;
268         mServiceBinder = null;
269         mServiceCallbacks = null;
270         mRootId = null;
271         mMediaSessionToken = null;
272     }
273 
274     /**
275      * Returns whether the browser is connected to the service.
276      */
isConnected()277     public boolean isConnected() {
278         return mState == CONNECT_STATE_CONNECTED;
279     }
280 
281     /**
282      * Gets the service component that the media browser is connected to.
283      */
getServiceComponent()284     public @NonNull ComponentName getServiceComponent() {
285         if (!isConnected()) {
286             throw new IllegalStateException("getServiceComponent() called while not connected"
287                     + " (state=" + mState + ")");
288         }
289         return mServiceComponent;
290     }
291 
292     /**
293      * Gets the root id.
294      * <p>
295      * Note that the root id may become invalid or change when the
296      * browser is disconnected.
297      * </p>
298      *
299      * @throws IllegalStateException if not connected.
300      */
getRoot()301     public @NonNull String getRoot() {
302         if (!isConnected()) {
303             throw new IllegalStateException("getRoot() called while not connected (state="
304                     + getStateLabel(mState) + ")");
305         }
306         return mRootId;
307     }
308 
309     /**
310      * Gets any extras for the media service.
311      *
312      * @throws IllegalStateException if not connected.
313      */
getExtras()314     public @Nullable Bundle getExtras() {
315         if (!isConnected()) {
316             throw new IllegalStateException("getExtras() called while not connected (state="
317                     + getStateLabel(mState) + ")");
318         }
319         return mExtras;
320     }
321 
322     /**
323      * Gets the media session token associated with the media browser.
324      * <p>
325      * Note that the session token may become invalid or change when the
326      * browser is disconnected.
327      * </p>
328      *
329      * @return The session token for the browser, never null.
330      *
331      * @throws IllegalStateException if not connected.
332      */
getSessionToken()333     public @NonNull MediaSession.Token getSessionToken() {
334         if (!isConnected()) {
335             throw new IllegalStateException("getSessionToken() called while not connected (state="
336                     + mState + ")");
337         }
338         return mMediaSessionToken;
339     }
340 
341     /**
342      * Queries for information about the media items that are contained within
343      * the specified id and subscribes to receive updates when they change.
344      * <p>
345      * The list of subscriptions is maintained even when not connected and is
346      * restored after the reconnection. It is ok to subscribe while not connected
347      * but the results will not be returned until the connection completes.
348      * </p>
349      * <p>
350      * If the id is already subscribed with a different callback then the new
351      * callback will replace the previous one and the child data will be
352      * reloaded.
353      * </p>
354      *
355      * @param parentId The id of the parent media item whose list of children
356      *            will be subscribed.
357      * @param callback The callback to receive the list of children.
358      */
subscribe(@onNull String parentId, @NonNull SubscriptionCallback callback)359     public void subscribe(@NonNull String parentId, @NonNull SubscriptionCallback callback) {
360         subscribeInternal(parentId, null, callback);
361     }
362 
363     /**
364      * Queries with service-specific arguments for information about the media items
365      * that are contained within the specified id and subscribes to receive updates
366      * when they change.
367      * <p>
368      * The list of subscriptions is maintained even when not connected and is
369      * restored after the reconnection. It is ok to subscribe while not connected
370      * but the results will not be returned until the connection completes.
371      * </p>
372      * <p>
373      * If the id is already subscribed with a different callback then the new
374      * callback will replace the previous one and the child data will be
375      * reloaded.
376      * </p>
377      *
378      * @param parentId The id of the parent media item whose list of children
379      *            will be subscribed.
380      * @param options The bundle of service-specific arguments to send to the media
381      *            browser service. The contents of this bundle may affect the
382      *            information returned when browsing.
383      * @param callback The callback to receive the list of children.
384      */
subscribe(@onNull String parentId, @NonNull Bundle options, @NonNull SubscriptionCallback callback)385     public void subscribe(@NonNull String parentId, @NonNull Bundle options,
386             @NonNull SubscriptionCallback callback) {
387         if (options == null) {
388             throw new IllegalArgumentException("options cannot be null");
389         }
390         subscribeInternal(parentId, new Bundle(options), callback);
391     }
392 
393     /**
394      * Unsubscribes for changes to the children of the specified media id.
395      * <p>
396      * The query callback will no longer be invoked for results associated with
397      * this id once this method returns.
398      * </p>
399      *
400      * @param parentId The id of the parent media item whose list of children
401      *            will be unsubscribed.
402      */
unsubscribe(@onNull String parentId)403     public void unsubscribe(@NonNull String parentId) {
404         unsubscribeInternal(parentId, null);
405     }
406 
407     /**
408      * Unsubscribes for changes to the children of the specified media id through a callback.
409      * <p>
410      * The query callback will no longer be invoked for results associated with
411      * this id once this method returns.
412      * </p>
413      *
414      * @param parentId The id of the parent media item whose list of children
415      *            will be unsubscribed.
416      * @param callback A callback sent to the media browser service to subscribe.
417      */
unsubscribe(@onNull String parentId, @NonNull SubscriptionCallback callback)418     public void unsubscribe(@NonNull String parentId, @NonNull SubscriptionCallback callback) {
419         if (callback == null) {
420             throw new IllegalArgumentException("callback cannot be null");
421         }
422         unsubscribeInternal(parentId, callback);
423     }
424 
425     /**
426      * Retrieves a specific {@link MediaItem} from the connected service. Not
427      * all services may support this, so falling back to subscribing to the
428      * parent's id should be used when unavailable.
429      *
430      * @param mediaId The id of the item to retrieve.
431      * @param cb The callback to receive the result on.
432      */
getItem(final @NonNull String mediaId, @NonNull final ItemCallback cb)433     public void getItem(final @NonNull String mediaId, @NonNull final ItemCallback cb) {
434         if (TextUtils.isEmpty(mediaId)) {
435             throw new IllegalArgumentException("mediaId cannot be empty.");
436         }
437         if (cb == null) {
438             throw new IllegalArgumentException("cb cannot be null.");
439         }
440         if (mState != CONNECT_STATE_CONNECTED) {
441             Log.i(TAG, "Not connected, unable to retrieve the MediaItem.");
442             mHandler.post(new Runnable() {
443                 @Override
444                 public void run() {
445                     cb.onError(mediaId);
446                 }
447             });
448             return;
449         }
450         ResultReceiver receiver = new ResultReceiver(mHandler) {
451             @Override
452             protected void onReceiveResult(int resultCode, Bundle resultData) {
453                 if (!isConnected()) {
454                     return;
455                 }
456                 if (resultCode != 0 || resultData == null
457                         || !resultData.containsKey(MediaBrowserService.KEY_MEDIA_ITEM)) {
458                     cb.onError(mediaId);
459                     return;
460                 }
461                 Parcelable item = resultData.getParcelable(MediaBrowserService.KEY_MEDIA_ITEM);
462                 if (item != null && !(item instanceof MediaItem)) {
463                     cb.onError(mediaId);
464                     return;
465                 }
466                 cb.onItemLoaded((MediaItem) item);
467             }
468         };
469         try {
470             mServiceBinder.getMediaItem(mediaId, receiver, mServiceCallbacks);
471         } catch (RemoteException e) {
472             Log.i(TAG, "Remote error getting media item.");
473             mHandler.post(new Runnable() {
474                 @Override
475                 public void run() {
476                     cb.onError(mediaId);
477                 }
478             });
479         }
480     }
481 
subscribeInternal(String parentId, Bundle options, SubscriptionCallback callback)482     private void subscribeInternal(String parentId, Bundle options, SubscriptionCallback callback) {
483         // Check arguments.
484         if (TextUtils.isEmpty(parentId)) {
485             throw new IllegalArgumentException("parentId cannot be empty.");
486         }
487         if (callback == null) {
488             throw new IllegalArgumentException("callback cannot be null");
489         }
490         // Update or create the subscription.
491         Subscription sub = mSubscriptions.get(parentId);
492         if (sub == null) {
493             sub = new Subscription();
494             mSubscriptions.put(parentId, sub);
495         }
496         sub.putCallback(mContext, options, callback);
497 
498         // If we are connected, tell the service that we are watching. If we aren't connected,
499         // the service will be told when we connect.
500         if (isConnected()) {
501             try {
502                 if (options == null) {
503                     mServiceBinder.addSubscriptionDeprecated(parentId, mServiceCallbacks);
504                 }
505                 mServiceBinder.addSubscription(parentId, callback.mToken, options,
506                         mServiceCallbacks);
507             } catch (RemoteException ex) {
508                 // Process is crashing. We will disconnect, and upon reconnect we will
509                 // automatically reregister. So nothing to do here.
510                 Log.d(TAG, "addSubscription failed with RemoteException parentId=" + parentId);
511             }
512         }
513     }
514 
unsubscribeInternal(String parentId, SubscriptionCallback callback)515     private void unsubscribeInternal(String parentId, SubscriptionCallback callback) {
516         // Check arguments.
517         if (TextUtils.isEmpty(parentId)) {
518             throw new IllegalArgumentException("parentId cannot be empty.");
519         }
520 
521         Subscription sub = mSubscriptions.get(parentId);
522         if (sub == null) {
523             return;
524         }
525         // Tell the service if necessary.
526         try {
527             if (callback == null) {
528                 if (isConnected()) {
529                     mServiceBinder.removeSubscriptionDeprecated(parentId, mServiceCallbacks);
530                     mServiceBinder.removeSubscription(parentId, null, mServiceCallbacks);
531                 }
532             } else {
533                 final List<SubscriptionCallback> callbacks = sub.getCallbacks();
534                 final List<Bundle> optionsList = sub.getOptionsList();
535                 for (int i = callbacks.size() - 1; i >= 0; --i) {
536                     if (callbacks.get(i) == callback) {
537                         if (isConnected()) {
538                             mServiceBinder.removeSubscription(
539                                     parentId, callback.mToken, mServiceCallbacks);
540                         }
541                         callbacks.remove(i);
542                         optionsList.remove(i);
543                     }
544                 }
545             }
546         } catch (RemoteException ex) {
547             // Process is crashing. We will disconnect, and upon reconnect we will
548             // automatically reregister. So nothing to do here.
549             Log.d(TAG, "removeSubscription failed with RemoteException parentId=" + parentId);
550         }
551 
552         if (sub.isEmpty() || callback == null) {
553             mSubscriptions.remove(parentId);
554         }
555     }
556 
557     /**
558      * For debugging.
559      */
getStateLabel(int state)560     private static String getStateLabel(int state) {
561         switch (state) {
562             case CONNECT_STATE_DISCONNECTING:
563                 return "CONNECT_STATE_DISCONNECTING";
564             case CONNECT_STATE_DISCONNECTED:
565                 return "CONNECT_STATE_DISCONNECTED";
566             case CONNECT_STATE_CONNECTING:
567                 return "CONNECT_STATE_CONNECTING";
568             case CONNECT_STATE_CONNECTED:
569                 return "CONNECT_STATE_CONNECTED";
570             case CONNECT_STATE_SUSPENDED:
571                 return "CONNECT_STATE_SUSPENDED";
572             default:
573                 return "UNKNOWN/" + state;
574         }
575     }
576 
onServiceConnected(final IMediaBrowserServiceCallbacks callback, final String root, final MediaSession.Token session, final Bundle extra)577     private void onServiceConnected(final IMediaBrowserServiceCallbacks callback,
578             final String root, final MediaSession.Token session, final Bundle extra) {
579         mHandler.post(new Runnable() {
580             @Override
581             public void run() {
582                 // Check to make sure there hasn't been a disconnect or a different
583                 // ServiceConnection.
584                 if (!isCurrent(callback, "onConnect")) {
585                     return;
586                 }
587                 // Don't allow them to call us twice.
588                 if (mState != CONNECT_STATE_CONNECTING) {
589                     Log.w(TAG, "onConnect from service while mState="
590                             + getStateLabel(mState) + "... ignoring");
591                     return;
592                 }
593                 mRootId = root;
594                 mMediaSessionToken = session;
595                 mExtras = extra;
596                 mState = CONNECT_STATE_CONNECTED;
597 
598                 if (DBG) {
599                     Log.d(TAG, "ServiceCallbacks.onConnect...");
600                     dump();
601                 }
602                 mCallback.onConnected();
603 
604                 // we may receive some subscriptions before we are connected, so re-subscribe
605                 // everything now
606                 for (Entry<String, Subscription> subscriptionEntry : mSubscriptions.entrySet()) {
607                     String id = subscriptionEntry.getKey();
608                     Subscription sub = subscriptionEntry.getValue();
609                     List<SubscriptionCallback> callbackList = sub.getCallbacks();
610                     List<Bundle> optionsList = sub.getOptionsList();
611                     for (int i = 0; i < callbackList.size(); ++i) {
612                         try {
613                             mServiceBinder.addSubscription(id, callbackList.get(i).mToken,
614                                     optionsList.get(i), mServiceCallbacks);
615                         } catch (RemoteException ex) {
616                             // Process is crashing. We will disconnect, and upon reconnect we will
617                             // automatically reregister. So nothing to do here.
618                             Log.d(TAG, "addSubscription failed with RemoteException parentId="
619                                     + id);
620                         }
621                     }
622                 }
623             }
624         });
625     }
626 
onConnectionFailed(final IMediaBrowserServiceCallbacks callback)627     private void onConnectionFailed(final IMediaBrowserServiceCallbacks callback) {
628         mHandler.post(new Runnable() {
629             @Override
630             public void run() {
631                 Log.e(TAG, "onConnectFailed for " + mServiceComponent);
632 
633                 // Check to make sure there hasn't been a disconnect or a different
634                 // ServiceConnection.
635                 if (!isCurrent(callback, "onConnectFailed")) {
636                     return;
637                 }
638                 // Don't allow them to call us twice.
639                 if (mState != CONNECT_STATE_CONNECTING) {
640                     Log.w(TAG, "onConnect from service while mState="
641                             + getStateLabel(mState) + "... ignoring");
642                     return;
643                 }
644 
645                 // Clean up
646                 forceCloseConnection();
647 
648                 // Tell the app.
649                 mCallback.onConnectionFailed();
650             }
651         });
652     }
653 
onLoadChildren(final IMediaBrowserServiceCallbacks callback, final String parentId, final ParceledListSlice list, final Bundle options)654     private void onLoadChildren(final IMediaBrowserServiceCallbacks callback,
655             final String parentId, final ParceledListSlice list, final Bundle options) {
656         mHandler.post(new Runnable() {
657             @Override
658             public void run() {
659                 // Check that there hasn't been a disconnect or a different
660                 // ServiceConnection.
661                 if (!isCurrent(callback, "onLoadChildren")) {
662                     return;
663                 }
664 
665                 if (DBG) {
666                     Log.d(TAG, "onLoadChildren for " + mServiceComponent + " id=" + parentId);
667                 }
668 
669                 // Check that the subscription is still subscribed.
670                 final Subscription subscription = mSubscriptions.get(parentId);
671                 if (subscription != null) {
672                     // Tell the app.
673                     SubscriptionCallback subscriptionCallback =
674                             subscription.getCallback(mContext, options);
675                     if (subscriptionCallback != null) {
676                         List<MediaItem> data = list == null ? null : list.getList();
677                         if (options == null) {
678                             if (data == null) {
679                                 subscriptionCallback.onError(parentId);
680                             } else {
681                                 subscriptionCallback.onChildrenLoaded(parentId, data);
682                             }
683                         } else {
684                             if (data == null) {
685                                 subscriptionCallback.onError(parentId, options);
686                             } else {
687                                 subscriptionCallback.onChildrenLoaded(parentId, data, options);
688                             }
689                         }
690                         return;
691                     }
692                 }
693                 if (DBG) {
694                     Log.d(TAG, "onLoadChildren for id that isn't subscribed id=" + parentId);
695                 }
696             }
697         });
698     }
699 
700     /**
701      * Return true if {@code callback} is the current ServiceCallbacks. Also logs if it's not.
702      */
isCurrent(IMediaBrowserServiceCallbacks callback, String funcName)703     private boolean isCurrent(IMediaBrowserServiceCallbacks callback, String funcName) {
704         if (mServiceCallbacks != callback || mState == CONNECT_STATE_DISCONNECTING
705                 || mState == CONNECT_STATE_DISCONNECTED) {
706             if (mState != CONNECT_STATE_DISCONNECTING && mState != CONNECT_STATE_DISCONNECTED) {
707                 Log.i(TAG, funcName + " for " + mServiceComponent + " with mServiceConnection="
708                         + mServiceCallbacks + " this=" + this);
709             }
710             return false;
711         }
712         return true;
713     }
714 
getNewServiceCallbacks()715     private ServiceCallbacks getNewServiceCallbacks() {
716         return new ServiceCallbacks(this);
717     }
718 
719     /**
720      * Log internal state.
721      * @hide
722      */
dump()723     void dump() {
724         Log.d(TAG, "MediaBrowser...");
725         Log.d(TAG, "  mServiceComponent=" + mServiceComponent);
726         Log.d(TAG, "  mCallback=" + mCallback);
727         Log.d(TAG, "  mRootHints=" + mRootHints);
728         Log.d(TAG, "  mState=" + getStateLabel(mState));
729         Log.d(TAG, "  mServiceConnection=" + mServiceConnection);
730         Log.d(TAG, "  mServiceBinder=" + mServiceBinder);
731         Log.d(TAG, "  mServiceCallbacks=" + mServiceCallbacks);
732         Log.d(TAG, "  mRootId=" + mRootId);
733         Log.d(TAG, "  mMediaSessionToken=" + mMediaSessionToken);
734     }
735 
736     /**
737      * A class with information on a single media item for use in browsing/searching media.
738      * MediaItems are application dependent so we cannot guarantee that they contain the
739      * right values.
740      */
741     public static class MediaItem implements Parcelable {
742         private final int mFlags;
743         private final MediaDescription mDescription;
744 
745         /** @hide */
746         @Retention(RetentionPolicy.SOURCE)
747         @IntDef(flag = true, value = { FLAG_BROWSABLE, FLAG_PLAYABLE })
748         public @interface Flags { }
749 
750         /**
751          * Flag: Indicates that the item has children of its own.
752          */
753         public static final int FLAG_BROWSABLE = 1 << 0;
754 
755         /**
756          * Flag: Indicates that the item is playable.
757          * <p>
758          * The id of this item may be passed to
759          * {@link android.media.session.MediaController.TransportControls
760          * #playFromMediaId(String, Bundle)} to start playing it.
761          * </p>
762          */
763         public static final int FLAG_PLAYABLE = 1 << 1;
764 
765         /**
766          * Create a new MediaItem for use in browsing media.
767          * @param description The description of the media, which must include a
768          *            media id.
769          * @param flags The flags for this item.
770          */
MediaItem(@onNull MediaDescription description, @Flags int flags)771         public MediaItem(@NonNull MediaDescription description, @Flags int flags) {
772             if (description == null) {
773                 throw new IllegalArgumentException("description cannot be null");
774             }
775             if (TextUtils.isEmpty(description.getMediaId())) {
776                 throw new IllegalArgumentException("description must have a non-empty media id");
777             }
778             mFlags = flags;
779             mDescription = description;
780         }
781 
782         /**
783          * Private constructor.
784          */
MediaItem(Parcel in)785         private MediaItem(Parcel in) {
786             mFlags = in.readInt();
787             mDescription = MediaDescription.CREATOR.createFromParcel(in);
788         }
789 
790         @Override
describeContents()791         public int describeContents() {
792             return 0;
793         }
794 
795         @Override
writeToParcel(Parcel out, int flags)796         public void writeToParcel(Parcel out, int flags) {
797             out.writeInt(mFlags);
798             mDescription.writeToParcel(out, flags);
799         }
800 
801         @Override
toString()802         public String toString() {
803             final StringBuilder sb = new StringBuilder("MediaItem{");
804             sb.append("mFlags=").append(mFlags);
805             sb.append(", mDescription=").append(mDescription);
806             sb.append('}');
807             return sb.toString();
808         }
809 
810         public static final @android.annotation.NonNull Parcelable.Creator<MediaItem> CREATOR =
811                 new Parcelable.Creator<MediaItem>() {
812                     @Override
813                     public MediaItem createFromParcel(Parcel in) {
814                         return new MediaItem(in);
815                     }
816 
817                     @Override
818                     public MediaItem[] newArray(int size) {
819                         return new MediaItem[size];
820                     }
821                 };
822 
823         /**
824          * Gets the flags of the item.
825          */
getFlags()826         public @Flags int getFlags() {
827             return mFlags;
828         }
829 
830         /**
831          * Returns whether this item is browsable.
832          * @see #FLAG_BROWSABLE
833          */
isBrowsable()834         public boolean isBrowsable() {
835             return (mFlags & FLAG_BROWSABLE) != 0;
836         }
837 
838         /**
839          * Returns whether this item is playable.
840          * @see #FLAG_PLAYABLE
841          */
isPlayable()842         public boolean isPlayable() {
843             return (mFlags & FLAG_PLAYABLE) != 0;
844         }
845 
846         /**
847          * Returns the description of the media.
848          */
getDescription()849         public @NonNull MediaDescription getDescription() {
850             return mDescription;
851         }
852 
853         /**
854          * Returns the media id in the {@link MediaDescription} for this item.
855          * @see android.media.MediaMetadata#METADATA_KEY_MEDIA_ID
856          */
getMediaId()857         public @Nullable String getMediaId() {
858             return mDescription.getMediaId();
859         }
860     }
861 
862     /**
863      * Callbacks for connection related events.
864      */
865     public static class ConnectionCallback {
866         /**
867          * Invoked after {@link MediaBrowser#connect()} when the request has successfully completed.
868          */
onConnected()869         public void onConnected() {
870         }
871 
872         /**
873          * Invoked when the client is disconnected from the media browser.
874          */
onConnectionSuspended()875         public void onConnectionSuspended() {
876         }
877 
878         /**
879          * Invoked when the connection to the media browser failed.
880          */
onConnectionFailed()881         public void onConnectionFailed() {
882         }
883     }
884 
885     /**
886      * Callbacks for subscription related events.
887      */
888     public abstract static class SubscriptionCallback {
889         Binder mToken;
890 
SubscriptionCallback()891         public SubscriptionCallback() {
892             mToken = new Binder();
893         }
894 
895         /**
896          * Called when the list of children is loaded or updated.
897          *
898          * @param parentId The media id of the parent media item.
899          * @param children The children which were loaded.
900          */
onChildrenLoaded(@onNull String parentId, @NonNull List<MediaItem> children)901         public void onChildrenLoaded(@NonNull String parentId, @NonNull List<MediaItem> children) {
902         }
903 
904         /**
905          * Called when the list of children is loaded or updated.
906          *
907          * @param parentId The media id of the parent media item.
908          * @param children The children which were loaded.
909          * @param options The bundle of service-specific arguments sent to the media
910          *            browser service. The contents of this bundle may affect the
911          *            information returned when browsing.
912          */
onChildrenLoaded(@onNull String parentId, @NonNull List<MediaItem> children, @NonNull Bundle options)913         public void onChildrenLoaded(@NonNull String parentId, @NonNull List<MediaItem> children,
914                 @NonNull Bundle options) {
915         }
916 
917         /**
918          * Called when the id doesn't exist or other errors in subscribing.
919          * <p>
920          * If this is called, the subscription remains until {@link MediaBrowser#unsubscribe}
921          * called, because some errors may heal themselves.
922          * </p>
923          *
924          * @param parentId The media id of the parent media item whose children could
925          *            not be loaded.
926          */
onError(@onNull String parentId)927         public void onError(@NonNull String parentId) {
928         }
929 
930         /**
931          * Called when the id doesn't exist or other errors in subscribing.
932          * <p>
933          * If this is called, the subscription remains until {@link MediaBrowser#unsubscribe}
934          * called, because some errors may heal themselves.
935          * </p>
936          *
937          * @param parentId The media id of the parent media item whose children could
938          *            not be loaded.
939          * @param options The bundle of service-specific arguments sent to the media
940          *            browser service.
941          */
onError(@onNull String parentId, @NonNull Bundle options)942         public void onError(@NonNull String parentId, @NonNull Bundle options) {
943         }
944     }
945 
946     /**
947      * Callback for receiving the result of {@link #getItem}.
948      */
949     public abstract static class ItemCallback {
950         /**
951          * Called when the item has been returned by the connected service.
952          *
953          * @param item The item that was returned or null if it doesn't exist.
954          */
onItemLoaded(MediaItem item)955         public void onItemLoaded(MediaItem item) {
956         }
957 
958         /**
959          * Called there was an error retrieving it or the connected service doesn't support
960          * {@link #getItem}.
961          *
962          * @param mediaId The media id of the media item which could not be loaded.
963          */
onError(@onNull String mediaId)964         public void onError(@NonNull String mediaId) {
965         }
966     }
967 
968     /**
969      * ServiceConnection to the other app.
970      */
971     private class MediaServiceConnection implements ServiceConnection {
972         @Override
onServiceConnected(final ComponentName name, final IBinder binder)973         public void onServiceConnected(final ComponentName name, final IBinder binder) {
974             postOrRun(new Runnable() {
975                 @Override
976                 public void run() {
977                     if (DBG) {
978                         Log.d(TAG, "MediaServiceConnection.onServiceConnected name=" + name
979                                 + " binder=" + binder);
980                         dump();
981                     }
982 
983                     // Make sure we are still the current connection, and that they haven't called
984                     // disconnect().
985                     if (!isCurrent("onServiceConnected")) {
986                         return;
987                     }
988 
989                     // Save their binder
990                     mServiceBinder = IMediaBrowserService.Stub.asInterface(binder);
991 
992                     // We make a new mServiceCallbacks each time we connect so that we can drop
993                     // responses from previous connections.
994                     mServiceCallbacks = getNewServiceCallbacks();
995                     mState = CONNECT_STATE_CONNECTING;
996 
997                     // Call connect, which is async. When we get a response from that we will
998                     // say that we're connected.
999                     try {
1000                         if (DBG) {
1001                             Log.d(TAG, "ServiceCallbacks.onConnect...");
1002                             dump();
1003                         }
1004                         mServiceBinder.connect(mContext.getPackageName(), mRootHints,
1005                                 mServiceCallbacks);
1006                     } catch (RemoteException ex) {
1007                         // Connect failed, which isn't good. But the auto-reconnect on the service
1008                         // will take over and we will come back. We will also get the
1009                         // onServiceDisconnected, which has all the cleanup code. So let that do
1010                         // it.
1011                         Log.w(TAG, "RemoteException during connect for " + mServiceComponent);
1012                         if (DBG) {
1013                             Log.d(TAG, "ServiceCallbacks.onConnect...");
1014                             dump();
1015                         }
1016                     }
1017                 }
1018             });
1019         }
1020 
1021         @Override
onServiceDisconnected(final ComponentName name)1022         public void onServiceDisconnected(final ComponentName name) {
1023             postOrRun(new Runnable() {
1024                 @Override
1025                 public void run() {
1026                     if (DBG) {
1027                         Log.d(TAG, "MediaServiceConnection.onServiceDisconnected name=" + name
1028                                 + " this=" + this + " mServiceConnection=" + mServiceConnection);
1029                         dump();
1030                     }
1031 
1032                     // Make sure we are still the current connection, and that they haven't called
1033                     // disconnect().
1034                     if (!isCurrent("onServiceDisconnected")) {
1035                         return;
1036                     }
1037 
1038                     // Clear out what we set in onServiceConnected
1039                     mServiceBinder = null;
1040                     mServiceCallbacks = null;
1041 
1042                     // And tell the app that it's suspended.
1043                     mState = CONNECT_STATE_SUSPENDED;
1044                     mCallback.onConnectionSuspended();
1045                 }
1046             });
1047         }
1048 
postOrRun(Runnable r)1049         private void postOrRun(Runnable r) {
1050             if (Thread.currentThread() == mHandler.getLooper().getThread()) {
1051                 r.run();
1052             } else {
1053                 mHandler.post(r);
1054             }
1055         }
1056 
1057         /**
1058          * Return true if this is the current ServiceConnection. Also logs if it's not.
1059          */
isCurrent(String funcName)1060         private boolean isCurrent(String funcName) {
1061             if (mServiceConnection != this || mState == CONNECT_STATE_DISCONNECTING
1062                     || mState == CONNECT_STATE_DISCONNECTED) {
1063                 if (mState != CONNECT_STATE_DISCONNECTING && mState != CONNECT_STATE_DISCONNECTED) {
1064                     // Check mState, because otherwise this log is noisy.
1065                     Log.i(TAG, funcName + " for " + mServiceComponent + " with mServiceConnection="
1066                             + mServiceConnection + " this=" + this);
1067                 }
1068                 return false;
1069             }
1070             return true;
1071         }
1072     }
1073 
1074     /**
1075      * Callbacks from the service.
1076      */
1077     private static class ServiceCallbacks extends IMediaBrowserServiceCallbacks.Stub {
1078         private WeakReference<MediaBrowser> mMediaBrowser;
1079 
ServiceCallbacks(MediaBrowser mediaBrowser)1080         ServiceCallbacks(MediaBrowser mediaBrowser) {
1081             mMediaBrowser = new WeakReference<MediaBrowser>(mediaBrowser);
1082         }
1083 
1084         /**
1085          * The other side has acknowledged our connection. The parameters to this function
1086          * are the initial data as requested.
1087          */
1088         @Override
onConnect(String root, MediaSession.Token session, final Bundle extras)1089         public void onConnect(String root, MediaSession.Token session,
1090                 final Bundle extras) {
1091             MediaBrowser mediaBrowser = mMediaBrowser.get();
1092             if (mediaBrowser != null) {
1093                 mediaBrowser.onServiceConnected(this, root, session, extras);
1094             }
1095         }
1096 
1097         /**
1098          * The other side does not like us. Tell the app via onConnectionFailed.
1099          */
1100         @Override
onConnectFailed()1101         public void onConnectFailed() {
1102             MediaBrowser mediaBrowser = mMediaBrowser.get();
1103             if (mediaBrowser != null) {
1104                 mediaBrowser.onConnectionFailed(this);
1105             }
1106         }
1107 
1108         @Override
onLoadChildren(String parentId, ParceledListSlice list, Bundle options)1109         public void onLoadChildren(String parentId, ParceledListSlice list, Bundle options) {
1110             MediaBrowser mediaBrowser = mMediaBrowser.get();
1111             if (mediaBrowser != null) {
1112                 mediaBrowser.onLoadChildren(this, parentId, list, options);
1113             }
1114         }
1115     }
1116 
1117     private static class Subscription {
1118         private final List<SubscriptionCallback> mCallbacks;
1119         private final List<Bundle> mOptionsList;
1120 
Subscription()1121         Subscription() {
1122             mCallbacks = new ArrayList<>();
1123             mOptionsList = new ArrayList<>();
1124         }
1125 
isEmpty()1126         public boolean isEmpty() {
1127             return mCallbacks.isEmpty();
1128         }
1129 
getOptionsList()1130         public List<Bundle> getOptionsList() {
1131             return mOptionsList;
1132         }
1133 
getCallbacks()1134         public List<SubscriptionCallback> getCallbacks() {
1135             return mCallbacks;
1136         }
1137 
getCallback(Context context, Bundle options)1138         public SubscriptionCallback getCallback(Context context, Bundle options) {
1139             if (options != null) {
1140                 options.setClassLoader(context.getClassLoader());
1141             }
1142             for (int i = 0; i < mOptionsList.size(); ++i) {
1143                 if (MediaBrowserUtils.areSameOptions(mOptionsList.get(i), options)) {
1144                     return mCallbacks.get(i);
1145                 }
1146             }
1147             return null;
1148         }
1149 
putCallback(Context context, Bundle options, SubscriptionCallback callback)1150         public void putCallback(Context context, Bundle options, SubscriptionCallback callback) {
1151             if (options != null) {
1152                 options.setClassLoader(context.getClassLoader());
1153             }
1154             for (int i = 0; i < mOptionsList.size(); ++i) {
1155                 if (MediaBrowserUtils.areSameOptions(mOptionsList.get(i), options)) {
1156                     mCallbacks.set(i, callback);
1157                     return;
1158                 }
1159             }
1160             mCallbacks.add(callback);
1161             mOptionsList.add(options);
1162         }
1163     }
1164 }
1165