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 {
onCreate()185         void onCreate();
onBind(Intent intent)186         IBinder onBind(Intent intent);
setSessionToken(MediaSessionCompat.Token token)187         void setSessionToken(MediaSessionCompat.Token token);
notifyChildrenChanged(final String parentId, final Bundle options)188         void notifyChildrenChanged(final String parentId, final Bundle options);
getBrowserRootHints()189         Bundle getBrowserRootHints();
getCurrentBrowserInfo()190         RemoteUserInfo getCurrentBrowserInfo();
getSubscribingBrowsers(String parentId)191         List<RemoteUserInfo> getSubscribingBrowsers(String parentId);
192     }
193 
194     class MediaBrowserServiceImplBase implements MediaBrowserServiceImpl {
195         private Messenger mMessenger;
196 
197         @Override
onCreate()198         public void onCreate() {
199             mMessenger = new Messenger(mHandler);
200         }
201 
202         @Override
onBind(Intent intent)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
setSessionToken(final MediaSessionCompat.Token token)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
notifyChildrenChanged(@onNull final String parentId, final Bundle options)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
getBrowserRootHints()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
getCurrentBrowserInfo()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
getSubscribingBrowsers(String parentId)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
onCreate()294         public void onCreate() {
295             mServiceObj = MediaBrowserServiceCompatApi21.createService(
296                     MediaBrowserServiceCompat.this, this);
297             MediaBrowserServiceCompatApi21.onCreate(mServiceObj);
298         }
299 
300         @Override
onBind(Intent intent)301         public IBinder onBind(Intent intent) {
302             return MediaBrowserServiceCompatApi21.onBind(mServiceObj, intent);
303         }
304 
305         @Override
setSessionToken(final MediaSessionCompat.Token token)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
notifyChildrenChanged(final String parentId, final Bundle options)326         public void notifyChildrenChanged(final String parentId, final Bundle options) {
327             notifyChildrenChangedForFramework(parentId, options);
328             notifyChildrenChangedForCompat(parentId, options);
329         }
330 
331         @Override
onGetRoot( String clientPackageName, int clientUid, Bundle rootHints)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
onLoadChildren(String parentId, final MediaBrowserServiceCompatApi21.ResultWrapper<List<Parcel>> resultWrapper)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
getSubscribingBrowsers(String parentId)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 
notifyChildrenChangedForFramework(final String parentId, final Bundle options)409         void notifyChildrenChangedForFramework(final String parentId, final Bundle options) {
410             MediaBrowserServiceCompatApi21.notifyChildrenChanged(mServiceObj, parentId);
411         }
412 
notifyChildrenChangedForCompat(final String parentId, final Bundle options)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
getBrowserRootHints()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
getCurrentBrowserInfo()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
onCreate()462         public void onCreate() {
463             mServiceObj = MediaBrowserServiceCompatApi23.createService(
464                     MediaBrowserServiceCompat.this, this);
465             MediaBrowserServiceCompatApi21.onCreate(mServiceObj);
466         }
467 
468         @Override
onLoadItem(String itemId, final MediaBrowserServiceCompatApi21.ResultWrapper<Parcel> resultWrapper)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
onCreate()497         public void onCreate() {
498             mServiceObj = MediaBrowserServiceCompatApi26.createService(
499                     MediaBrowserServiceCompat.this, this);
500             MediaBrowserServiceCompatApi21.onCreate(mServiceObj);
501         }
502 
503         @Override
onLoadChildren(String parentId, final MediaBrowserServiceCompatApi26.ResultWrapper resultWrapper, Bundle options)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
getBrowserRootHints()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
notifyChildrenChangedForFramework(final String parentId, final Bundle options)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
getCurrentBrowserInfo()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 
ServiceHandler()569         ServiceHandler() {
570         }
571 
572         @Override
handleMessage(Message msg)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
sendMessageAtTime(Message msg, long uptimeMillis)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 
postOrRun(Runnable r)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 
ConnectionRecord(String pkg, int pid, int uid, Bundle rootHints, ServiceCallbacks callback)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
binderDied()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 
Result(Object debug)710         Result(Object debug) {
711             mDebug = debug;
712         }
713 
714         /**
715          * Send the result back to the caller.
716          */
sendResult(T result)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          */
sendProgressUpdate(Bundle extras)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          */
sendError(Bundle extras)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          */
detach()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 
isDone()777         boolean isDone() {
778             return mDetachCalled || mSendResultCalled || mSendErrorCalled;
779         }
780 
setFlags(@esultFlags int flags)781         void setFlags(@ResultFlags int flags) {
782             mFlags = flags;
783         }
784 
getFlags()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          */
onResultSent(T result)793         void onResultSent(T result) {
794         }
795 
796         /**
797          * Called when an interim update is sent.
798          */
onProgressUpdateSent(Bundle extras)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          */
onErrorSent(Bundle extras)808         void onErrorSent(Bundle extras) {
809             throw new UnsupportedOperationException("It is not supported to send an error for "
810                     + mDebug);
811         }
812 
checkExtraFields(Bundle extras)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 {
ServiceBinderImpl()828         ServiceBinderImpl() {
829         }
830 
connect(final String pkg, final int pid, final int uid, final Bundle rootHints, final ServiceCallbacks callbacks)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 
disconnect(final ServiceCallbacks callbacks)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 
addSubscription(final String id, final IBinder token, final Bundle options, final ServiceCallbacks callbacks)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 
removeSubscription(final String id, final IBinder token, final ServiceCallbacks callbacks)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 
getMediaItem(final String mediaId, final ResultReceiver receiver, final ServiceCallbacks callbacks)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.
registerCallbacks(final ServiceCallbacks callbacks, final String pkg, final int pid, final int uid, final Bundle rootHints)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.
unregisterCallbacks(final ServiceCallbacks callbacks)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 
search(final String query, final Bundle extras, final ResultReceiver receiver, final ServiceCallbacks callbacks)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 
sendCustomAction(final String action, final Bundle extras, final ResultReceiver receiver, final ServiceCallbacks callbacks)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 {
asBinder()1041         IBinder asBinder();
onConnect(String root, MediaSessionCompat.Token session, Bundle extras)1042         void onConnect(String root, MediaSessionCompat.Token session, Bundle extras)
1043                 throws RemoteException;
onConnectFailed()1044         void onConnectFailed() throws RemoteException;
onLoadChildren(String mediaId, List<MediaBrowserCompat.MediaItem> list, Bundle options, Bundle notifyChildrenChangedOptions)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 
ServiceCallbacksCompat(Messenger callbacks)1052         ServiceCallbacksCompat(Messenger callbacks) {
1053             mCallbacks = callbacks;
1054         }
1055 
1056         @Override
asBinder()1057         public IBinder asBinder() {
1058             return mCallbacks.getBinder();
1059         }
1060 
1061         @Override
onConnect(String root, MediaSessionCompat.Token session, Bundle extras)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
onConnectFailed()1076         public void onConnectFailed() throws RemoteException {
1077             sendRequest(SERVICE_MSG_ON_CONNECT_FAILED, null);
1078         }
1079 
1080         @Override
onLoadChildren(String mediaId, List<MediaBrowserCompat.MediaItem> list, Bundle options, Bundle notifyChildrenChangedOptions)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 
sendRequest(int what, Bundle data)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)
attachToBaseContext(Context base)1114     public void attachToBaseContext(Context base) {
1115         attachBaseContext(base);
1116     }
1117 
1118     @Override
onCreate()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
onBind(Intent intent)1136     public IBinder onBind(Intent intent) {
1137         return mImpl.onBind(intent);
1138     }
1139 
1140     @Override
dump(FileDescriptor fd, PrintWriter writer, String[] args)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      */
onGetRoot(@onNull String clientPackageName, int clientUid, @Nullable Bundle rootHints)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      */
onLoadChildren(@onNull String parentId, @NonNull Result<List<MediaBrowserCompat.MediaItem>> result)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      */
onLoadChildren(@onNull String parentId, @NonNull Result<List<MediaBrowserCompat.MediaItem>> result, @NonNull Bundle options)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      */
onLoadItem(String itemId, @NonNull Result<MediaBrowserCompat.MediaItem> result)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      */
onSearch(@onNull String query, Bundle extras, @NonNull Result<List<MediaBrowserCompat.MediaItem>> result)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      */
onCustomAction(@onNull String action, Bundle extras, @NonNull Result<Bundle> result)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      */
setSessionToken(MediaSessionCompat.Token token)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      */
getSessionToken()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      */
getBrowserRootHints()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      */
getCurrentBrowserInfo()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      */
notifyChildrenChanged(@onNull String parentId)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      */
notifyChildrenChanged(@onNull String parentId, @NonNull Bundle options)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)
getSubscribingBrowsers(@onNull String parentId)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      */
isValidPackage(String pkg, int uid)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      */
addSubscription(String id, ConnectionRecord connection, IBinder token, Bundle options)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      */
removeSubscription(String id, ConnectionRecord connection, IBinder token)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      */
performLoadChildren(final String parentId, final ConnectionRecord connection, final Bundle subscribeOptions, final Bundle notifyChildrenChangedOptions)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 
applyOptions(List<MediaBrowserCompat.MediaItem> list, final Bundle options)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 
performLoadItem(String itemId, ConnectionRecord connection, final ResultReceiver receiver)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 
performSearch(final String query, Bundle extras, ConnectionRecord connection, final ResultReceiver receiver)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 
performCustomAction(final String action, Bundle extras, ConnectionRecord connection, final ResultReceiver receiver)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          */
BrowserRoot(@onNull String rootId, @Nullable Bundle extras)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          */
getRootId()1720         public String getRootId() {
1721             return mRootId;
1722         }
1723 
1724         /**
1725          * Gets any extras about the browser service.
1726          */
getExtras()1727         public Bundle getExtras() {
1728             return mExtras;
1729         }
1730     }
1731 }
1732