Home | History | Annotate | Download | only in media
      1 /*
      2  * Copyright 2018 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 androidx.media;
     18 
     19 import static androidx.annotation.RestrictTo.Scope.LIBRARY;
     20 import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_ADD_SUBSCRIPTION;
     21 import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_CONNECT;
     22 import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_DISCONNECT;
     23 import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_GET_MEDIA_ITEM;
     24 import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_REGISTER_CALLBACK_MESSENGER;
     25 import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_REMOVE_SUBSCRIPTION;
     26 import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_SEARCH;
     27 import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_SEND_CUSTOM_ACTION;
     28 import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_UNREGISTER_CALLBACK_MESSENGER;
     29 import static androidx.media.MediaBrowserProtocol.DATA_CALLBACK_TOKEN;
     30 import static androidx.media.MediaBrowserProtocol.DATA_CALLING_PID;
     31 import static androidx.media.MediaBrowserProtocol.DATA_CALLING_UID;
     32 import static androidx.media.MediaBrowserProtocol.DATA_CUSTOM_ACTION;
     33 import static androidx.media.MediaBrowserProtocol.DATA_CUSTOM_ACTION_EXTRAS;
     34 import static androidx.media.MediaBrowserProtocol.DATA_MEDIA_ITEM_ID;
     35 import static androidx.media.MediaBrowserProtocol.DATA_MEDIA_ITEM_LIST;
     36 import static androidx.media.MediaBrowserProtocol.DATA_MEDIA_SESSION_TOKEN;
     37 import static androidx.media.MediaBrowserProtocol.DATA_NOTIFY_CHILDREN_CHANGED_OPTIONS;
     38 import static androidx.media.MediaBrowserProtocol.DATA_OPTIONS;
     39 import static androidx.media.MediaBrowserProtocol.DATA_PACKAGE_NAME;
     40 import static androidx.media.MediaBrowserProtocol.DATA_RESULT_RECEIVER;
     41 import static androidx.media.MediaBrowserProtocol.DATA_ROOT_HINTS;
     42 import static androidx.media.MediaBrowserProtocol.DATA_SEARCH_EXTRAS;
     43 import static androidx.media.MediaBrowserProtocol.DATA_SEARCH_QUERY;
     44 import static androidx.media.MediaBrowserProtocol.EXTRA_CLIENT_VERSION;
     45 import static androidx.media.MediaBrowserProtocol.EXTRA_MESSENGER_BINDER;
     46 import static androidx.media.MediaBrowserProtocol.EXTRA_SERVICE_VERSION;
     47 import static androidx.media.MediaBrowserProtocol.EXTRA_SESSION_BINDER;
     48 import static androidx.media.MediaBrowserProtocol.SERVICE_MSG_ON_CONNECT;
     49 import static androidx.media.MediaBrowserProtocol.SERVICE_MSG_ON_CONNECT_FAILED;
     50 import static androidx.media.MediaBrowserProtocol.SERVICE_MSG_ON_LOAD_CHILDREN;
     51 import static androidx.media.MediaBrowserProtocol.SERVICE_VERSION_CURRENT;
     52 
     53 import android.app.Service;
     54 import android.content.Context;
     55 import android.content.Intent;
     56 import android.content.pm.PackageManager;
     57 import android.os.Binder;
     58 import android.os.Build;
     59 import android.os.Bundle;
     60 import android.os.Handler;
     61 import android.os.IBinder;
     62 import android.os.Message;
     63 import android.os.Messenger;
     64 import android.os.Parcel;
     65 import android.os.RemoteException;
     66 import android.service.media.MediaBrowserService;
     67 import android.support.v4.media.MediaBrowserCompat;
     68 import android.support.v4.media.session.IMediaSession;
     69 import android.support.v4.media.session.MediaSessionCompat;
     70 import android.support.v4.os.ResultReceiver;
     71 import android.text.TextUtils;
     72 import android.util.Log;
     73 
     74 import androidx.annotation.IntDef;
     75 import androidx.annotation.NonNull;
     76 import androidx.annotation.Nullable;
     77 import androidx.annotation.RequiresApi;
     78 import androidx.annotation.RestrictTo;
     79 import androidx.collection.ArrayMap;
     80 import androidx.core.app.BundleCompat;
     81 import androidx.core.util.Pair;
     82 import androidx.media.MediaSessionManager.RemoteUserInfo;
     83 
     84 import java.io.FileDescriptor;
     85 import java.io.PrintWriter;
     86 import java.lang.annotation.Retention;
     87 import java.lang.annotation.RetentionPolicy;
     88 import java.util.ArrayList;
     89 import java.util.Collections;
     90 import java.util.HashMap;
     91 import java.util.Iterator;
     92 import java.util.List;
     93 
     94 /**
     95  * Base class for media browse services.
     96  * <p>
     97  * Media browse services enable applications to browse media content provided by an application
     98  * and ask the application to start playing it. They may also be used to control content that
     99  * is already playing by way of a {@link MediaSessionCompat}.
    100  * </p>
    101  *
    102  * To extend this class, you must declare the service in your manifest file with
    103  * an intent filter with the {@link #SERVICE_INTERFACE} action.
    104  *
    105  * For example:
    106  * </p><pre>
    107  * &lt;service android:name=".MyMediaBrowserServiceCompat"
    108  *          android:label="&#64;string/service_name" >
    109  *     &lt;intent-filter>
    110  *         &lt;action android:name="android.media.browse.MediaBrowserService" />
    111  *     &lt;/intent-filter>
    112  * &lt;/service>
    113  * </pre>
    114  *
    115  * <div class="special reference">
    116  * <h3>Developer Guides</h3>
    117  * <p>For information about building your media application, read the
    118  * <a href="{@docRoot}guide/topics/media-apps/index.html">Media Apps</a> developer guide.</p>
    119  * </div>
    120  */
    121 public abstract class MediaBrowserServiceCompat extends Service {
    122     static final String TAG = "MBServiceCompat";
    123     static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
    124 
    125     private static final float EPSILON = 0.00001f;
    126 
    127     private MediaBrowserServiceImpl mImpl;
    128 
    129     /**
    130      * The {@link Intent} that must be declared as handled by the service.
    131      */
    132     public static final String SERVICE_INTERFACE = "android.media.browse.MediaBrowserService";
    133 
    134     /**
    135      * A key for passing the MediaItem to the ResultReceiver in getItem.
    136      *
    137      * @hide
    138      */
    139     @RestrictTo(LIBRARY)
    140     public static final String KEY_MEDIA_ITEM = "media_item";
    141 
    142     /**
    143      * A key for passing the list of MediaItems to the ResultReceiver in search.
    144      *
    145      * @hide
    146      */
    147     @RestrictTo(LIBRARY)
    148     public static final String KEY_SEARCH_RESULTS = "search_results";
    149 
    150     static final int RESULT_FLAG_OPTION_NOT_HANDLED = 1 << 0;
    151     static final int RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED = 1 << 1;
    152     static final int RESULT_FLAG_ON_SEARCH_NOT_IMPLEMENTED = 1 << 2;
    153 
    154     /**
    155      * @hide
    156      */
    157     @RestrictTo(LIBRARY)
    158     public static final int RESULT_ERROR = -1;
    159     /**
    160      * @hide
    161      */
    162     @RestrictTo(LIBRARY)
    163     public static final int RESULT_OK = 0;
    164 
    165     /**
    166      * @hide
    167      */
    168     @RestrictTo(LIBRARY)
    169     public static final int RESULT_PROGRESS_UPDATE = 1;
    170 
    171     /** @hide */
    172     @RestrictTo(LIBRARY)
    173     @Retention(RetentionPolicy.SOURCE)
    174     @IntDef(flag = true, value = {RESULT_FLAG_OPTION_NOT_HANDLED,
    175             RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED, RESULT_FLAG_ON_SEARCH_NOT_IMPLEMENTED})
    176     private @interface ResultFlags {
    177     }
    178 
    179     final ArrayMap<IBinder, ConnectionRecord> mConnections = new ArrayMap<>();
    180     ConnectionRecord mCurConnection;
    181     final ServiceHandler mHandler = new ServiceHandler();
    182     MediaSessionCompat.Token mSession;
    183 
    184     interface MediaBrowserServiceImpl {
    185         void onCreate();
    186         IBinder onBind(Intent intent);
    187         void setSessionToken(MediaSessionCompat.Token token);
    188         void notifyChildrenChanged(final String parentId, final Bundle options);
    189         Bundle getBrowserRootHints();
    190         RemoteUserInfo getCurrentBrowserInfo();
    191         List<RemoteUserInfo> getSubscribingBrowsers(String parentId);
    192     }
    193 
    194     class MediaBrowserServiceImplBase implements MediaBrowserServiceImpl {
    195         private Messenger mMessenger;
    196 
    197         @Override
    198         public void onCreate() {
    199             mMessenger = new Messenger(mHandler);
    200         }
    201 
    202         @Override
    203         public IBinder onBind(Intent intent) {
    204             if (SERVICE_INTERFACE.equals(intent.getAction())) {
    205                 return mMessenger.getBinder();
    206             }
    207             return null;
    208         }
    209 
    210         @Override
    211         public void setSessionToken(final MediaSessionCompat.Token token) {
    212             mHandler.post(new Runnable() {
    213                 @Override
    214                 public void run() {
    215                     Iterator<ConnectionRecord> iter = mConnections.values().iterator();
    216                     while (iter.hasNext()) {
    217                         ConnectionRecord connection = iter.next();
    218                         try {
    219                             connection.callbacks.onConnect(connection.root.getRootId(), token,
    220                                     connection.root.getExtras());
    221                         } catch (RemoteException e) {
    222                             Log.w(TAG, "Connection for " + connection.pkg + " is no longer valid.");
    223                             iter.remove();
    224                         }
    225                     }
    226                 }
    227             });
    228         }
    229 
    230         @Override
    231         public void notifyChildrenChanged(@NonNull final String parentId, final Bundle options) {
    232             mHandler.post(new Runnable() {
    233                 @Override
    234                 public void run() {
    235                     for (IBinder binder : mConnections.keySet()) {
    236                         ConnectionRecord connection = mConnections.get(binder);
    237                         List<Pair<IBinder, Bundle>> callbackList =
    238                                 connection.subscriptions.get(parentId);
    239                         if (callbackList != null) {
    240                             for (Pair<IBinder, Bundle> callback : callbackList) {
    241                                 if (MediaBrowserCompatUtils.hasDuplicatedItems(
    242                                         options, callback.second)) {
    243                                     performLoadChildren(parentId, connection, callback.second,
    244                                             options);
    245                                 }
    246                             }
    247                         }
    248                     }
    249                 }
    250             });
    251         }
    252 
    253         @Override
    254         public Bundle getBrowserRootHints() {
    255             if (mCurConnection == null) {
    256                 throw new IllegalStateException("This should be called inside of onLoadChildren,"
    257                         + " onLoadItem, onSearch, or onCustomAction methods");
    258             }
    259             return mCurConnection.rootHints == null ? null : new Bundle(mCurConnection.rootHints);
    260         }
    261 
    262         @Override
    263         public RemoteUserInfo getCurrentBrowserInfo() {
    264             if (mCurConnection == null) {
    265                 throw new IllegalStateException("This should be called inside of onLoadChildren,"
    266                         + " onLoadItem, onSearch, or onCustomAction methods");
    267             }
    268             return mCurConnection.browserInfo;
    269         }
    270 
    271         @Override
    272         public List<RemoteUserInfo> getSubscribingBrowsers(String parentId) {
    273             List<RemoteUserInfo> result = new ArrayList<>();
    274             for (IBinder binder : mConnections.keySet()) {
    275                 ConnectionRecord connection = mConnections.get(binder);
    276                 List<Pair<IBinder, Bundle>> callbackList =
    277                         connection.subscriptions.get(parentId);
    278                 if (callbackList != null) {
    279                     result.add(connection.browserInfo);
    280                 }
    281             }
    282             return result;
    283         }
    284     }
    285 
    286     @RequiresApi(21)
    287     class MediaBrowserServiceImplApi21 implements MediaBrowserServiceImpl,
    288             MediaBrowserServiceCompatApi21.ServiceCompatProxy {
    289         final List<Bundle> mRootExtrasList = new ArrayList<>();
    290         Object mServiceObj;
    291         Messenger mMessenger;
    292 
    293         @Override
    294         public void onCreate() {
    295             mServiceObj = MediaBrowserServiceCompatApi21.createService(
    296                     MediaBrowserServiceCompat.this, this);
    297             MediaBrowserServiceCompatApi21.onCreate(mServiceObj);
    298         }
    299 
    300         @Override
    301         public IBinder onBind(Intent intent) {
    302             return MediaBrowserServiceCompatApi21.onBind(mServiceObj, intent);
    303         }
    304 
    305         @Override
    306         public void setSessionToken(final MediaSessionCompat.Token token) {
    307             mHandler.postOrRun(new Runnable() {
    308                 @Override
    309                 public void run() {
    310                     if (!mRootExtrasList.isEmpty()) {
    311                         IMediaSession extraBinder = token.getExtraBinder();
    312                         if (extraBinder != null) {
    313                             for (Bundle rootExtras : mRootExtrasList) {
    314                                 BundleCompat.putBinder(rootExtras, EXTRA_SESSION_BINDER,
    315                                         extraBinder.asBinder());
    316                             }
    317                         }
    318                         mRootExtrasList.clear();
    319                     }
    320                     MediaBrowserServiceCompatApi21.setSessionToken(mServiceObj, token.getToken());
    321                 }
    322             });
    323         }
    324 
    325         @Override
    326         public void notifyChildrenChanged(final String parentId, final Bundle options) {
    327             notifyChildrenChangedForFramework(parentId, options);
    328             notifyChildrenChangedForCompat(parentId, options);
    329         }
    330 
    331         @Override
    332         public MediaBrowserServiceCompatApi21.BrowserRoot onGetRoot(
    333                 String clientPackageName, int clientUid, Bundle rootHints) {
    334             Bundle rootExtras = null;
    335             if (rootHints != null && rootHints.getInt(EXTRA_CLIENT_VERSION, 0) != 0) {
    336                 rootHints.remove(EXTRA_CLIENT_VERSION);
    337                 mMessenger = new Messenger(mHandler);
    338                 rootExtras = new Bundle();
    339                 rootExtras.putInt(EXTRA_SERVICE_VERSION, SERVICE_VERSION_CURRENT);
    340                 BundleCompat.putBinder(rootExtras, EXTRA_MESSENGER_BINDER, mMessenger.getBinder());
    341                 if (mSession != null) {
    342                     IMediaSession extraBinder = mSession.getExtraBinder();
    343                     BundleCompat.putBinder(rootExtras, EXTRA_SESSION_BINDER,
    344                             extraBinder == null ? null : extraBinder.asBinder());
    345                 } else {
    346                     mRootExtrasList.add(rootExtras);
    347                 }
    348             }
    349             // We aren't sure whether this connection request would be accepted.
    350             // Temporarily set mCurConnection just to make getCurrentBrowserInfo() working.
    351             mCurConnection = new ConnectionRecord(clientPackageName, -1, clientUid, rootHints,
    352                     null);
    353             BrowserRoot root = MediaBrowserServiceCompat.this.onGetRoot(
    354                     clientPackageName, clientUid, rootHints);
    355             mCurConnection = null;
    356             if (root == null) {
    357                 return null;
    358             }
    359             if (rootExtras == null) {
    360                 rootExtras = root.getExtras();
    361             } else if (root.getExtras() != null) {
    362                 rootExtras.putAll(root.getExtras());
    363             }
    364             return new MediaBrowserServiceCompatApi21.BrowserRoot(
    365                     root.getRootId(), rootExtras);
    366         }
    367 
    368         @Override
    369         public void onLoadChildren(String parentId,
    370                 final MediaBrowserServiceCompatApi21.ResultWrapper<List<Parcel>> resultWrapper) {
    371             final Result<List<MediaBrowserCompat.MediaItem>> result
    372                     = new Result<List<MediaBrowserCompat.MediaItem>>(parentId) {
    373                 @Override
    374                 void onResultSent(List<MediaBrowserCompat.MediaItem> list) {
    375                     List<Parcel> parcelList = null;
    376                     if (list != null) {
    377                         parcelList = new ArrayList<>();
    378                         for (MediaBrowserCompat.MediaItem item : list) {
    379                             Parcel parcel = Parcel.obtain();
    380                             item.writeToParcel(parcel, 0);
    381                             parcelList.add(parcel);
    382                         }
    383                     }
    384                     resultWrapper.sendResult(parcelList);
    385                 }
    386 
    387                 @Override
    388                 public void detach() {
    389                     resultWrapper.detach();
    390                 }
    391             };
    392             MediaBrowserServiceCompat.this.onLoadChildren(parentId, result);
    393         }
    394 
    395         @Override
    396         public List<RemoteUserInfo> getSubscribingBrowsers(String parentId) {
    397             List<RemoteUserInfo> result = new ArrayList<>();
    398             for (IBinder binder : mConnections.keySet()) {
    399                 ConnectionRecord connection = mConnections.get(binder);
    400                 List<Pair<IBinder, Bundle>> callbackList =
    401                         connection.subscriptions.get(parentId);
    402                 if (callbackList != null) {
    403                     result.add(connection.browserInfo);
    404                 }
    405             }
    406             return result;
    407         }
    408 
    409         void notifyChildrenChangedForFramework(final String parentId, final Bundle options) {
    410             MediaBrowserServiceCompatApi21.notifyChildrenChanged(mServiceObj, parentId);
    411         }
    412 
    413         void notifyChildrenChangedForCompat(final String parentId, final Bundle options) {
    414             mHandler.post(new Runnable() {
    415                 @Override
    416                 public void run() {
    417                     for (IBinder binder : mConnections.keySet()) {
    418                         ConnectionRecord connection = mConnections.get(binder);
    419                         List<Pair<IBinder, Bundle>> callbackList =
    420                                 connection.subscriptions.get(parentId);
    421                         if (callbackList != null) {
    422                             for (Pair<IBinder, Bundle> callback : callbackList) {
    423                                 if (MediaBrowserCompatUtils.hasDuplicatedItems(
    424                                         options, callback.second)) {
    425                                     performLoadChildren(parentId, connection, callback.second,
    426                                             options);
    427                                 }
    428                             }
    429                         }
    430                     }
    431                 }
    432             });
    433         }
    434 
    435         @Override
    436         public Bundle getBrowserRootHints() {
    437             if (mMessenger == null) {
    438                 // TODO: Handle getBrowserRootHints when connected with framework MediaBrowser.
    439                 return null;
    440             }
    441             if (mCurConnection == null) {
    442                 throw new IllegalStateException("This should be called inside of onGetRoot,"
    443                         + " onLoadChildren, onLoadItem, onSearch, or onCustomAction methods");
    444             }
    445             return mCurConnection.rootHints == null ? null : new Bundle(mCurConnection.rootHints);
    446         }
    447 
    448         @Override
    449         public RemoteUserInfo getCurrentBrowserInfo() {
    450             if (mCurConnection == null) {
    451                 throw new IllegalStateException("This should be called inside of onGetRoot,"
    452                         + " onLoadChildren, onLoadItem, onSearch, or onCustomAction methods");
    453             }
    454             return mCurConnection.browserInfo;
    455         }
    456     }
    457 
    458     @RequiresApi(23)
    459     class MediaBrowserServiceImplApi23 extends MediaBrowserServiceImplApi21 implements
    460             MediaBrowserServiceCompatApi23.ServiceCompatProxy {
    461         @Override
    462         public void onCreate() {
    463             mServiceObj = MediaBrowserServiceCompatApi23.createService(
    464                     MediaBrowserServiceCompat.this, this);
    465             MediaBrowserServiceCompatApi21.onCreate(mServiceObj);
    466         }
    467 
    468         @Override
    469         public void onLoadItem(String itemId,
    470                 final MediaBrowserServiceCompatApi21.ResultWrapper<Parcel> resultWrapper) {
    471             final Result<MediaBrowserCompat.MediaItem> result
    472                     = new Result<MediaBrowserCompat.MediaItem>(itemId) {
    473                 @Override
    474                 void onResultSent(MediaBrowserCompat.MediaItem item) {
    475                     if (item == null) {
    476                         resultWrapper.sendResult(null);
    477                     } else {
    478                         Parcel parcelItem = Parcel.obtain();
    479                         item.writeToParcel(parcelItem, 0);
    480                         resultWrapper.sendResult(parcelItem);
    481                     }
    482                 }
    483 
    484                 @Override
    485                 public void detach() {
    486                     resultWrapper.detach();
    487                 }
    488             };
    489             MediaBrowserServiceCompat.this.onLoadItem(itemId, result);
    490         }
    491     }
    492 
    493     @RequiresApi(26)
    494     class MediaBrowserServiceImplApi26 extends MediaBrowserServiceImplApi23 implements
    495             MediaBrowserServiceCompatApi26.ServiceCompatProxy {
    496         @Override
    497         public void onCreate() {
    498             mServiceObj = MediaBrowserServiceCompatApi26.createService(
    499                     MediaBrowserServiceCompat.this, this);
    500             MediaBrowserServiceCompatApi21.onCreate(mServiceObj);
    501         }
    502 
    503         @Override
    504         public void onLoadChildren(String parentId,
    505                 final MediaBrowserServiceCompatApi26.ResultWrapper resultWrapper, Bundle options) {
    506             final Result<List<MediaBrowserCompat.MediaItem>> result
    507                     = new Result<List<MediaBrowserCompat.MediaItem>>(parentId) {
    508                 @Override
    509                 void onResultSent(List<MediaBrowserCompat.MediaItem> list) {
    510                     List<Parcel> parcelList = null;
    511                     if (list != null) {
    512                         parcelList = new ArrayList<>();
    513                         for (MediaBrowserCompat.MediaItem item : list) {
    514                             Parcel parcel = Parcel.obtain();
    515                             item.writeToParcel(parcel, 0);
    516                             parcelList.add(parcel);
    517                         }
    518                     }
    519                     resultWrapper.sendResult(parcelList, getFlags());
    520                 }
    521 
    522                 @Override
    523                 public void detach() {
    524                     resultWrapper.detach();
    525                 }
    526             };
    527             MediaBrowserServiceCompat.this.onLoadChildren(parentId, result, options);
    528         }
    529 
    530         @Override
    531         public Bundle getBrowserRootHints() {
    532             // mCurConnection is not null when EXTRA_MESSENGER_BINDER is used.
    533             if (mCurConnection != null) {
    534                 return mCurConnection.rootHints == null ? null
    535                         : new Bundle(mCurConnection.rootHints);
    536             }
    537             return MediaBrowserServiceCompatApi26.getBrowserRootHints(mServiceObj);
    538         }
    539 
    540         @Override
    541         void notifyChildrenChangedForFramework(final String parentId, final Bundle options) {
    542             if (options != null) {
    543                 MediaBrowserServiceCompatApi26.notifyChildrenChanged(mServiceObj, parentId,
    544                         options);
    545             } else {
    546                 super.notifyChildrenChangedForFramework(parentId, options);
    547             }
    548         }
    549     }
    550 
    551     @RequiresApi(28)
    552     class MediaBrowserServiceImplApi28 extends MediaBrowserServiceImplApi26 {
    553         @Override
    554         public RemoteUserInfo getCurrentBrowserInfo() {
    555             // mCurConnection is not null when EXTRA_MESSENGER_BINDER is used.
    556             if (mCurConnection != null) {
    557                 return mCurConnection.browserInfo;
    558             }
    559             android.media.session.MediaSessionManager.RemoteUserInfo userInfoObj =
    560                     ((MediaBrowserService) mServiceObj).getCurrentBrowserInfo();
    561             return new RemoteUserInfo(
    562                     userInfoObj.getPackageName(), userInfoObj.getPid(), userInfoObj.getUid());
    563         }
    564     }
    565 
    566     private final class ServiceHandler extends Handler {
    567         private final ServiceBinderImpl mServiceBinderImpl = new ServiceBinderImpl();
    568 
    569         ServiceHandler() {
    570         }
    571 
    572         @Override
    573         public void handleMessage(Message msg) {
    574             Bundle data = msg.getData();
    575             switch (msg.what) {
    576                 case CLIENT_MSG_CONNECT:
    577                     mServiceBinderImpl.connect(data.getString(DATA_PACKAGE_NAME),
    578                             data.getInt(DATA_CALLING_PID), data.getInt(DATA_CALLING_UID),
    579                             data.getBundle(DATA_ROOT_HINTS),
    580                             new ServiceCallbacksCompat(msg.replyTo));
    581                     break;
    582                 case CLIENT_MSG_DISCONNECT:
    583                     mServiceBinderImpl.disconnect(new ServiceCallbacksCompat(msg.replyTo));
    584                     break;
    585                 case CLIENT_MSG_ADD_SUBSCRIPTION:
    586                     mServiceBinderImpl.addSubscription(data.getString(DATA_MEDIA_ITEM_ID),
    587                             BundleCompat.getBinder(data, DATA_CALLBACK_TOKEN),
    588                             data.getBundle(DATA_OPTIONS),
    589                             new ServiceCallbacksCompat(msg.replyTo));
    590                     break;
    591                 case CLIENT_MSG_REMOVE_SUBSCRIPTION:
    592                     mServiceBinderImpl.removeSubscription(data.getString(DATA_MEDIA_ITEM_ID),
    593                             BundleCompat.getBinder(data, DATA_CALLBACK_TOKEN),
    594                             new ServiceCallbacksCompat(msg.replyTo));
    595                     break;
    596                 case CLIENT_MSG_GET_MEDIA_ITEM:
    597                     mServiceBinderImpl.getMediaItem(data.getString(DATA_MEDIA_ITEM_ID),
    598                             (ResultReceiver) data.getParcelable(DATA_RESULT_RECEIVER),
    599                             new ServiceCallbacksCompat(msg.replyTo));
    600                     break;
    601                 case CLIENT_MSG_REGISTER_CALLBACK_MESSENGER:
    602                     mServiceBinderImpl.registerCallbacks(new ServiceCallbacksCompat(msg.replyTo),
    603                             data.getString(DATA_PACKAGE_NAME), data.getInt(DATA_CALLING_PID),
    604                             data.getInt(DATA_CALLING_UID), data.getBundle(DATA_ROOT_HINTS));
    605                     break;
    606                 case CLIENT_MSG_UNREGISTER_CALLBACK_MESSENGER:
    607                     mServiceBinderImpl.unregisterCallbacks(new ServiceCallbacksCompat(msg.replyTo));
    608                     break;
    609                 case CLIENT_MSG_SEARCH:
    610                     mServiceBinderImpl.search(data.getString(DATA_SEARCH_QUERY),
    611                             data.getBundle(DATA_SEARCH_EXTRAS),
    612                             (ResultReceiver) data.getParcelable(DATA_RESULT_RECEIVER),
    613                             new ServiceCallbacksCompat(msg.replyTo));
    614                     break;
    615                 case CLIENT_MSG_SEND_CUSTOM_ACTION:
    616                     mServiceBinderImpl.sendCustomAction(data.getString(DATA_CUSTOM_ACTION),
    617                             data.getBundle(DATA_CUSTOM_ACTION_EXTRAS),
    618                             (ResultReceiver) data.getParcelable(DATA_RESULT_RECEIVER),
    619                             new ServiceCallbacksCompat(msg.replyTo));
    620                     break;
    621                 default:
    622                     Log.w(TAG, "Unhandled message: " + msg
    623                             + "\n  Service version: " + SERVICE_VERSION_CURRENT
    624                             + "\n  Client version: " + msg.arg1);
    625             }
    626         }
    627 
    628         @Override
    629         public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
    630             // Binder.getCallingUid() in handleMessage will return the uid of this process.
    631             // In order to get the right calling uid, Binder.getCallingUid() should be called here.
    632             Bundle data = msg.getData();
    633             data.setClassLoader(MediaBrowserCompat.class.getClassLoader());
    634             data.putInt(DATA_CALLING_UID, Binder.getCallingUid());
    635             data.putInt(DATA_CALLING_PID, Binder.getCallingPid());
    636             return super.sendMessageAtTime(msg, uptimeMillis);
    637         }
    638 
    639         public void postOrRun(Runnable r) {
    640             if (Thread.currentThread() == getLooper().getThread()) {
    641                 r.run();
    642             } else {
    643                 post(r);
    644             }
    645         }
    646     }
    647 
    648     /**
    649      * All the info about a connection.
    650      */
    651     private class ConnectionRecord implements IBinder.DeathRecipient {
    652         public final String pkg;
    653         public final int pid;
    654         public final int uid;
    655         public final RemoteUserInfo browserInfo;
    656         public final Bundle rootHints;
    657         public final ServiceCallbacks callbacks;
    658         public final HashMap<String, List<Pair<IBinder, Bundle>>> subscriptions = new HashMap<>();
    659         public BrowserRoot root;
    660 
    661         ConnectionRecord(String pkg, int pid, int uid, Bundle rootHints,
    662                 ServiceCallbacks callback) {
    663             this.pkg = pkg;
    664             this.pid = pid;
    665             this.uid = uid;
    666             this.browserInfo = new RemoteUserInfo(pkg, pid, uid);
    667             this.rootHints = rootHints;
    668             this.callbacks = callback;
    669         }
    670 
    671         @Override
    672         public void binderDied() {
    673             mHandler.post(new Runnable() {
    674                 @Override
    675                 public void run() {
    676                     mConnections.remove(callbacks.asBinder());
    677                 }
    678             });
    679         }
    680     }
    681 
    682     /**
    683      * Completion handler for asynchronous callback methods in {@link MediaBrowserServiceCompat}.
    684      * <p>
    685      * Each of the methods that takes one of these to send the result must call either
    686      * {@link #sendResult} or {@link #sendError} to respond to the caller with the given results or
    687      * errors. If those functions return without calling {@link #sendResult} or {@link #sendError},
    688      * they must instead call {@link #detach} before returning, and then may call
    689      * {@link #sendResult} or {@link #sendError} when they are done. If {@link #sendResult},
    690      * {@link #sendError}, or {@link #detach} is called twice, an exception will be thrown.
    691      * </p><p>
    692      * Those functions might also want to call {@link #sendProgressUpdate} to send interim updates
    693      * to the caller. If it is called after calling {@link #sendResult} or {@link #sendError}, an
    694      * exception will be thrown.
    695      * </p>
    696      *
    697      * @see MediaBrowserServiceCompat#onLoadChildren
    698      * @see MediaBrowserServiceCompat#onLoadItem
    699      * @see MediaBrowserServiceCompat#onSearch
    700      * @see MediaBrowserServiceCompat#onCustomAction
    701      */
    702     public static class Result<T> {
    703         private final Object mDebug;
    704         private boolean mDetachCalled;
    705         private boolean mSendResultCalled;
    706         private boolean mSendProgressUpdateCalled;
    707         private boolean mSendErrorCalled;
    708         private int mFlags;
    709 
    710         Result(Object debug) {
    711             mDebug = debug;
    712         }
    713 
    714         /**
    715          * Send the result back to the caller.
    716          */
    717         public void sendResult(T result) {
    718             if (mSendResultCalled || mSendErrorCalled) {
    719                 throw new IllegalStateException("sendResult() called when either sendResult() or "
    720                         + "sendError() had already been called for: " + mDebug);
    721             }
    722             mSendResultCalled = true;
    723             onResultSent(result);
    724         }
    725 
    726         /**
    727          * Send an interim update to the caller. This method is supported only when it is used in
    728          * {@link #onCustomAction}.
    729          *
    730          * @param extras A bundle that contains extra data.
    731          */
    732         public void sendProgressUpdate(Bundle extras) {
    733             if (mSendResultCalled || mSendErrorCalled) {
    734                 throw new IllegalStateException("sendProgressUpdate() called when either "
    735                         + "sendResult() or sendError() had already been called for: " + mDebug);
    736             }
    737             checkExtraFields(extras);
    738             mSendProgressUpdateCalled = true;
    739             onProgressUpdateSent(extras);
    740         }
    741 
    742         /**
    743          * Notify the caller of a failure. This is supported only when it is used in
    744          * {@link #onCustomAction}.
    745          *
    746          * @param extras A bundle that contains extra data.
    747          */
    748         public void sendError(Bundle extras) {
    749             if (mSendResultCalled || mSendErrorCalled) {
    750                 throw new IllegalStateException("sendError() called when either sendResult() or "
    751                         + "sendError() had already been called for: " + mDebug);
    752             }
    753             mSendErrorCalled = true;
    754             onErrorSent(extras);
    755         }
    756 
    757         /**
    758          * Detach this message from the current thread and allow the {@link #sendResult}
    759          * call to happen later.
    760          */
    761         public void detach() {
    762             if (mDetachCalled) {
    763                 throw new IllegalStateException("detach() called when detach() had already"
    764                         + " been called for: " + mDebug);
    765             }
    766             if (mSendResultCalled) {
    767                 throw new IllegalStateException("detach() called when sendResult() had already"
    768                         + " been called for: " + mDebug);
    769             }
    770             if (mSendErrorCalled) {
    771                 throw new IllegalStateException("detach() called when sendError() had already"
    772                         + " been called for: " + mDebug);
    773             }
    774             mDetachCalled = true;
    775         }
    776 
    777         boolean isDone() {
    778             return mDetachCalled || mSendResultCalled || mSendErrorCalled;
    779         }
    780 
    781         void setFlags(@ResultFlags int flags) {
    782             mFlags = flags;
    783         }
    784 
    785         int getFlags() {
    786             return mFlags;
    787         }
    788 
    789         /**
    790          * Called when the result is sent, after assertions about not being called twice have
    791          * happened.
    792          */
    793         void onResultSent(T result) {
    794         }
    795 
    796         /**
    797          * Called when an interim update is sent.
    798          */
    799         void onProgressUpdateSent(Bundle extras) {
    800             throw new UnsupportedOperationException("It is not supported to send an interim update "
    801                     + "for " + mDebug);
    802         }
    803 
    804         /**
    805          * Called when an error is sent, after assertions about not being called twice have
    806          * happened.
    807          */
    808         void onErrorSent(Bundle extras) {
    809             throw new UnsupportedOperationException("It is not supported to send an error for "
    810                     + mDebug);
    811         }
    812 
    813         private void checkExtraFields(Bundle extras) {
    814             if (extras == null) {
    815                 return;
    816             }
    817             if (extras.containsKey(MediaBrowserCompat.EXTRA_DOWNLOAD_PROGRESS)) {
    818                 float value = extras.getFloat(MediaBrowserCompat.EXTRA_DOWNLOAD_PROGRESS);
    819                 if (value < -EPSILON || value > 1.0f + EPSILON) {
    820                     throw new IllegalArgumentException("The value of the EXTRA_DOWNLOAD_PROGRESS "
    821                             + "field must be a float number within [0.0, 1.0].");
    822                 }
    823             }
    824         }
    825     }
    826 
    827     private class ServiceBinderImpl {
    828         ServiceBinderImpl() {
    829         }
    830 
    831         public void connect(final String pkg, final int pid, final int uid, final Bundle rootHints,
    832                 final ServiceCallbacks callbacks) {
    833 
    834             if (!isValidPackage(pkg, uid)) {
    835                 throw new IllegalArgumentException("Package/uid mismatch: uid=" + uid
    836                         + " package=" + pkg);
    837             }
    838 
    839             mHandler.postOrRun(new Runnable() {
    840                 @Override
    841                 public void run() {
    842                     final IBinder b = callbacks.asBinder();
    843 
    844                     // Clear out the old subscriptions. We are getting new ones.
    845                     mConnections.remove(b);
    846 
    847                     final ConnectionRecord connection = new ConnectionRecord(pkg, pid, uid,
    848                             rootHints, callbacks);
    849                     mCurConnection = connection;
    850                     connection.root = MediaBrowserServiceCompat.this.onGetRoot(pkg, uid, rootHints);
    851                     mCurConnection = null;
    852 
    853                     // If they didn't return something, don't allow this client.
    854                     if (connection.root == null) {
    855                         Log.i(TAG, "No root for client " + pkg + " from service "
    856                                 + getClass().getName());
    857                         try {
    858                             callbacks.onConnectFailed();
    859                         } catch (RemoteException ex) {
    860                             Log.w(TAG, "Calling onConnectFailed() failed. Ignoring. "
    861                                     + "pkg=" + pkg);
    862                         }
    863                     } else {
    864                         try {
    865                             mConnections.put(b, connection);
    866                             b.linkToDeath(connection, 0);
    867                             if (mSession != null) {
    868                                 callbacks.onConnect(connection.root.getRootId(),
    869                                         mSession, connection.root.getExtras());
    870                             }
    871                         } catch (RemoteException ex) {
    872                             Log.w(TAG, "Calling onConnect() failed. Dropping client. "
    873                                     + "pkg=" + pkg);
    874                             mConnections.remove(b);
    875                         }
    876                     }
    877                 }
    878             });
    879         }
    880 
    881         public void disconnect(final ServiceCallbacks callbacks) {
    882             mHandler.postOrRun(new Runnable() {
    883                 @Override
    884                 public void run() {
    885                     final IBinder b = callbacks.asBinder();
    886 
    887                     // Clear out the old subscriptions. We are getting new ones.
    888                     final ConnectionRecord old = mConnections.remove(b);
    889                     if (old != null) {
    890                         // TODO
    891                         old.callbacks.asBinder().unlinkToDeath(old, 0);
    892                     }
    893                 }
    894             });
    895         }
    896 
    897         public void addSubscription(final String id, final IBinder token, final Bundle options,
    898                 final ServiceCallbacks callbacks) {
    899             mHandler.postOrRun(new Runnable() {
    900                 @Override
    901                 public void run() {
    902                     final IBinder b = callbacks.asBinder();
    903 
    904                     // Get the record for the connection
    905                     final ConnectionRecord connection = mConnections.get(b);
    906                     if (connection == null) {
    907                         Log.w(TAG, "addSubscription for callback that isn't registered id="
    908                                 + id);
    909                         return;
    910                     }
    911 
    912                     MediaBrowserServiceCompat.this.addSubscription(id, connection, token, options);
    913                 }
    914             });
    915         }
    916 
    917         public void removeSubscription(final String id, final IBinder token,
    918                 final ServiceCallbacks callbacks) {
    919             mHandler.postOrRun(new Runnable() {
    920                 @Override
    921                 public void run() {
    922                     final IBinder b = callbacks.asBinder();
    923 
    924                     ConnectionRecord connection = mConnections.get(b);
    925                     if (connection == null) {
    926                         Log.w(TAG, "removeSubscription for callback that isn't registered id="
    927                                 + id);
    928                         return;
    929                     }
    930                     if (!MediaBrowserServiceCompat.this.removeSubscription(
    931                             id, connection, token)) {
    932                         Log.w(TAG, "removeSubscription called for " + id
    933                                 + " which is not subscribed");
    934                     }
    935                 }
    936             });
    937         }
    938 
    939         public void getMediaItem(final String mediaId, final ResultReceiver receiver,
    940                 final ServiceCallbacks callbacks) {
    941             if (TextUtils.isEmpty(mediaId) || receiver == null) {
    942                 return;
    943             }
    944 
    945             mHandler.postOrRun(new Runnable() {
    946                 @Override
    947                 public void run() {
    948                     final IBinder b = callbacks.asBinder();
    949 
    950                     ConnectionRecord connection = mConnections.get(b);
    951                     if (connection == null) {
    952                         Log.w(TAG, "getMediaItem for callback that isn't registered id=" + mediaId);
    953                         return;
    954                     }
    955                     performLoadItem(mediaId, connection, receiver);
    956                 }
    957             });
    958         }
    959 
    960         // Used when {@link MediaBrowserProtocol#EXTRA_MESSENGER_BINDER} is used.
    961         public void registerCallbacks(final ServiceCallbacks callbacks, final String pkg,
    962                 final int pid, final int uid, final Bundle rootHints) {
    963             mHandler.postOrRun(new Runnable() {
    964                 @Override
    965                 public void run() {
    966                     final IBinder b = callbacks.asBinder();
    967                     // Clear out the old subscriptions. We are getting new ones.
    968                     mConnections.remove(b);
    969 
    970                     final ConnectionRecord connection = new ConnectionRecord(pkg, pid, uid,
    971                             rootHints, callbacks);
    972                     mConnections.put(b, connection);
    973                     try {
    974                         b.linkToDeath(connection, 0);
    975                     } catch (RemoteException e) {
    976                         Log.w(TAG, "IBinder is already dead.");
    977                     }
    978                 }
    979             });
    980         }
    981 
    982         // Used when {@link MediaBrowserProtocol#EXTRA_MESSENGER_BINDER} is used.
    983         public void unregisterCallbacks(final ServiceCallbacks callbacks) {
    984             mHandler.postOrRun(new Runnable() {
    985                 @Override
    986                 public void run() {
    987                     final IBinder b = callbacks.asBinder();
    988                     ConnectionRecord old = mConnections.remove(b);
    989                     if (old != null) {
    990                         b.unlinkToDeath(old, 0);
    991                     }
    992                 }
    993             });
    994         }
    995 
    996         public void search(final String query, final Bundle extras, final ResultReceiver receiver,
    997                 final ServiceCallbacks callbacks) {
    998             if (TextUtils.isEmpty(query) || receiver == null) {
    999                 return;
   1000             }
   1001 
   1002             mHandler.postOrRun(new Runnable() {
   1003                 @Override
   1004                 public void run() {
   1005                     final IBinder b = callbacks.asBinder();
   1006 
   1007                     ConnectionRecord connection = mConnections.get(b);
   1008                     if (connection == null) {
   1009                         Log.w(TAG, "search for callback that isn't registered query=" + query);
   1010                         return;
   1011                     }
   1012                     performSearch(query, extras, connection, receiver);
   1013                 }
   1014             });
   1015         }
   1016 
   1017         public void sendCustomAction(final String action, final Bundle extras,
   1018                 final ResultReceiver receiver, final ServiceCallbacks callbacks) {
   1019             if (TextUtils.isEmpty(action) || receiver == null) {
   1020                 return;
   1021             }
   1022 
   1023             mHandler.postOrRun(new Runnable() {
   1024                 @Override
   1025                 public void run() {
   1026                     final IBinder b = callbacks.asBinder();
   1027 
   1028                     ConnectionRecord connection = mConnections.get(b);
   1029                     if (connection == null) {
   1030                         Log.w(TAG, "sendCustomAction for callback that isn't registered action="
   1031                                 + action + ", extras=" + extras);
   1032                         return;
   1033                     }
   1034                     performCustomAction(action, extras, connection, receiver);
   1035                 }
   1036             });
   1037         }
   1038     }
   1039 
   1040     private interface ServiceCallbacks {
   1041         IBinder asBinder();
   1042         void onConnect(String root, MediaSessionCompat.Token session, Bundle extras)
   1043                 throws RemoteException;
   1044         void onConnectFailed() throws RemoteException;
   1045         void onLoadChildren(String mediaId, List<MediaBrowserCompat.MediaItem> list, Bundle options,
   1046                 Bundle notifyChildrenChangedOptions) throws RemoteException;
   1047     }
   1048 
   1049     private static class ServiceCallbacksCompat implements ServiceCallbacks {
   1050         final Messenger mCallbacks;
   1051 
   1052         ServiceCallbacksCompat(Messenger callbacks) {
   1053             mCallbacks = callbacks;
   1054         }
   1055 
   1056         @Override
   1057         public IBinder asBinder() {
   1058             return mCallbacks.getBinder();
   1059         }
   1060 
   1061         @Override
   1062         public void onConnect(String root, MediaSessionCompat.Token session, Bundle extras)
   1063                 throws RemoteException {
   1064             if (extras == null) {
   1065                 extras = new Bundle();
   1066             }
   1067             extras.putInt(EXTRA_SERVICE_VERSION, SERVICE_VERSION_CURRENT);
   1068             Bundle data = new Bundle();
   1069             data.putString(DATA_MEDIA_ITEM_ID, root);
   1070             data.putParcelable(DATA_MEDIA_SESSION_TOKEN, session);
   1071             data.putBundle(DATA_ROOT_HINTS, extras);
   1072             sendRequest(SERVICE_MSG_ON_CONNECT, data);
   1073         }
   1074 
   1075         @Override
   1076         public void onConnectFailed() throws RemoteException {
   1077             sendRequest(SERVICE_MSG_ON_CONNECT_FAILED, null);
   1078         }
   1079 
   1080         @Override
   1081         public void onLoadChildren(String mediaId, List<MediaBrowserCompat.MediaItem> list,
   1082                 Bundle options, Bundle notifyChildrenChangedOptions) throws RemoteException {
   1083             Bundle data = new Bundle();
   1084             data.putString(DATA_MEDIA_ITEM_ID, mediaId);
   1085             data.putBundle(DATA_OPTIONS, options);
   1086             data.putBundle(DATA_NOTIFY_CHILDREN_CHANGED_OPTIONS, notifyChildrenChangedOptions);
   1087             if (list != null) {
   1088                 data.putParcelableArrayList(DATA_MEDIA_ITEM_LIST,
   1089                         list instanceof ArrayList ? (ArrayList) list : new ArrayList<>(list));
   1090             }
   1091             sendRequest(SERVICE_MSG_ON_LOAD_CHILDREN, data);
   1092         }
   1093 
   1094         private void sendRequest(int what, Bundle data) throws RemoteException {
   1095             Message msg = Message.obtain();
   1096             msg.what = what;
   1097             msg.arg1 = SERVICE_VERSION_CURRENT;
   1098             msg.setData(data);
   1099             mCallbacks.send(msg);
   1100         }
   1101     }
   1102 
   1103     /**
   1104      * Attaches to the base context. This method is added to change the visibility of
   1105      * {@link Service#attachBaseContext(Context)}.
   1106      * <p>
   1107      * Note that we cannot simply override {@link Service#attachBaseContext(Context)} and hide it
   1108      * because lint checks considers the overriden method as the new public API that needs update
   1109      * of current.txt.
   1110      *
   1111      * @hide
   1112      */
   1113     @RestrictTo(LIBRARY)
   1114     public void attachToBaseContext(Context base) {
   1115         attachBaseContext(base);
   1116     }
   1117 
   1118     @Override
   1119     public void onCreate() {
   1120         super.onCreate();
   1121         if (Build.VERSION.SDK_INT >= 28) {
   1122             mImpl = new MediaBrowserServiceImplApi28();
   1123         } else if (Build.VERSION.SDK_INT >= 26) {
   1124             mImpl = new MediaBrowserServiceImplApi26();
   1125         } else if (Build.VERSION.SDK_INT >= 23) {
   1126             mImpl = new MediaBrowserServiceImplApi23();
   1127         } else if (Build.VERSION.SDK_INT >= 21) {
   1128             mImpl = new MediaBrowserServiceImplApi21();
   1129         } else {
   1130             mImpl = new MediaBrowserServiceImplBase();
   1131         }
   1132         mImpl.onCreate();
   1133     }
   1134 
   1135     @Override
   1136     public IBinder onBind(Intent intent) {
   1137         return mImpl.onBind(intent);
   1138     }
   1139 
   1140     @Override
   1141     public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
   1142     }
   1143 
   1144     /**
   1145      * Called to get the root information for browsing by a particular client.
   1146      * <p>
   1147      * The implementation should verify that the client package has permission
   1148      * to access browse media information before returning the root id; it
   1149      * should return null if the client is not allowed to access this
   1150      * information.
   1151      * </p>
   1152      *
   1153      * @param clientPackageName The package name of the application which is
   1154      *            requesting access to browse media.
   1155      * @param clientUid The uid of the application which is requesting access to
   1156      *            browse media.
   1157      * @param rootHints An optional bundle of service-specific arguments to send
   1158      *            to the media browse service when connecting and retrieving the
   1159      *            root id for browsing, or null if none. The contents of this
   1160      *            bundle may affect the information returned when browsing.
   1161      * @return The {@link BrowserRoot} for accessing this app's content or null.
   1162      * @see BrowserRoot#EXTRA_RECENT
   1163      * @see BrowserRoot#EXTRA_OFFLINE
   1164      * @see BrowserRoot#EXTRA_SUGGESTED
   1165      */
   1166     public abstract @Nullable BrowserRoot onGetRoot(@NonNull String clientPackageName,
   1167             int clientUid, @Nullable Bundle rootHints);
   1168 
   1169     /**
   1170      * Called to get information about the children of a media item.
   1171      * <p>
   1172      * Implementations must call {@link Result#sendResult result.sendResult}
   1173      * with the list of children. If loading the children will be an expensive
   1174      * operation that should be performed on another thread,
   1175      * {@link Result#detach result.detach} may be called before returning from
   1176      * this function, and then {@link Result#sendResult result.sendResult}
   1177      * called when the loading is complete.
   1178      * </p><p>
   1179      * In case the media item does not have any children, call {@link Result#sendResult}
   1180      * with an empty list. When the given {@code parentId} is invalid, implementations must
   1181      * call {@link Result#sendResult result.sendResult} with {@code null}, which will invoke
   1182      * {@link MediaBrowserCompat.SubscriptionCallback#onError}.
   1183      * </p>
   1184      *
   1185      * @param parentId The id of the parent media item whose children are to be
   1186      *            queried.
   1187      * @param result The Result to send the list of children to.
   1188      */
   1189     public abstract void onLoadChildren(@NonNull String parentId,
   1190             @NonNull Result<List<MediaBrowserCompat.MediaItem>> result);
   1191 
   1192     /**
   1193      * Called to get information about the children of a media item.
   1194      * <p>
   1195      * Implementations must call {@link Result#sendResult result.sendResult}
   1196      * with the list of children. If loading the children will be an expensive
   1197      * operation that should be performed on another thread,
   1198      * {@link Result#detach result.detach} may be called before returning from
   1199      * this function, and then {@link Result#sendResult result.sendResult}
   1200      * called when the loading is complete.
   1201      * </p><p>
   1202      * In case the media item does not have any children, call {@link Result#sendResult}
   1203      * with an empty list. When the given {@code parentId} is invalid, implementations must
   1204      * call {@link Result#sendResult result.sendResult} with {@code null}, which will invoke
   1205      * {@link MediaBrowserCompat.SubscriptionCallback#onError}.
   1206      * </p>
   1207      *
   1208      * @param parentId The id of the parent media item whose children are to be
   1209      *            queried.
   1210      * @param result The Result to send the list of children to.
   1211      * @param options A bundle of service-specific arguments sent from the media
   1212      *            browse. The information returned through the result should be
   1213      *            affected by the contents of this bundle.
   1214      */
   1215     public void onLoadChildren(@NonNull String parentId,
   1216             @NonNull Result<List<MediaBrowserCompat.MediaItem>> result, @NonNull Bundle options) {
   1217         // To support backward compatibility, when the implementation of MediaBrowserService doesn't
   1218         // override onLoadChildren() with options, onLoadChildren() without options will be used
   1219         // instead, and the options will be applied in the implementation of result.onResultSent().
   1220         result.setFlags(RESULT_FLAG_OPTION_NOT_HANDLED);
   1221         onLoadChildren(parentId, result);
   1222     }
   1223 
   1224     /**
   1225      * Called to get information about a specific media item.
   1226      * <p>
   1227      * Implementations must call {@link Result#sendResult result.sendResult}. If
   1228      * loading the item will be an expensive operation {@link Result#detach
   1229      * result.detach} may be called before returning from this function, and
   1230      * then {@link Result#sendResult result.sendResult} called when the item has
   1231      * been loaded.
   1232      * </p><p>
   1233      * When the given {@code itemId} is invalid, implementations must call
   1234      * {@link Result#sendResult result.sendResult} with {@code null}.
   1235      * </p><p>
   1236      * The default implementation will invoke {@link MediaBrowserCompat.ItemCallback#onError}.
   1237      *
   1238      * @param itemId The id for the specific {@link MediaBrowserCompat.MediaItem}.
   1239      * @param result The Result to send the item to, or null if the id is
   1240      *            invalid.
   1241      */
   1242     public void onLoadItem(String itemId, @NonNull Result<MediaBrowserCompat.MediaItem> result) {
   1243         result.setFlags(RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED);
   1244         result.sendResult(null);
   1245     }
   1246 
   1247     /**
   1248      * Called to get the search result.
   1249      * <p>
   1250      * Implementations must call {@link Result#sendResult result.sendResult}. If the search will be
   1251      * an expensive operation {@link Result#detach result.detach} may be called before returning
   1252      * from this function, and then {@link Result#sendResult result.sendResult} called when the
   1253      * search has been completed.
   1254      * </p><p>
   1255      * In case there are no search results, call {@link Result#sendResult result.sendResult} with an
   1256      * empty list. In case there are some errors happened, call {@link Result#sendResult
   1257      * result.sendResult} with {@code null}, which will invoke {@link
   1258      * MediaBrowserCompat.SearchCallback#onError}.
   1259      * </p><p>
   1260      * The default implementation will invoke {@link MediaBrowserCompat.SearchCallback#onError}.
   1261      * </p>
   1262      *
   1263      * @param query The search query sent from the media browser. It contains keywords separated
   1264      *            by space.
   1265      * @param extras The bundle of service-specific arguments sent from the media browser.
   1266      * @param result The {@link Result} to send the search result.
   1267      */
   1268     public void onSearch(@NonNull String query, Bundle extras,
   1269             @NonNull Result<List<MediaBrowserCompat.MediaItem>> result) {
   1270         result.setFlags(RESULT_FLAG_ON_SEARCH_NOT_IMPLEMENTED);
   1271         result.sendResult(null);
   1272     }
   1273 
   1274     /**
   1275      * Called to request a custom action to this service.
   1276      * <p>
   1277      * Implementations must call either {@link Result#sendResult} or {@link Result#sendError}. If
   1278      * the requested custom action will be an expensive operation {@link Result#detach} may be
   1279      * called before returning from this function, and then the service can send the result later
   1280      * when the custom action is completed. Implementation can also call
   1281      * {@link Result#sendProgressUpdate} to send an interim update to the requester.
   1282      * </p><p>
   1283      * If the requested custom action is not supported by this service, call
   1284      * {@link Result#sendError}. The default implementation will invoke {@link Result#sendError}.
   1285      * </p>
   1286      *
   1287      * @param action The custom action sent from the media browser.
   1288      * @param extras The bundle of service-specific arguments sent from the media browser.
   1289      * @param result The {@link Result} to send the result of the requested custom action.
   1290      * @see MediaBrowserCompat#CUSTOM_ACTION_DOWNLOAD
   1291      * @see MediaBrowserCompat#CUSTOM_ACTION_REMOVE_DOWNLOADED_FILE
   1292      */
   1293     public void onCustomAction(@NonNull String action, Bundle extras,
   1294             @NonNull Result<Bundle> result) {
   1295         result.sendError(null);
   1296     }
   1297 
   1298     /**
   1299      * Call to set the media session.
   1300      * <p>
   1301      * This should be called as soon as possible during the service's startup.
   1302      * It may only be called once.
   1303      *
   1304      * @param token The token for the service's {@link MediaSessionCompat}.
   1305      */
   1306     public void setSessionToken(MediaSessionCompat.Token token) {
   1307         if (token == null) {
   1308             throw new IllegalArgumentException("Session token may not be null.");
   1309         }
   1310         if (mSession != null) {
   1311             throw new IllegalStateException("The session token has already been set.");
   1312         }
   1313         mSession = token;
   1314         mImpl.setSessionToken(token);
   1315     }
   1316 
   1317     /**
   1318      * Gets the session token, or null if it has not yet been created
   1319      * or if it has been destroyed.
   1320      */
   1321     public @Nullable MediaSessionCompat.Token getSessionToken() {
   1322         return mSession;
   1323     }
   1324 
   1325     /**
   1326      * Gets the root hints sent from the currently connected {@link MediaBrowserCompat}.
   1327      * The root hints are service-specific arguments included in an optional bundle sent to the
   1328      * media browser service when connecting and retrieving the root id for browsing, or null if
   1329      * none. The contents of this bundle may affect the information returned when browsing.
   1330      * <p>
   1331      * Note that this will return null when connected to {@link android.media.browse.MediaBrowser}
   1332      * and running on API 23 or lower.
   1333      *
   1334      * @throws IllegalStateException If this method is called outside of {@link #onLoadChildren},
   1335      *             {@link #onLoadItem} or {@link #onSearch}.
   1336      * @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_RECENT
   1337      * @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_OFFLINE
   1338      * @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_SUGGESTED
   1339      */
   1340     public final Bundle getBrowserRootHints() {
   1341         return mImpl.getBrowserRootHints();
   1342     }
   1343 
   1344     /**
   1345      * Gets the browser information who sent the current request.
   1346      *
   1347      * @throws IllegalStateException If this method is called outside of {@link #onGetRoot} or
   1348      *             {@link #onLoadChildren} or {@link #onLoadItem}.
   1349      * @see MediaSessionManager#isTrustedForMediaControl(RemoteUserInfo)
   1350      */
   1351     public final @NonNull RemoteUserInfo getCurrentBrowserInfo() {
   1352         return mImpl.getCurrentBrowserInfo();
   1353     }
   1354 
   1355     /**
   1356      * Notifies all connected media browsers that the children of
   1357      * the specified parent id have changed in some way.
   1358      * This will cause browsers to fetch subscribed content again.
   1359      *
   1360      * @param parentId The id of the parent media item whose
   1361      * children changed.
   1362      */
   1363     public void notifyChildrenChanged(@NonNull String parentId) {
   1364         if (parentId == null) {
   1365             throw new IllegalArgumentException("parentId cannot be null in notifyChildrenChanged");
   1366         }
   1367         mImpl.notifyChildrenChanged(parentId, null);
   1368     }
   1369 
   1370     /**
   1371      * Notifies all connected media browsers that the children of
   1372      * the specified parent id have changed in some way.
   1373      * This will cause browsers to fetch subscribed content again.
   1374      *
   1375      * @param parentId The id of the parent media item whose
   1376      *            children changed.
   1377      * @param options A bundle of service-specific arguments to send
   1378      *            to the media browse. The contents of this bundle may
   1379      *            contain the information about the change.
   1380      */
   1381     public void notifyChildrenChanged(@NonNull String parentId, @NonNull Bundle options) {
   1382         if (parentId == null) {
   1383             throw new IllegalArgumentException("parentId cannot be null in notifyChildrenChanged");
   1384         }
   1385         if (options == null) {
   1386             throw new IllegalArgumentException("options cannot be null in notifyChildrenChanged");
   1387         }
   1388         mImpl.notifyChildrenChanged(parentId, options);
   1389     }
   1390 
   1391     /**
   1392      * Gets {@link RemoteUserInfo} of all browsers which are subscribing to the given parentId.
   1393      * @hide
   1394      */
   1395     @RestrictTo(LIBRARY)
   1396     public @NonNull List<RemoteUserInfo> getSubscribingBrowsers(@NonNull String parentId) {
   1397         if (parentId == null) {
   1398             throw new IllegalArgumentException("parentId cannot be null in getSubscribingBrowsers");
   1399         }
   1400         return mImpl.getSubscribingBrowsers(parentId);
   1401     }
   1402 
   1403     /**
   1404      * Return whether the given package is one of the ones that is owned by the uid.
   1405      */
   1406     boolean isValidPackage(String pkg, int uid) {
   1407         if (pkg == null) {
   1408             return false;
   1409         }
   1410         final PackageManager pm = getPackageManager();
   1411         final String[] packages = pm.getPackagesForUid(uid);
   1412         final int N = packages.length;
   1413         for (int i=0; i<N; i++) {
   1414             if (packages[i].equals(pkg)) {
   1415                 return true;
   1416             }
   1417         }
   1418         return false;
   1419     }
   1420 
   1421     /**
   1422      * Save the subscription and if it is a new subscription send the results.
   1423      */
   1424     void addSubscription(String id, ConnectionRecord connection, IBinder token,
   1425             Bundle options) {
   1426         // Save the subscription
   1427         List<Pair<IBinder, Bundle>> callbackList = connection.subscriptions.get(id);
   1428         if (callbackList == null) {
   1429             callbackList = new ArrayList<>();
   1430         }
   1431         for (Pair<IBinder, Bundle> callback : callbackList) {
   1432             if (token == callback.first
   1433                     && MediaBrowserCompatUtils.areSameOptions(options, callback.second)) {
   1434                 return;
   1435             }
   1436         }
   1437         callbackList.add(new Pair<>(token, options));
   1438         connection.subscriptions.put(id, callbackList);
   1439         // send the results
   1440         performLoadChildren(id, connection, options, null);
   1441     }
   1442 
   1443     /**
   1444      * Remove the subscription.
   1445      */
   1446     boolean removeSubscription(String id, ConnectionRecord connection, IBinder token) {
   1447         if (token == null) {
   1448             return connection.subscriptions.remove(id) != null;
   1449         }
   1450         boolean removed = false;
   1451         List<Pair<IBinder, Bundle>> callbackList = connection.subscriptions.get(id);
   1452         if (callbackList != null) {
   1453             Iterator<Pair<IBinder, Bundle>> iter = callbackList.iterator();
   1454             while (iter.hasNext()){
   1455                 if (token == iter.next().first) {
   1456                     removed = true;
   1457                     iter.remove();
   1458                 }
   1459             }
   1460             if (callbackList.size() == 0) {
   1461                 connection.subscriptions.remove(id);
   1462             }
   1463         }
   1464         return removed;
   1465     }
   1466 
   1467     /**
   1468      * Call onLoadChildren and then send the results back to the connection.
   1469      * <p>
   1470      * Callers must make sure that this connection is still connected.
   1471      */
   1472     void performLoadChildren(final String parentId, final ConnectionRecord connection,
   1473             final Bundle subscribeOptions, final Bundle notifyChildrenChangedOptions) {
   1474         final Result<List<MediaBrowserCompat.MediaItem>> result
   1475                 = new Result<List<MediaBrowserCompat.MediaItem>>(parentId) {
   1476             @Override
   1477             void onResultSent(List<MediaBrowserCompat.MediaItem> list) {
   1478                 if (mConnections.get(connection.callbacks.asBinder()) != connection) {
   1479                     if (DEBUG) {
   1480                         Log.d(TAG, "Not sending onLoadChildren result for connection that has"
   1481                                 + " been disconnected. pkg=" + connection.pkg + " id=" + parentId);
   1482                     }
   1483                     return;
   1484                 }
   1485 
   1486                 List<MediaBrowserCompat.MediaItem> filteredList =
   1487                         (getFlags() & RESULT_FLAG_OPTION_NOT_HANDLED) != 0
   1488                                 ? applyOptions(list, subscribeOptions) : list;
   1489                 try {
   1490                     connection.callbacks.onLoadChildren(parentId, filteredList, subscribeOptions,
   1491                             notifyChildrenChangedOptions);
   1492                 } catch (RemoteException ex) {
   1493                     // The other side is in the process of crashing.
   1494                     Log.w(TAG, "Calling onLoadChildren() failed for id=" + parentId
   1495                             + " package=" + connection.pkg);
   1496                 }
   1497             }
   1498         };
   1499 
   1500         mCurConnection = connection;
   1501         if (subscribeOptions == null) {
   1502             onLoadChildren(parentId, result);
   1503         } else {
   1504             onLoadChildren(parentId, result, subscribeOptions);
   1505         }
   1506         mCurConnection = null;
   1507 
   1508         if (!result.isDone()) {
   1509             throw new IllegalStateException("onLoadChildren must call detach() or sendResult()"
   1510                     + " before returning for package=" + connection.pkg + " id=" + parentId);
   1511         }
   1512     }
   1513 
   1514     List<MediaBrowserCompat.MediaItem> applyOptions(List<MediaBrowserCompat.MediaItem> list,
   1515             final Bundle options) {
   1516         if (list == null) {
   1517             return null;
   1518         }
   1519         int page = options.getInt(MediaBrowserCompat.EXTRA_PAGE, -1);
   1520         int pageSize = options.getInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, -1);
   1521         if (page == -1 && pageSize == -1) {
   1522             return list;
   1523         }
   1524         int fromIndex = pageSize * page;
   1525         int toIndex = fromIndex + pageSize;
   1526         if (page < 0 || pageSize < 1 || fromIndex >= list.size()) {
   1527             return Collections.EMPTY_LIST;
   1528         }
   1529         if (toIndex > list.size()) {
   1530             toIndex = list.size();
   1531         }
   1532         return list.subList(fromIndex, toIndex);
   1533     }
   1534 
   1535     void performLoadItem(String itemId, ConnectionRecord connection,
   1536             final ResultReceiver receiver) {
   1537         final Result<MediaBrowserCompat.MediaItem> result =
   1538                 new Result<MediaBrowserCompat.MediaItem>(itemId) {
   1539                     @Override
   1540                     void onResultSent(MediaBrowserCompat.MediaItem item) {
   1541                         if ((getFlags() & RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED) != 0) {
   1542                             receiver.send(RESULT_ERROR, null);
   1543                             return;
   1544                         }
   1545                         Bundle bundle = new Bundle();
   1546                         bundle.putParcelable(KEY_MEDIA_ITEM, item);
   1547                         receiver.send(RESULT_OK, bundle);
   1548                     }
   1549                 };
   1550 
   1551         mCurConnection = connection;
   1552         onLoadItem(itemId, result);
   1553         mCurConnection = null;
   1554 
   1555         if (!result.isDone()) {
   1556             throw new IllegalStateException("onLoadItem must call detach() or sendResult()"
   1557                     + " before returning for id=" + itemId);
   1558         }
   1559     }
   1560 
   1561     void performSearch(final String query, Bundle extras, ConnectionRecord connection,
   1562             final ResultReceiver receiver) {
   1563         final Result<List<MediaBrowserCompat.MediaItem>> result =
   1564                 new Result<List<MediaBrowserCompat.MediaItem>>(query) {
   1565             @Override
   1566             void onResultSent(List<MediaBrowserCompat.MediaItem> items) {
   1567                 if ((getFlags() & RESULT_FLAG_ON_SEARCH_NOT_IMPLEMENTED) != 0
   1568                         || items == null) {
   1569                     receiver.send(RESULT_ERROR, null);
   1570                     return;
   1571                 }
   1572                 Bundle bundle = new Bundle();
   1573                 bundle.putParcelableArray(KEY_SEARCH_RESULTS,
   1574                         items.toArray(new MediaBrowserCompat.MediaItem[0]));
   1575                 receiver.send(RESULT_OK, bundle);
   1576             }
   1577         };
   1578 
   1579         mCurConnection = connection;
   1580         onSearch(query, extras, result);
   1581         mCurConnection = null;
   1582 
   1583         if (!result.isDone()) {
   1584             throw new IllegalStateException("onSearch must call detach() or sendResult()"
   1585                     + " before returning for query=" + query);
   1586         }
   1587     }
   1588 
   1589     void performCustomAction(final String action, Bundle extras, ConnectionRecord connection,
   1590             final ResultReceiver receiver) {
   1591         final Result<Bundle> result = new Result<Bundle>(action) {
   1592                 @Override
   1593                 void onResultSent(Bundle result) {
   1594                     receiver.send(RESULT_OK, result);
   1595                 }
   1596 
   1597                 @Override
   1598                 void onProgressUpdateSent(Bundle data) {
   1599                     receiver.send(RESULT_PROGRESS_UPDATE, data);
   1600                 }
   1601 
   1602                 @Override
   1603                 void onErrorSent(Bundle data) {
   1604                     receiver.send(RESULT_ERROR, data);
   1605                 }
   1606             };
   1607 
   1608         mCurConnection = connection;
   1609         onCustomAction(action, extras, result);
   1610         mCurConnection = null;
   1611 
   1612         if (!result.isDone()) {
   1613             throw new IllegalStateException("onCustomAction must call detach() or sendResult() or "
   1614                     + "sendError() before returning for action=" + action + " extras="
   1615                     + extras);
   1616         }
   1617     }
   1618 
   1619     /**
   1620      * Contains information that the browser service needs to send to the client
   1621      * when first connected.
   1622      */
   1623     public static final class BrowserRoot {
   1624         /**
   1625          * The lookup key for a boolean that indicates whether the browser service should return a
   1626          * browser root for recently played media items.
   1627          *
   1628          * <p>When creating a media browser for a given media browser service, this key can be
   1629          * supplied as a root hint for retrieving media items that are recently played.
   1630          * If the media browser service can provide such media items, the implementation must return
   1631          * the key in the root hint when {@link #onGetRoot(String, int, Bundle)} is called back.
   1632          *
   1633          * <p>The root hint may contain multiple keys.
   1634          *
   1635          * @see #EXTRA_OFFLINE
   1636          * @see #EXTRA_SUGGESTED
   1637          */
   1638         public static final String EXTRA_RECENT = "android.service.media.extra.RECENT";
   1639 
   1640         /**
   1641          * The lookup key for a boolean that indicates whether the browser service should return a
   1642          * browser root for offline media items.
   1643          *
   1644          * <p>When creating a media browser for a given media browser service, this key can be
   1645          * supplied as a root hint for retrieving media items that are can be played without an
   1646          * internet connection.
   1647          * If the media browser service can provide such media items, the implementation must return
   1648          * the key in the root hint when {@link #onGetRoot(String, int, Bundle)} is called back.
   1649          *
   1650          * <p>The root hint may contain multiple keys.
   1651          *
   1652          * @see #EXTRA_RECENT
   1653          * @see #EXTRA_SUGGESTED
   1654          */
   1655         public static final String EXTRA_OFFLINE = "android.service.media.extra.OFFLINE";
   1656 
   1657         /**
   1658          * The lookup key for a boolean that indicates whether the browser service should return a
   1659          * browser root for suggested media items.
   1660          *
   1661          * <p>When creating a media browser for a given media browser service, this key can be
   1662          * supplied as a root hint for retrieving the media items suggested by the media browser
   1663          * service. The list of media items passed in {@link MediaBrowserCompat.SubscriptionCallback#onChildrenLoaded(String, List)}
   1664          * is considered ordered by relevance, first being the top suggestion.
   1665          * If the media browser service can provide such media items, the implementation must return
   1666          * the key in the root hint when {@link #onGetRoot(String, int, Bundle)} is called back.
   1667          *
   1668          * <p>The root hint may contain multiple keys.
   1669          *
   1670          * @see #EXTRA_RECENT
   1671          * @see #EXTRA_OFFLINE
   1672          */
   1673         public static final String EXTRA_SUGGESTED = "android.service.media.extra.SUGGESTED";
   1674 
   1675         /**
   1676          * The lookup key for a string that indicates specific keywords which will be considered
   1677          * when the browser service suggests media items.
   1678          *
   1679          * <p>When creating a media browser for a given media browser service, this key can be
   1680          * supplied as a root hint together with {@link #EXTRA_SUGGESTED} for retrieving suggested
   1681          * media items related with the keywords. The list of media items passed in
   1682          * {@link android.media.browse.MediaBrowser.SubscriptionCallback#onChildrenLoaded(String, List)}
   1683          * is considered ordered by relevance, first being the top suggestion.
   1684          * If the media browser service can provide such media items, the implementation must return
   1685          * the key in the root hint when {@link #onGetRoot(String, int, Bundle)} is called back.
   1686          *
   1687          * <p>The root hint may contain multiple keys.
   1688          *
   1689          * @see #EXTRA_RECENT
   1690          * @see #EXTRA_OFFLINE
   1691          * @see #EXTRA_SUGGESTED
   1692          * @deprecated The search functionality is now supported by the methods
   1693          *             {@link MediaBrowserCompat#search} and {@link #onSearch}. Use those methods
   1694          *             instead.
   1695          */
   1696         @Deprecated
   1697         public static final String EXTRA_SUGGESTION_KEYWORDS
   1698                 = "android.service.media.extra.SUGGESTION_KEYWORDS";
   1699 
   1700         final private String mRootId;
   1701         final private Bundle mExtras;
   1702 
   1703         /**
   1704          * Constructs a browser root.
   1705          * @param rootId The root id for browsing.
   1706          * @param extras Any extras about the browser service.
   1707          */
   1708         public BrowserRoot(@NonNull String rootId, @Nullable Bundle extras) {
   1709             if (rootId == null) {
   1710                 throw new IllegalArgumentException("The root id in BrowserRoot cannot be null. " +
   1711                         "Use null for BrowserRoot instead.");
   1712             }
   1713             mRootId = rootId;
   1714             mExtras = extras;
   1715         }
   1716 
   1717         /**
   1718          * Gets the root id for browsing.
   1719          */
   1720         public String getRootId() {
   1721             return mRootId;
   1722         }
   1723 
   1724         /**
   1725          * Gets any extras about the browser service.
   1726          */
   1727         public Bundle getExtras() {
   1728             return mExtras;
   1729         }
   1730     }
   1731 }
   1732