• 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 
onDisconnectRequested(ServiceCallbacks callback)700     private void onDisconnectRequested(ServiceCallbacks callback) {
701         mHandler.post(
702                 () -> {
703                     Log.i(TAG, "onDisconnectRequest for " + mServiceComponent);
704 
705                     if (!isCurrent(callback, "onDisconnectRequest")) {
706                         return;
707                     }
708                     forceCloseConnection();
709                     mCallback.onDisconnected();
710                 });
711     }
712 
713     /**
714      * Return true if {@code callback} is the current ServiceCallbacks. Also logs if it's not.
715      */
isCurrent(IMediaBrowserServiceCallbacks callback, String funcName)716     private boolean isCurrent(IMediaBrowserServiceCallbacks callback, String funcName) {
717         if (mServiceCallbacks != callback || mState == CONNECT_STATE_DISCONNECTING
718                 || mState == CONNECT_STATE_DISCONNECTED) {
719             if (mState != CONNECT_STATE_DISCONNECTING && mState != CONNECT_STATE_DISCONNECTED) {
720                 Log.i(TAG, funcName + " for " + mServiceComponent + " with mServiceConnection="
721                         + mServiceCallbacks + " this=" + this);
722             }
723             return false;
724         }
725         return true;
726     }
727 
getNewServiceCallbacks()728     private ServiceCallbacks getNewServiceCallbacks() {
729         return new ServiceCallbacks(this);
730     }
731 
732     /**
733      * Log internal state.
734      * @hide
735      */
dump()736     void dump() {
737         Log.d(TAG, "MediaBrowser...");
738         Log.d(TAG, "  mServiceComponent=" + mServiceComponent);
739         Log.d(TAG, "  mCallback=" + mCallback);
740         Log.d(TAG, "  mRootHints=" + mRootHints);
741         Log.d(TAG, "  mState=" + getStateLabel(mState));
742         Log.d(TAG, "  mServiceConnection=" + mServiceConnection);
743         Log.d(TAG, "  mServiceBinder=" + mServiceBinder);
744         Log.d(TAG, "  mServiceCallbacks=" + mServiceCallbacks);
745         Log.d(TAG, "  mRootId=" + mRootId);
746         Log.d(TAG, "  mMediaSessionToken=" + mMediaSessionToken);
747     }
748 
749     /**
750      * A class with information on a single media item for use in browsing/searching media.
751      * MediaItems are application dependent so we cannot guarantee that they contain the
752      * right values.
753      */
754     public static class MediaItem implements Parcelable {
755         private final int mFlags;
756         private final MediaDescription mDescription;
757 
758         /** @hide */
759         @Retention(RetentionPolicy.SOURCE)
760         @IntDef(flag = true, value = { FLAG_BROWSABLE, FLAG_PLAYABLE })
761         public @interface Flags { }
762 
763         /**
764          * Flag: Indicates that the item has children of its own.
765          */
766         public static final int FLAG_BROWSABLE = 1 << 0;
767 
768         /**
769          * Flag: Indicates that the item is playable.
770          * <p>
771          * The id of this item may be passed to
772          * {@link android.media.session.MediaController.TransportControls
773          * #playFromMediaId(String, Bundle)} to start playing it.
774          * </p>
775          */
776         public static final int FLAG_PLAYABLE = 1 << 1;
777 
778         /**
779          * Create a new MediaItem for use in browsing media.
780          * @param description The description of the media, which must include a
781          *            media id.
782          * @param flags The flags for this item.
783          */
MediaItem(@onNull MediaDescription description, @Flags int flags)784         public MediaItem(@NonNull MediaDescription description, @Flags int flags) {
785             if (description == null) {
786                 throw new IllegalArgumentException("description cannot be null");
787             }
788             if (TextUtils.isEmpty(description.getMediaId())) {
789                 throw new IllegalArgumentException("description must have a non-empty media id");
790             }
791             mFlags = flags;
792             mDescription = description;
793         }
794 
795         /**
796          * Private constructor.
797          */
MediaItem(Parcel in)798         private MediaItem(Parcel in) {
799             mFlags = in.readInt();
800             mDescription = MediaDescription.CREATOR.createFromParcel(in);
801         }
802 
803         @Override
describeContents()804         public int describeContents() {
805             return 0;
806         }
807 
808         @Override
writeToParcel(Parcel out, int flags)809         public void writeToParcel(Parcel out, int flags) {
810             out.writeInt(mFlags);
811             mDescription.writeToParcel(out, flags);
812         }
813 
814         @Override
toString()815         public String toString() {
816             final StringBuilder sb = new StringBuilder("MediaItem{");
817             sb.append("mFlags=").append(mFlags);
818             sb.append(", mDescription=").append(mDescription);
819             sb.append('}');
820             return sb.toString();
821         }
822 
823         public static final @android.annotation.NonNull Parcelable.Creator<MediaItem> CREATOR =
824                 new Parcelable.Creator<MediaItem>() {
825                     @Override
826                     public MediaItem createFromParcel(Parcel in) {
827                         return new MediaItem(in);
828                     }
829 
830                     @Override
831                     public MediaItem[] newArray(int size) {
832                         return new MediaItem[size];
833                     }
834                 };
835 
836         /**
837          * Gets the flags of the item.
838          */
getFlags()839         public @Flags int getFlags() {
840             return mFlags;
841         }
842 
843         /**
844          * Returns whether this item is browsable.
845          * @see #FLAG_BROWSABLE
846          */
isBrowsable()847         public boolean isBrowsable() {
848             return (mFlags & FLAG_BROWSABLE) != 0;
849         }
850 
851         /**
852          * Returns whether this item is playable.
853          * @see #FLAG_PLAYABLE
854          */
isPlayable()855         public boolean isPlayable() {
856             return (mFlags & FLAG_PLAYABLE) != 0;
857         }
858 
859         /**
860          * Returns the description of the media.
861          */
getDescription()862         public @NonNull MediaDescription getDescription() {
863             return mDescription;
864         }
865 
866         /**
867          * Returns the media id in the {@link MediaDescription} for this item.
868          * @see android.media.MediaMetadata#METADATA_KEY_MEDIA_ID
869          */
getMediaId()870         public @Nullable String getMediaId() {
871             return mDescription.getMediaId();
872         }
873     }
874 
875     /**
876      * Callbacks for connection related events.
877      */
878     public static class ConnectionCallback {
879         /**
880          * Invoked after {@link MediaBrowser#connect()} when the request has successfully completed.
881          */
onConnected()882         public void onConnected() {
883         }
884 
885         /**
886          * Invoked when the client is disconnected from the media browser.
887          */
onConnectionSuspended()888         public void onConnectionSuspended() {
889         }
890 
891         /**
892          * Invoked when the connection to the media browser failed.
893          */
onConnectionFailed()894         public void onConnectionFailed() {
895         }
896 
897         /**
898          * Invoked after disconnecting by request of the {@link MediaBrowserService}.
899          *
900          * <p>The default implementation of this method calls {@link #onConnectionFailed()}.
901          *
902          * @hide
903          */
904         // TODO: b/185136506 - Consider publishing this API in the next window for API changes, if
905         // the need arises.
onDisconnected()906         public void onDisconnected() {
907             onConnectionFailed();
908         }
909     }
910 
911     /**
912      * Callbacks for subscription related events.
913      */
914     public abstract static class SubscriptionCallback {
915         Binder mToken;
916 
SubscriptionCallback()917         public SubscriptionCallback() {
918             mToken = new Binder();
919         }
920 
921         /**
922          * Called when the list of children is loaded or updated.
923          *
924          * @param parentId The media id of the parent media item.
925          * @param children The children which were loaded.
926          */
onChildrenLoaded(@onNull String parentId, @NonNull List<MediaItem> children)927         public void onChildrenLoaded(@NonNull String parentId, @NonNull List<MediaItem> children) {
928         }
929 
930         /**
931          * Called when the list of children is loaded or updated.
932          *
933          * @param parentId The media id of the parent media item.
934          * @param children The children which were loaded.
935          * @param options The bundle of service-specific arguments sent to the media
936          *            browser service. The contents of this bundle may affect the
937          *            information returned when browsing.
938          */
onChildrenLoaded(@onNull String parentId, @NonNull List<MediaItem> children, @NonNull Bundle options)939         public void onChildrenLoaded(@NonNull String parentId, @NonNull List<MediaItem> children,
940                 @NonNull Bundle options) {
941         }
942 
943         /**
944          * Called when the id doesn't exist or other errors in subscribing.
945          * <p>
946          * If this is called, the subscription remains until {@link MediaBrowser#unsubscribe}
947          * called, because some errors may heal themselves.
948          * </p>
949          *
950          * @param parentId The media id of the parent media item whose children could
951          *            not be loaded.
952          */
onError(@onNull String parentId)953         public void onError(@NonNull String parentId) {
954         }
955 
956         /**
957          * Called when the id doesn't exist or other errors in subscribing.
958          * <p>
959          * If this is called, the subscription remains until {@link MediaBrowser#unsubscribe}
960          * called, because some errors may heal themselves.
961          * </p>
962          *
963          * @param parentId The media id of the parent media item whose children could
964          *            not be loaded.
965          * @param options The bundle of service-specific arguments sent to the media
966          *            browser service.
967          */
onError(@onNull String parentId, @NonNull Bundle options)968         public void onError(@NonNull String parentId, @NonNull Bundle options) {
969         }
970     }
971 
972     /**
973      * Callback for receiving the result of {@link #getItem}.
974      */
975     public abstract static class ItemCallback {
976         /**
977          * Called when the item has been returned by the connected service.
978          *
979          * @param item The item that was returned or null if it doesn't exist.
980          */
onItemLoaded(MediaItem item)981         public void onItemLoaded(MediaItem item) {
982         }
983 
984         /**
985          * Called there was an error retrieving it or the connected service doesn't support
986          * {@link #getItem}.
987          *
988          * @param mediaId The media id of the media item which could not be loaded.
989          */
onError(@onNull String mediaId)990         public void onError(@NonNull String mediaId) {
991         }
992     }
993 
994     /**
995      * ServiceConnection to the other app.
996      */
997     private class MediaServiceConnection implements ServiceConnection {
998         @Override
onServiceConnected(final ComponentName name, final IBinder binder)999         public void onServiceConnected(final ComponentName name, final IBinder binder) {
1000             postOrRun(new Runnable() {
1001                 @Override
1002                 public void run() {
1003                     if (DBG) {
1004                         Log.d(TAG, "MediaServiceConnection.onServiceConnected name=" + name
1005                                 + " binder=" + binder);
1006                         dump();
1007                     }
1008 
1009                     // Make sure we are still the current connection, and that they haven't called
1010                     // disconnect().
1011                     if (!isCurrent("onServiceConnected")) {
1012                         return;
1013                     }
1014 
1015                     // Save their binder
1016                     mServiceBinder = IMediaBrowserService.Stub.asInterface(binder);
1017 
1018                     // We make a new mServiceCallbacks each time we connect so that we can drop
1019                     // responses from previous connections.
1020                     mServiceCallbacks = getNewServiceCallbacks();
1021                     mState = CONNECT_STATE_CONNECTING;
1022 
1023                     // Call connect, which is async. When we get a response from that we will
1024                     // say that we're connected.
1025                     try {
1026                         if (DBG) {
1027                             Log.d(TAG, "ServiceCallbacks.onConnect...");
1028                             dump();
1029                         }
1030                         mServiceBinder.connect(mContext.getPackageName(), mRootHints,
1031                                 mServiceCallbacks);
1032                     } catch (RemoteException ex) {
1033                         // Connect failed, which isn't good. But the auto-reconnect on the service
1034                         // will take over and we will come back. We will also get the
1035                         // onServiceDisconnected, which has all the cleanup code. So let that do
1036                         // it.
1037                         Log.w(TAG, "RemoteException during connect for " + mServiceComponent);
1038                         if (DBG) {
1039                             Log.d(TAG, "ServiceCallbacks.onConnect...");
1040                             dump();
1041                         }
1042                     }
1043                 }
1044             });
1045         }
1046 
1047         @Override
onServiceDisconnected(final ComponentName name)1048         public void onServiceDisconnected(final ComponentName name) {
1049             postOrRun(new Runnable() {
1050                 @Override
1051                 public void run() {
1052                     if (DBG) {
1053                         Log.d(TAG, "MediaServiceConnection.onServiceDisconnected name=" + name
1054                                 + " this=" + this + " mServiceConnection=" + mServiceConnection);
1055                         dump();
1056                     }
1057 
1058                     // Make sure we are still the current connection, and that they haven't called
1059                     // disconnect().
1060                     if (!isCurrent("onServiceDisconnected")) {
1061                         return;
1062                     }
1063 
1064                     // Clear out what we set in onServiceConnected
1065                     mServiceBinder = null;
1066                     mServiceCallbacks = null;
1067 
1068                     // And tell the app that it's suspended.
1069                     mState = CONNECT_STATE_SUSPENDED;
1070                     mCallback.onConnectionSuspended();
1071                 }
1072             });
1073         }
1074 
postOrRun(Runnable r)1075         private void postOrRun(Runnable r) {
1076             if (Thread.currentThread() == mHandler.getLooper().getThread()) {
1077                 r.run();
1078             } else {
1079                 mHandler.post(r);
1080             }
1081         }
1082 
1083         /**
1084          * Return true if this is the current ServiceConnection. Also logs if it's not.
1085          */
isCurrent(String funcName)1086         private boolean isCurrent(String funcName) {
1087             if (mServiceConnection != this || mState == CONNECT_STATE_DISCONNECTING
1088                     || mState == CONNECT_STATE_DISCONNECTED) {
1089                 if (mState != CONNECT_STATE_DISCONNECTING && mState != CONNECT_STATE_DISCONNECTED) {
1090                     // Check mState, because otherwise this log is noisy.
1091                     Log.i(TAG, funcName + " for " + mServiceComponent + " with mServiceConnection="
1092                             + mServiceConnection + " this=" + this);
1093                 }
1094                 return false;
1095             }
1096             return true;
1097         }
1098     }
1099 
1100     /**
1101      * Callbacks from the service.
1102      */
1103     private static class ServiceCallbacks extends IMediaBrowserServiceCallbacks.Stub {
1104         private WeakReference<MediaBrowser> mMediaBrowser;
1105 
ServiceCallbacks(MediaBrowser mediaBrowser)1106         ServiceCallbacks(MediaBrowser mediaBrowser) {
1107             mMediaBrowser = new WeakReference<MediaBrowser>(mediaBrowser);
1108         }
1109 
1110         /**
1111          * The other side has acknowledged our connection. The parameters to this function
1112          * are the initial data as requested.
1113          */
1114         @Override
onConnect(String root, MediaSession.Token session, final Bundle extras)1115         public void onConnect(String root, MediaSession.Token session,
1116                 final Bundle extras) {
1117             MediaBrowser mediaBrowser = mMediaBrowser.get();
1118             if (mediaBrowser != null) {
1119                 mediaBrowser.onServiceConnected(this, root, session, extras);
1120             }
1121         }
1122 
1123         /**
1124          * The other side does not like us. Tell the app via onConnectionFailed.
1125          */
1126         @Override
onConnectFailed()1127         public void onConnectFailed() {
1128             MediaBrowser mediaBrowser = mMediaBrowser.get();
1129             if (mediaBrowser != null) {
1130                 mediaBrowser.onConnectionFailed(this);
1131             }
1132         }
1133 
1134         @Override
onLoadChildren(String parentId, ParceledListSlice list, Bundle options)1135         public void onLoadChildren(String parentId, ParceledListSlice list, Bundle options) {
1136             MediaBrowser mediaBrowser = mMediaBrowser.get();
1137             if (mediaBrowser != null) {
1138                 mediaBrowser.onLoadChildren(this, parentId, list, options);
1139             }
1140         }
1141 
1142         @Override
onDisconnect()1143         public void onDisconnect() {
1144             MediaBrowser mediaBrowser = mMediaBrowser.get();
1145             if (mediaBrowser != null) {
1146                 mediaBrowser.onDisconnectRequested(this);
1147             }
1148         }
1149     }
1150 
1151     private static class Subscription {
1152         private final List<SubscriptionCallback> mCallbacks;
1153         private final List<Bundle> mOptionsList;
1154 
Subscription()1155         Subscription() {
1156             mCallbacks = new ArrayList<>();
1157             mOptionsList = new ArrayList<>();
1158         }
1159 
isEmpty()1160         public boolean isEmpty() {
1161             return mCallbacks.isEmpty();
1162         }
1163 
getOptionsList()1164         public List<Bundle> getOptionsList() {
1165             return mOptionsList;
1166         }
1167 
getCallbacks()1168         public List<SubscriptionCallback> getCallbacks() {
1169             return mCallbacks;
1170         }
1171 
getCallback(Context context, Bundle options)1172         public SubscriptionCallback getCallback(Context context, Bundle options) {
1173             if (options != null) {
1174                 options.setClassLoader(context.getClassLoader());
1175             }
1176             for (int i = 0; i < mOptionsList.size(); ++i) {
1177                 if (MediaBrowserUtils.areSameOptions(mOptionsList.get(i), options)) {
1178                     return mCallbacks.get(i);
1179                 }
1180             }
1181             return null;
1182         }
1183 
putCallback(Context context, Bundle options, SubscriptionCallback callback)1184         public void putCallback(Context context, Bundle options, SubscriptionCallback callback) {
1185             if (options != null) {
1186                 options.setClassLoader(context.getClassLoader());
1187             }
1188             for (int i = 0; i < mOptionsList.size(); ++i) {
1189                 if (MediaBrowserUtils.areSameOptions(mOptionsList.get(i), options)) {
1190                     mCallbacks.set(i, callback);
1191                     return;
1192                 }
1193             }
1194             mCallbacks.add(callback);
1195             mOptionsList.add(options);
1196         }
1197     }
1198 }
1199