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