Home | History | Annotate | Download | only in browse
      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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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 
    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 
    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      */
    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 
    578     private final 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 
    628     private final 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 
    655     private final 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      */
    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 
    716     private ServiceCallbacks getNewServiceCallbacks() {
    717         return new ServiceCallbacks(this);
    718     }
    719 
    720     /**
    721      * Log internal state.
    722      * @hide
    723      */
    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          */
    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          */
    786         private MediaItem(Parcel in) {
    787             mFlags = in.readInt();
    788             mDescription = MediaDescription.CREATOR.createFromParcel(in);
    789         }
    790 
    791         @Override
    792         public int describeContents() {
    793             return 0;
    794         }
    795 
    796         @Override
    797         public void writeToParcel(Parcel out, int flags) {
    798             out.writeInt(mFlags);
    799             mDescription.writeToParcel(out, flags);
    800         }
    801 
    802         @Override
    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 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          */
    827         public @Flags int getFlags() {
    828             return mFlags;
    829         }
    830 
    831         /**
    832          * Returns whether this item is browsable.
    833          * @see #FLAG_BROWSABLE
    834          */
    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          */
    843         public boolean isPlayable() {
    844             return (mFlags & FLAG_PLAYABLE) != 0;
    845         }
    846 
    847         /**
    848          * Returns the description of the media.
    849          */
    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          */
    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          */
    870         public void onConnected() {
    871         }
    872 
    873         /**
    874          * Invoked when the client is disconnected from the media browser.
    875          */
    876         public void onConnectionSuspended() {
    877         }
    878 
    879         /**
    880          * Invoked when the connection to the media browser failed.
    881          */
    882         public void onConnectionFailed() {
    883         }
    884     }
    885 
    886     /**
    887      * Callbacks for subscription related events.
    888      */
    889     public static abstract class SubscriptionCallback {
    890         Binder mToken;
    891 
    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          */
    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          */
    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          */
    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          */
    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 static abstract 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          */
    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          */
    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
    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
   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 
   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          */
   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 
   1081         public 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
   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
   1102         public void onConnectFailed() {
   1103             MediaBrowser mediaBrowser = mMediaBrowser.get();
   1104             if (mediaBrowser != null) {
   1105                 mediaBrowser.onConnectionFailed(this);
   1106             }
   1107         }
   1108 
   1109         @Override
   1110         public void onLoadChildren(String parentId, ParceledListSlice list) {
   1111             onLoadChildrenWithOptions(parentId, list, null);
   1112         }
   1113 
   1114         @Override
   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 
   1128         public Subscription() {
   1129             mCallbacks = new ArrayList<>();
   1130             mOptionsList = new ArrayList<>();
   1131         }
   1132 
   1133         public boolean isEmpty() {
   1134             return mCallbacks.isEmpty();
   1135         }
   1136 
   1137         public List<Bundle> getOptionsList() {
   1138             return mOptionsList;
   1139         }
   1140 
   1141         public List<SubscriptionCallback> getCallbacks() {
   1142             return mCallbacks;
   1143         }
   1144 
   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 
   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