• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.support.v4.media;
18 
19 import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_ADD_SUBSCRIPTION;
20 import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_CONNECT;
21 import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_DISCONNECT;
22 import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_GET_MEDIA_ITEM;
23 import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_REGISTER_CALLBACK_MESSENGER;
24 import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_REMOVE_SUBSCRIPTION;
25 import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_UNREGISTER_CALLBACK_MESSENGER;
26 import static android.support.v4.media.MediaBrowserProtocol.DATA_CALLBACK_TOKEN;
27 import static android.support.v4.media.MediaBrowserProtocol.DATA_CALLING_UID;
28 import static android.support.v4.media.MediaBrowserProtocol.DATA_MEDIA_ITEM_ID;
29 import static android.support.v4.media.MediaBrowserProtocol.DATA_MEDIA_ITEM_LIST;
30 import static android.support.v4.media.MediaBrowserProtocol.DATA_MEDIA_SESSION_TOKEN;
31 import static android.support.v4.media.MediaBrowserProtocol.DATA_OPTIONS;
32 import static android.support.v4.media.MediaBrowserProtocol.DATA_PACKAGE_NAME;
33 import static android.support.v4.media.MediaBrowserProtocol.DATA_RESULT_RECEIVER;
34 import static android.support.v4.media.MediaBrowserProtocol.DATA_ROOT_HINTS;
35 import static android.support.v4.media.MediaBrowserProtocol.EXTRA_CLIENT_VERSION;
36 import static android.support.v4.media.MediaBrowserProtocol.EXTRA_MESSENGER_BINDER;
37 import static android.support.v4.media.MediaBrowserProtocol.EXTRA_SERVICE_VERSION;
38 import static android.support.v4.media.MediaBrowserProtocol.SERVICE_MSG_ON_CONNECT;
39 import static android.support.v4.media.MediaBrowserProtocol.SERVICE_MSG_ON_CONNECT_FAILED;
40 import static android.support.v4.media.MediaBrowserProtocol.SERVICE_MSG_ON_LOAD_CHILDREN;
41 import static android.support.v4.media.MediaBrowserProtocol.SERVICE_VERSION_CURRENT;
42 
43 import android.app.Service;
44 import android.content.Intent;
45 import android.content.pm.PackageManager;
46 import android.os.Binder;
47 import android.os.Build;
48 import android.os.Bundle;
49 import android.os.Handler;
50 import android.os.IBinder;
51 import android.os.Message;
52 import android.os.Messenger;
53 import android.os.Parcel;
54 import android.os.RemoteException;
55 import android.support.annotation.IntDef;
56 import android.support.annotation.NonNull;
57 import android.support.annotation.Nullable;
58 import android.support.v4.app.BundleCompat;
59 import android.support.v4.media.session.MediaSessionCompat;
60 import android.support.v4.os.BuildCompat;
61 import android.support.v4.os.ResultReceiver;
62 import android.support.v4.util.ArrayMap;
63 import android.support.v4.util.Pair;
64 import android.text.TextUtils;
65 import android.util.Log;
66 
67 import java.io.FileDescriptor;
68 import java.io.PrintWriter;
69 import java.lang.annotation.Retention;
70 import java.lang.annotation.RetentionPolicy;
71 import java.util.ArrayList;
72 import java.util.Collections;
73 import java.util.HashMap;
74 import java.util.Iterator;
75 import java.util.List;
76 
77 /**
78  * Base class for media browse services.
79  * <p>
80  * Media browse services enable applications to browse media content provided by an application
81  * and ask the application to start playing it. They may also be used to control content that
82  * is already playing by way of a {@link MediaSessionCompat}.
83  * </p>
84  *
85  * To extend this class, you must declare the service in your manifest file with
86  * an intent filter with the {@link #SERVICE_INTERFACE} action.
87  *
88  * For example:
89  * </p><pre>
90  * &lt;service android:name=".MyMediaBrowserServiceCompat"
91  *          android:label="&#64;string/service_name" >
92  *     &lt;intent-filter>
93  *         &lt;action android:name="android.media.browse.MediaBrowserService" />
94  *     &lt;/intent-filter>
95  * &lt;/service>
96  * </pre>
97  */
98 public abstract class MediaBrowserServiceCompat extends Service {
99     private static final String TAG = "MBServiceCompat";
100     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
101 
102     private MediaBrowserServiceImpl mImpl;
103 
104     /**
105      * The {@link Intent} that must be declared as handled by the service.
106      */
107     public static final String SERVICE_INTERFACE = "android.media.browse.MediaBrowserService";
108 
109     /**
110      * A key for passing the MediaItem to the ResultReceiver in getItem.
111      *
112      * @hide
113      */
114     public static final String KEY_MEDIA_ITEM = "media_item";
115 
116     private static final int RESULT_FLAG_OPTION_NOT_HANDLED = 0x00000001;
117 
118     /** @hide */
119     @Retention(RetentionPolicy.SOURCE)
120     @IntDef(flag=true, value = { RESULT_FLAG_OPTION_NOT_HANDLED })
121     private @interface ResultFlags { }
122 
123     private final ArrayMap<IBinder, ConnectionRecord> mConnections = new ArrayMap<>();
124     private ConnectionRecord mCurConnection;
125     private final ServiceHandler mHandler = new ServiceHandler();
126     MediaSessionCompat.Token mSession;
127 
128     interface MediaBrowserServiceImpl {
onCreate()129         void onCreate();
onBind(Intent intent)130         IBinder onBind(Intent intent);
setSessionToken(MediaSessionCompat.Token token)131         void setSessionToken(MediaSessionCompat.Token token);
notifyChildrenChanged(final String parentId, final Bundle options)132         void notifyChildrenChanged(final String parentId, final Bundle options);
getBrowserRootHints()133         Bundle getBrowserRootHints();
134     }
135 
136     class MediaBrowserServiceImplBase implements MediaBrowserServiceImpl {
137         private Messenger mMessenger;
138 
139         @Override
onCreate()140         public void onCreate() {
141             mMessenger = new Messenger(mHandler);
142         }
143 
144         @Override
onBind(Intent intent)145         public IBinder onBind(Intent intent) {
146             if (SERVICE_INTERFACE.equals(intent.getAction())) {
147                 return mMessenger.getBinder();
148             }
149             return null;
150         }
151 
152         @Override
setSessionToken(final MediaSessionCompat.Token token)153         public void setSessionToken(final MediaSessionCompat.Token token) {
154             mHandler.post(new Runnable() {
155                 @Override
156                 public void run() {
157                     Iterator<ConnectionRecord> iter = mConnections.values().iterator();
158                     while (iter.hasNext()){
159                         ConnectionRecord connection = iter.next();
160                         try {
161                             connection.callbacks.onConnect(connection.root.getRootId(), token,
162                                     connection.root.getExtras());
163                         } catch (RemoteException e) {
164                             Log.w(TAG, "Connection for " + connection.pkg + " is no longer valid.");
165                             iter.remove();
166                         }
167                     }
168                 }
169             });
170         }
171 
172         @Override
notifyChildrenChanged(@onNull final String parentId, final Bundle options)173         public void notifyChildrenChanged(@NonNull final String parentId, final Bundle options) {
174             mHandler.post(new Runnable() {
175                 @Override
176                 public void run() {
177                     for (IBinder binder : mConnections.keySet()) {
178                         ConnectionRecord connection = mConnections.get(binder);
179                         List<Pair<IBinder, Bundle>> callbackList =
180                                 connection.subscriptions.get(parentId);
181                         if (callbackList != null) {
182                             for (Pair<IBinder, Bundle> callback : callbackList) {
183                                 if (MediaBrowserCompatUtils.hasDuplicatedItems(
184                                         options, callback.second)) {
185                                     performLoadChildren(parentId, connection, callback.second);
186                                 }
187                             }
188                         }
189                     }
190                 }
191             });
192         }
193 
194         @Override
getBrowserRootHints()195         public Bundle getBrowserRootHints() {
196             if (mCurConnection == null) {
197                 throw new IllegalStateException("This should be called inside of onLoadChildren or"
198                         + " onLoadItem methods");
199             }
200             return mCurConnection.rootHints == null ? null : new Bundle(mCurConnection.rootHints);
201         }
202     }
203 
204     class MediaBrowserServiceImplApi21 implements MediaBrowserServiceImpl,
205             MediaBrowserServiceCompatApi21.ServiceCompatProxy {
206         Object mServiceObj;
207         Messenger mMessenger;
208 
209         @Override
onCreate()210         public void onCreate() {
211             mServiceObj = MediaBrowserServiceCompatApi21.createService(
212                     MediaBrowserServiceCompat.this, this);
213             MediaBrowserServiceCompatApi21.onCreate(mServiceObj);
214         }
215 
216         @Override
onBind(Intent intent)217         public IBinder onBind(Intent intent) {
218             return MediaBrowserServiceCompatApi21.onBind(mServiceObj, intent);
219         }
220 
221         @Override
setSessionToken(MediaSessionCompat.Token token)222         public void setSessionToken(MediaSessionCompat.Token token) {
223             MediaBrowserServiceCompatApi21.setSessionToken(mServiceObj, token.getToken());
224         }
225 
226         @Override
notifyChildrenChanged(final String parentId, final Bundle options)227         public void notifyChildrenChanged(final String parentId, final Bundle options) {
228             if (mMessenger == null) {
229                 MediaBrowserServiceCompatApi21.notifyChildrenChanged(mServiceObj, parentId);
230             } else {
231                 mHandler.post(new Runnable() {
232                     @Override
233                     public void run() {
234                         for (IBinder binder : mConnections.keySet()) {
235                             ConnectionRecord connection = mConnections.get(binder);
236                             List<Pair<IBinder, Bundle>> callbackList =
237                                     connection.subscriptions.get(parentId);
238                             if (callbackList != null) {
239                                 for (Pair<IBinder, Bundle> callback : callbackList) {
240                                     if (MediaBrowserCompatUtils.hasDuplicatedItems(
241                                             options, callback.second)) {
242                                         performLoadChildren(parentId, connection, callback.second);
243                                     }
244                                 }
245                             }
246                         }
247                     }
248                 });
249             }
250         }
251 
252         @Override
getBrowserRootHints()253         public Bundle getBrowserRootHints() {
254             if (mMessenger == null) {
255                 // TODO: Handle getBrowserRootHints when connected with framework MediaBrowser.
256                 return null;
257             }
258             if (mCurConnection == null) {
259                 throw new IllegalStateException("This should be called inside of onLoadChildren or"
260                         + " onLoadItem methods");
261             }
262             return mCurConnection.rootHints == null ? null : new Bundle(mCurConnection.rootHints);
263         }
264 
265         @Override
onGetRoot( String clientPackageName, int clientUid, Bundle rootHints)266         public MediaBrowserServiceCompatApi21.BrowserRoot onGetRoot(
267                 String clientPackageName, int clientUid, Bundle rootHints) {
268             Bundle rootExtras = null;
269             if (rootHints != null && rootHints.getInt(EXTRA_CLIENT_VERSION, 0) != 0) {
270                 rootHints.remove(EXTRA_CLIENT_VERSION);
271                 mMessenger = new Messenger(mHandler);
272                 rootExtras = new Bundle();
273                 rootExtras.putInt(EXTRA_SERVICE_VERSION, SERVICE_VERSION_CURRENT);
274                 BundleCompat.putBinder(rootExtras, EXTRA_MESSENGER_BINDER, mMessenger.getBinder());
275             }
276             BrowserRoot root = MediaBrowserServiceCompat.this.onGetRoot(
277                     clientPackageName, clientUid, rootHints);
278             if (root == null) {
279                 return null;
280             }
281             if (rootExtras == null) {
282                 rootExtras = root.getExtras();
283             } else if (root.getExtras() != null) {
284                 rootExtras.putAll(root.getExtras());
285             }
286             return new MediaBrowserServiceCompatApi21.BrowserRoot(
287                     root.getRootId(), rootExtras);
288         }
289 
290         @Override
onLoadChildren(String parentId, final MediaBrowserServiceCompatApi21.ResultWrapper<List<Parcel>> resultWrapper)291         public void onLoadChildren(String parentId,
292                 final MediaBrowserServiceCompatApi21.ResultWrapper<List<Parcel>> resultWrapper) {
293             final Result<List<MediaBrowserCompat.MediaItem>> result
294                     = new Result<List<MediaBrowserCompat.MediaItem>>(parentId) {
295                 @Override
296                 void onResultSent(List<MediaBrowserCompat.MediaItem> list, @ResultFlags int flags) {
297                     List<Parcel> parcelList = null;
298                     if (list != null) {
299                         parcelList = new ArrayList<>();
300                         for (MediaBrowserCompat.MediaItem item : list) {
301                             Parcel parcel = Parcel.obtain();
302                             item.writeToParcel(parcel, 0);
303                             parcelList.add(parcel);
304                         }
305                     }
306                     resultWrapper.sendResult(parcelList);
307                 }
308 
309                 @Override
310                 public void detach() {
311                     resultWrapper.detach();
312                 }
313             };
314             MediaBrowserServiceCompat.this.onLoadChildren(parentId, result);
315         }
316     }
317 
318     class MediaBrowserServiceImplApi23 extends MediaBrowserServiceImplApi21 implements
319             MediaBrowserServiceCompatApi23.ServiceCompatProxy {
320         @Override
onCreate()321         public void onCreate() {
322             mServiceObj = MediaBrowserServiceCompatApi23.createService(
323                     MediaBrowserServiceCompat.this, this);
324             MediaBrowserServiceCompatApi21.onCreate(mServiceObj);
325         }
326 
327         @Override
onLoadItem(String itemId, final MediaBrowserServiceCompatApi21.ResultWrapper<Parcel> resultWrapper)328         public void onLoadItem(String itemId,
329                 final MediaBrowserServiceCompatApi21.ResultWrapper<Parcel> resultWrapper) {
330             final Result<MediaBrowserCompat.MediaItem> result
331                     = new Result<MediaBrowserCompat.MediaItem>(itemId) {
332                 @Override
333                 void onResultSent(MediaBrowserCompat.MediaItem item, @ResultFlags int flags) {
334                     Parcel parcelItem = Parcel.obtain();
335                     item.writeToParcel(parcelItem, 0);
336                     resultWrapper.sendResult(parcelItem);
337                 }
338 
339                 @Override
340                 public void detach() {
341                     resultWrapper.detach();
342                 }
343             };
344             MediaBrowserServiceCompat.this.onLoadItem(itemId, result);
345         }
346     }
347 
348     class MediaBrowserServiceImplApi24 extends MediaBrowserServiceImplApi23 implements
349             MediaBrowserServiceCompatApi24.ServiceCompatProxy {
350         @Override
onCreate()351         public void onCreate() {
352             mServiceObj = MediaBrowserServiceCompatApi24.createService(
353                     MediaBrowserServiceCompat.this, this);
354             MediaBrowserServiceCompatApi21.onCreate(mServiceObj);
355         }
356 
357         @Override
notifyChildrenChanged(final String parentId, final Bundle options)358         public void notifyChildrenChanged(final String parentId, final Bundle options) {
359             if (options == null) {
360                 MediaBrowserServiceCompatApi21.notifyChildrenChanged(mServiceObj, parentId);
361             } else {
362                 MediaBrowserServiceCompatApi24.notifyChildrenChanged(mServiceObj, parentId,
363                         options);
364             }
365         }
366 
367         @Override
onLoadChildren(String parentId, final MediaBrowserServiceCompatApi24.ResultWrapper resultWrapper, Bundle options)368         public void onLoadChildren(String parentId,
369                 final MediaBrowserServiceCompatApi24.ResultWrapper resultWrapper, Bundle options) {
370             final Result<List<MediaBrowserCompat.MediaItem>> result
371                     = new Result<List<MediaBrowserCompat.MediaItem>>(parentId) {
372                 @Override
373                 void onResultSent(List<MediaBrowserCompat.MediaItem> list, @ResultFlags int flags) {
374                     List<Parcel> parcelList = null;
375                     if (list != null) {
376                         parcelList = new ArrayList<>();
377                         for (MediaBrowserCompat.MediaItem item : list) {
378                             Parcel parcel = Parcel.obtain();
379                             item.writeToParcel(parcel, 0);
380                             parcelList.add(parcel);
381                         }
382                     }
383                     resultWrapper.sendResult(parcelList, flags);
384                 }
385 
386                 @Override
387                 public void detach() {
388                     resultWrapper.detach();
389                 }
390             };
391             MediaBrowserServiceCompat.this.onLoadChildren(parentId, result, options);
392         }
393 
394         @Override
getBrowserRootHints()395         public Bundle getBrowserRootHints() {
396             return MediaBrowserServiceCompatApi24.getBrowserRootHints(mServiceObj);
397         }
398     }
399 
400     private final class ServiceHandler extends Handler {
401         private final ServiceBinderImpl mServiceBinderImpl = new ServiceBinderImpl();
402 
403         @Override
handleMessage(Message msg)404         public void handleMessage(Message msg) {
405             Bundle data = msg.getData();
406             switch (msg.what) {
407                 case CLIENT_MSG_CONNECT:
408                     mServiceBinderImpl.connect(data.getString(DATA_PACKAGE_NAME),
409                             data.getInt(DATA_CALLING_UID), data.getBundle(DATA_ROOT_HINTS),
410                             new ServiceCallbacksCompat(msg.replyTo));
411                     break;
412                 case CLIENT_MSG_DISCONNECT:
413                     mServiceBinderImpl.disconnect(new ServiceCallbacksCompat(msg.replyTo));
414                     break;
415                 case CLIENT_MSG_ADD_SUBSCRIPTION:
416                     mServiceBinderImpl.addSubscription(data.getString(DATA_MEDIA_ITEM_ID),
417                             BundleCompat.getBinder(data, DATA_CALLBACK_TOKEN),
418                             data.getBundle(DATA_OPTIONS),
419                             new ServiceCallbacksCompat(msg.replyTo));
420                     break;
421                 case CLIENT_MSG_REMOVE_SUBSCRIPTION:
422                     mServiceBinderImpl.removeSubscription(data.getString(DATA_MEDIA_ITEM_ID),
423                             BundleCompat.getBinder(data, DATA_CALLBACK_TOKEN),
424                             new ServiceCallbacksCompat(msg.replyTo));
425                     break;
426                 case CLIENT_MSG_GET_MEDIA_ITEM:
427                     mServiceBinderImpl.getMediaItem(data.getString(DATA_MEDIA_ITEM_ID),
428                             (ResultReceiver) data.getParcelable(DATA_RESULT_RECEIVER),
429                             new ServiceCallbacksCompat(msg.replyTo));
430                     break;
431                 case CLIENT_MSG_REGISTER_CALLBACK_MESSENGER:
432                     mServiceBinderImpl.registerCallbacks(new ServiceCallbacksCompat(msg.replyTo),
433                             data.getBundle(DATA_ROOT_HINTS));
434                     break;
435                 case CLIENT_MSG_UNREGISTER_CALLBACK_MESSENGER:
436                     mServiceBinderImpl.unregisterCallbacks(new ServiceCallbacksCompat(msg.replyTo));
437                     break;
438                 default:
439                     Log.w(TAG, "Unhandled message: " + msg
440                             + "\n  Service version: " + SERVICE_VERSION_CURRENT
441                             + "\n  Client version: " + msg.arg1);
442             }
443         }
444 
445         @Override
sendMessageAtTime(Message msg, long uptimeMillis)446         public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
447             // Binder.getCallingUid() in handleMessage will return the uid of this process.
448             // In order to get the right calling uid, Binder.getCallingUid() should be called here.
449             Bundle data = msg.getData();
450             data.setClassLoader(MediaBrowserCompat.class.getClassLoader());
451             data.putInt(DATA_CALLING_UID, Binder.getCallingUid());
452             return super.sendMessageAtTime(msg, uptimeMillis);
453         }
454 
postOrRun(Runnable r)455         public void postOrRun(Runnable r) {
456             if (Thread.currentThread() == getLooper().getThread()) {
457                 r.run();
458             } else {
459                 post(r);
460             }
461         }
462     }
463 
464     /**
465      * All the info about a connection.
466      */
467     private class ConnectionRecord {
468         String pkg;
469         Bundle rootHints;
470         ServiceCallbacks callbacks;
471         BrowserRoot root;
472         HashMap<String, List<Pair<IBinder, Bundle>>> subscriptions = new HashMap();
473     }
474 
475     /**
476      * Completion handler for asynchronous callback methods in {@link MediaBrowserServiceCompat}.
477      * <p>
478      * Each of the methods that takes one of these to send the result must call
479      * {@link #sendResult} to respond to the caller with the given results. If those
480      * functions return without calling {@link #sendResult}, they must instead call
481      * {@link #detach} before returning, and then may call {@link #sendResult} when
482      * they are done. If more than one of those methods is called, an exception will
483      * be thrown.
484      *
485      * @see MediaBrowserServiceCompat#onLoadChildren
486      * @see MediaBrowserServiceCompat#onLoadItem
487      */
488     public static class Result<T> {
489         private Object mDebug;
490         private boolean mDetachCalled;
491         private boolean mSendResultCalled;
492         private int mFlags;
493 
Result(Object debug)494         Result(Object debug) {
495             mDebug = debug;
496         }
497 
498         /**
499          * Send the result back to the caller.
500          */
sendResult(T result)501         public void sendResult(T result) {
502             if (mSendResultCalled) {
503                 throw new IllegalStateException("sendResult() called twice for: " + mDebug);
504             }
505             mSendResultCalled = true;
506             onResultSent(result, mFlags);
507         }
508 
509         /**
510          * Detach this message from the current thread and allow the {@link #sendResult}
511          * call to happen later.
512          */
detach()513         public void detach() {
514             if (mDetachCalled) {
515                 throw new IllegalStateException("detach() called when detach() had already"
516                         + " been called for: " + mDebug);
517             }
518             if (mSendResultCalled) {
519                 throw new IllegalStateException("detach() called when sendResult() had already"
520                         + " been called for: " + mDebug);
521             }
522             mDetachCalled = true;
523         }
524 
isDone()525         boolean isDone() {
526             return mDetachCalled || mSendResultCalled;
527         }
528 
setFlags(@esultFlags int flags)529         void setFlags(@ResultFlags int flags) {
530             mFlags = flags;
531         }
532 
533         /**
534          * Called when the result is sent, after assertions about not being called twice
535          * have happened.
536          */
onResultSent(T result, @ResultFlags int flags)537         void onResultSent(T result, @ResultFlags int flags) {
538         }
539     }
540 
541     private class ServiceBinderImpl {
connect(final String pkg, final int uid, final Bundle rootHints, final ServiceCallbacks callbacks)542         public void connect(final String pkg, final int uid, final Bundle rootHints,
543                 final ServiceCallbacks callbacks) {
544 
545             if (!isValidPackage(pkg, uid)) {
546                 throw new IllegalArgumentException("Package/uid mismatch: uid=" + uid
547                         + " package=" + pkg);
548             }
549 
550             mHandler.postOrRun(new Runnable() {
551                 @Override
552                 public void run() {
553                     final IBinder b = callbacks.asBinder();
554 
555                     // Clear out the old subscriptions. We are getting new ones.
556                     mConnections.remove(b);
557 
558                     final ConnectionRecord connection = new ConnectionRecord();
559                     connection.pkg = pkg;
560                     connection.rootHints = rootHints;
561                     connection.callbacks = callbacks;
562 
563                     connection.root =
564                             MediaBrowserServiceCompat.this.onGetRoot(pkg, uid, rootHints);
565 
566                     // If they didn't return something, don't allow this client.
567                     if (connection.root == null) {
568                         Log.i(TAG, "No root for client " + pkg + " from service "
569                                 + getClass().getName());
570                         try {
571                             callbacks.onConnectFailed();
572                         } catch (RemoteException ex) {
573                             Log.w(TAG, "Calling onConnectFailed() failed. Ignoring. "
574                                     + "pkg=" + pkg);
575                         }
576                     } else {
577                         try {
578                             mConnections.put(b, connection);
579                             if (mSession != null) {
580                                 callbacks.onConnect(connection.root.getRootId(),
581                                         mSession, connection.root.getExtras());
582                             }
583                         } catch (RemoteException ex) {
584                             Log.w(TAG, "Calling onConnect() failed. Dropping client. "
585                                     + "pkg=" + pkg);
586                             mConnections.remove(b);
587                         }
588                     }
589                 }
590             });
591         }
592 
disconnect(final ServiceCallbacks callbacks)593         public void disconnect(final ServiceCallbacks callbacks) {
594             mHandler.postOrRun(new Runnable() {
595                 @Override
596                 public void run() {
597                     final IBinder b = callbacks.asBinder();
598 
599                     // Clear out the old subscriptions. We are getting new ones.
600                     final ConnectionRecord old = mConnections.remove(b);
601                     if (old != null) {
602                         // TODO
603                     }
604                 }
605             });
606         }
607 
addSubscription(final String id, final IBinder token, final Bundle options, final ServiceCallbacks callbacks)608         public void addSubscription(final String id, final IBinder token, final Bundle options,
609                 final ServiceCallbacks callbacks) {
610             mHandler.postOrRun(new Runnable() {
611                 @Override
612                 public void run() {
613                     final IBinder b = callbacks.asBinder();
614 
615                     // Get the record for the connection
616                     final ConnectionRecord connection = mConnections.get(b);
617                     if (connection == null) {
618                         Log.w(TAG, "addSubscription for callback that isn't registered id="
619                                 + id);
620                         return;
621                     }
622 
623                     MediaBrowserServiceCompat.this.addSubscription(id, connection, token, options);
624                 }
625             });
626         }
627 
removeSubscription(final String id, final IBinder token, final ServiceCallbacks callbacks)628         public void removeSubscription(final String id, final IBinder token,
629                 final ServiceCallbacks callbacks) {
630             mHandler.postOrRun(new Runnable() {
631                 @Override
632                 public void run() {
633                     final IBinder b = callbacks.asBinder();
634 
635                     ConnectionRecord connection = mConnections.get(b);
636                     if (connection == null) {
637                         Log.w(TAG, "removeSubscription for callback that isn't registered id="
638                                 + id);
639                         return;
640                     }
641                     if (!MediaBrowserServiceCompat.this.removeSubscription(
642                             id, connection, token)) {
643                         Log.w(TAG, "removeSubscription called for " + id
644                                 + " which is not subscribed");
645                     }
646                 }
647             });
648         }
649 
getMediaItem(final String mediaId, final ResultReceiver receiver, final ServiceCallbacks callbacks)650         public void getMediaItem(final String mediaId, final ResultReceiver receiver,
651                 final ServiceCallbacks callbacks) {
652             if (TextUtils.isEmpty(mediaId) || receiver == null) {
653                 return;
654             }
655 
656             mHandler.postOrRun(new Runnable() {
657                 @Override
658                 public void run() {
659                     final IBinder b = callbacks.asBinder();
660 
661                     ConnectionRecord connection = mConnections.get(b);
662                     if (connection == null) {
663                         Log.w(TAG, "getMediaItem for callback that isn't registered id=" + mediaId);
664                         return;
665                     }
666                     performLoadItem(mediaId, connection, receiver);
667                 }
668             });
669         }
670 
671         // Used when {@link MediaBrowserProtocol#EXTRA_MESSENGER_BINDER} is used.
registerCallbacks(final ServiceCallbacks callbacks, final Bundle rootHints)672         public void registerCallbacks(final ServiceCallbacks callbacks, final Bundle rootHints) {
673             mHandler.postOrRun(new Runnable() {
674                 @Override
675                 public void run() {
676                     final IBinder b = callbacks.asBinder();
677                     // Clear out the old subscriptions. We are getting new ones.
678                     mConnections.remove(b);
679 
680                     final ConnectionRecord connection = new ConnectionRecord();
681                     connection.callbacks = callbacks;
682                     connection.rootHints = rootHints;
683                     mConnections.put(b, connection);
684                 }
685             });
686         }
687 
688         // Used when {@link MediaBrowserProtocol#EXTRA_MESSENGER_BINDER} is used.
unregisterCallbacks(final ServiceCallbacks callbacks)689         public void unregisterCallbacks(final ServiceCallbacks callbacks) {
690             mHandler.postOrRun(new Runnable() {
691                 @Override
692                 public void run() {
693                     final IBinder b = callbacks.asBinder();
694                     mConnections.remove(b);
695                 }
696             });
697         }
698     }
699 
700     private interface ServiceCallbacks {
asBinder()701         IBinder asBinder();
onConnect(String root, MediaSessionCompat.Token session, Bundle extras)702         void onConnect(String root, MediaSessionCompat.Token session, Bundle extras)
703                 throws RemoteException;
onConnectFailed()704         void onConnectFailed() throws RemoteException;
onLoadChildren(String mediaId, List<MediaBrowserCompat.MediaItem> list, Bundle options)705         void onLoadChildren(String mediaId, List<MediaBrowserCompat.MediaItem> list, Bundle options)
706                 throws RemoteException;
707     }
708 
709     private class ServiceCallbacksCompat implements ServiceCallbacks {
710         final Messenger mCallbacks;
711 
ServiceCallbacksCompat(Messenger callbacks)712         ServiceCallbacksCompat(Messenger callbacks) {
713             mCallbacks = callbacks;
714         }
715 
716         @Override
asBinder()717         public IBinder asBinder() {
718             return mCallbacks.getBinder();
719         }
720 
721         @Override
onConnect(String root, MediaSessionCompat.Token session, Bundle extras)722         public void onConnect(String root, MediaSessionCompat.Token session, Bundle extras)
723                 throws RemoteException {
724             if (extras == null) {
725                 extras = new Bundle();
726             }
727             extras.putInt(EXTRA_SERVICE_VERSION, SERVICE_VERSION_CURRENT);
728             Bundle data = new Bundle();
729             data.putString(DATA_MEDIA_ITEM_ID, root);
730             data.putParcelable(DATA_MEDIA_SESSION_TOKEN, session);
731             data.putBundle(DATA_ROOT_HINTS, extras);
732             sendRequest(SERVICE_MSG_ON_CONNECT, data);
733         }
734 
735         @Override
onConnectFailed()736         public void onConnectFailed() throws RemoteException {
737             sendRequest(SERVICE_MSG_ON_CONNECT_FAILED, null);
738         }
739 
740         @Override
onLoadChildren(String mediaId, List<MediaBrowserCompat.MediaItem> list, Bundle options)741         public void onLoadChildren(String mediaId, List<MediaBrowserCompat.MediaItem> list,
742                 Bundle options) throws RemoteException {
743             Bundle data = new Bundle();
744             data.putString(DATA_MEDIA_ITEM_ID, mediaId);
745             data.putBundle(DATA_OPTIONS, options);
746             if (list != null) {
747                 data.putParcelableArrayList(DATA_MEDIA_ITEM_LIST,
748                         list instanceof ArrayList ? (ArrayList) list : new ArrayList<>(list));
749             }
750             sendRequest(SERVICE_MSG_ON_LOAD_CHILDREN, data);
751         }
752 
sendRequest(int what, Bundle data)753         private void sendRequest(int what, Bundle data) throws RemoteException {
754             Message msg = Message.obtain();
755             msg.what = what;
756             msg.arg1 = SERVICE_VERSION_CURRENT;
757             msg.setData(data);
758             mCallbacks.send(msg);
759         }
760     }
761 
762     @Override
onCreate()763     public void onCreate() {
764         super.onCreate();
765         if (Build.VERSION.SDK_INT >= 24 || BuildCompat.isAtLeastN()) {
766             mImpl = new MediaBrowserServiceImplApi24();
767         } else if (Build.VERSION.SDK_INT >= 23) {
768             mImpl = new MediaBrowserServiceImplApi23();
769         } else if (Build.VERSION.SDK_INT >= 21) {
770             mImpl = new MediaBrowserServiceImplApi21();
771         } else {
772             mImpl = new MediaBrowserServiceImplBase();
773         }
774         mImpl.onCreate();
775     }
776 
777     @Override
onBind(Intent intent)778     public IBinder onBind(Intent intent) {
779         return mImpl.onBind(intent);
780     }
781 
782     @Override
dump(FileDescriptor fd, PrintWriter writer, String[] args)783     public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
784     }
785 
786     /**
787      * Called to get the root information for browsing by a particular client.
788      * <p>
789      * The implementation should verify that the client package has permission
790      * to access browse media information before returning the root id; it
791      * should return null if the client is not allowed to access this
792      * information.
793      * </p>
794      *
795      * @param clientPackageName The package name of the application which is
796      *            requesting access to browse media.
797      * @param clientUid The uid of the application which is requesting access to
798      *            browse media.
799      * @param rootHints An optional bundle of service-specific arguments to send
800      *            to the media browse service when connecting and retrieving the
801      *            root id for browsing, or null if none. The contents of this
802      *            bundle may affect the information returned when browsing.
803      * @return The {@link BrowserRoot} for accessing this app's content or null.
804      * @see BrowserRoot#EXTRA_RECENT
805      * @see BrowserRoot#EXTRA_OFFLINE
806      * @see BrowserRoot#EXTRA_SUGGESTED
807      * @see BrowserRoot#EXTRA_SUGGESTION_KEYWORDS
808      */
onGetRoot(@onNull String clientPackageName, int clientUid, @Nullable Bundle rootHints)809     public abstract @Nullable BrowserRoot onGetRoot(@NonNull String clientPackageName,
810             int clientUid, @Nullable Bundle rootHints);
811 
812     /**
813      * Called to get information about the children of a media item.
814      * <p>
815      * Implementations must call {@link Result#sendResult result.sendResult}
816      * with the list of children. If loading the children will be an expensive
817      * operation that should be performed on another thread,
818      * {@link Result#detach result.detach} may be called before returning from
819      * this function, and then {@link Result#sendResult result.sendResult}
820      * called when the loading is complete.
821      *
822      * @param parentId The id of the parent media item whose children are to be
823      *            queried.
824      * @param result The Result to send the list of children to, or null if the
825      *            id is invalid.
826      */
onLoadChildren(@onNull String parentId, @NonNull Result<List<MediaBrowserCompat.MediaItem>> result)827     public abstract void onLoadChildren(@NonNull String parentId,
828             @NonNull Result<List<MediaBrowserCompat.MediaItem>> result);
829 
830     /**
831      * Called to get information about the children of a media item.
832      * <p>
833      * Implementations must call {@link Result#sendResult result.sendResult}
834      * with the list of children. If loading the children will be an expensive
835      * operation that should be performed on another thread,
836      * {@link Result#detach result.detach} may be called before returning from
837      * this function, and then {@link Result#sendResult result.sendResult}
838      * called when the loading is complete.
839      *
840      * @param parentId The id of the parent media item whose children are to be
841      *            queried.
842      * @param result The Result to send the list of children to, or null if the
843      *            id is invalid.
844      * @param options A bundle of service-specific arguments sent from the media
845      *            browse. The information returned through the result should be
846      *            affected by the contents of this bundle.
847      */
onLoadChildren(@onNull String parentId, @NonNull Result<List<MediaBrowserCompat.MediaItem>> result, @NonNull Bundle options)848     public void onLoadChildren(@NonNull String parentId,
849             @NonNull Result<List<MediaBrowserCompat.MediaItem>> result, @NonNull Bundle options) {
850         // To support backward compatibility, when the implementation of MediaBrowserService doesn't
851         // override onLoadChildren() with options, onLoadChildren() without options will be used
852         // instead, and the options will be applied in the implementation of result.onResultSent().
853         result.setFlags(RESULT_FLAG_OPTION_NOT_HANDLED);
854         onLoadChildren(parentId, result);
855     }
856 
857     /**
858      * Called to get information about a specific media item.
859      * <p>
860      * Implementations must call {@link Result#sendResult result.sendResult}. If
861      * loading the item will be an expensive operation {@link Result#detach
862      * result.detach} may be called before returning from this function, and
863      * then {@link Result#sendResult result.sendResult} called when the item has
864      * been loaded.
865      * <p>
866      * The default implementation sends a null result.
867      *
868      * @param itemId The id for the specific {@link MediaBrowserCompat.MediaItem}.
869      * @param result The Result to send the item to, or null if the id is
870      *            invalid.
871      */
onLoadItem(String itemId, Result<MediaBrowserCompat.MediaItem> result)872     public void onLoadItem(String itemId, Result<MediaBrowserCompat.MediaItem> result) {
873         result.sendResult(null);
874     }
875 
876     /**
877      * Call to set the media session.
878      * <p>
879      * This should be called as soon as possible during the service's startup.
880      * It may only be called once.
881      *
882      * @param token The token for the service's {@link MediaSessionCompat}.
883      */
setSessionToken(MediaSessionCompat.Token token)884     public void setSessionToken(MediaSessionCompat.Token token) {
885         if (token == null) {
886             throw new IllegalArgumentException("Session token may not be null.");
887         }
888         if (mSession != null) {
889             throw new IllegalStateException("The session token has already been set.");
890         }
891         mSession = token;
892         mImpl.setSessionToken(token);
893     }
894 
895     /**
896      * Gets the session token, or null if it has not yet been created
897      * or if it has been destroyed.
898      */
getSessionToken()899     public @Nullable MediaSessionCompat.Token getSessionToken() {
900         return mSession;
901     }
902 
903     /**
904      * Gets the root hints sent from the currently connected {@link MediaBrowserCompat}.
905      * The root hints are service-specific arguments included in an optional bundle sent to the
906      * media browser service when connecting and retrieving the root id for browsing, or null if
907      * none. The contents of this bundle may affect the information returned when browsing.
908      * <p>
909      * Note that this will return null when connected to {@link android.media.browse.MediaBrowser}
910      * and running on API 23 or lower.
911      *
912      * @throws IllegalStateException If this method is called outside of {@link #onLoadChildren}
913      *             or {@link #onLoadItem}
914      * @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_RECENT
915      * @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_OFFLINE
916      * @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_SUGGESTED
917      * @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_SUGGESTION_KEYWORDS
918      */
getBrowserRootHints()919     public final Bundle getBrowserRootHints() {
920         return mImpl.getBrowserRootHints();
921     }
922 
923     /**
924      * Notifies all connected media browsers that the children of
925      * the specified parent id have changed in some way.
926      * This will cause browsers to fetch subscribed content again.
927      *
928      * @param parentId The id of the parent media item whose
929      * children changed.
930      */
notifyChildrenChanged(@onNull String parentId)931     public void notifyChildrenChanged(@NonNull String parentId) {
932         if (parentId == null) {
933             throw new IllegalArgumentException("parentId cannot be null in notifyChildrenChanged");
934         }
935         mImpl.notifyChildrenChanged(parentId, null);
936     }
937 
938     /**
939      * Notifies all connected media browsers that the children of
940      * the specified parent id have changed in some way.
941      * This will cause browsers to fetch subscribed content again.
942      *
943      * @param parentId The id of the parent media item whose
944      *            children changed.
945      * @param options A bundle of service-specific arguments to send
946      *            to the media browse. The contents of this bundle may
947      *            contain the information about the change.
948      */
notifyChildrenChanged(@onNull String parentId, @NonNull Bundle options)949     public void notifyChildrenChanged(@NonNull String parentId, @NonNull Bundle options) {
950         if (parentId == null) {
951             throw new IllegalArgumentException("parentId cannot be null in notifyChildrenChanged");
952         }
953         if (options == null) {
954             throw new IllegalArgumentException("options cannot be null in notifyChildrenChanged");
955         }
956         mImpl.notifyChildrenChanged(parentId, options);
957     }
958 
959     /**
960      * Return whether the given package is one of the ones that is owned by the uid.
961      */
isValidPackage(String pkg, int uid)962     private boolean isValidPackage(String pkg, int uid) {
963         if (pkg == null) {
964             return false;
965         }
966         final PackageManager pm = getPackageManager();
967         final String[] packages = pm.getPackagesForUid(uid);
968         final int N = packages.length;
969         for (int i=0; i<N; i++) {
970             if (packages[i].equals(pkg)) {
971                 return true;
972             }
973         }
974         return false;
975     }
976 
977     /**
978      * Save the subscription and if it is a new subscription send the results.
979      */
addSubscription(String id, ConnectionRecord connection, IBinder token, Bundle options)980     private void addSubscription(String id, ConnectionRecord connection, IBinder token,
981             Bundle options) {
982         // Save the subscription
983         List<Pair<IBinder, Bundle>> callbackList = connection.subscriptions.get(id);
984         if (callbackList == null) {
985             callbackList = new ArrayList<>();
986         }
987         for (Pair<IBinder, Bundle> callback : callbackList) {
988             if (token == callback.first
989                     && MediaBrowserCompatUtils.areSameOptions(options, callback.second)) {
990                 return;
991             }
992         }
993         callbackList.add(new Pair<>(token, options));
994         connection.subscriptions.put(id, callbackList);
995         // send the results
996         performLoadChildren(id, connection, options);
997     }
998 
999     /**
1000      * Remove the subscription.
1001      */
removeSubscription(String id, ConnectionRecord connection, IBinder token)1002     private boolean removeSubscription(String id, ConnectionRecord connection, IBinder token) {
1003         if (token == null) {
1004             return connection.subscriptions.remove(id) != null;
1005         }
1006         boolean removed = false;
1007         List<Pair<IBinder, Bundle>> callbackList = connection.subscriptions.get(id);
1008         if (callbackList != null) {
1009             Iterator<Pair<IBinder, Bundle>> iter = callbackList.iterator();
1010             while (iter.hasNext()){
1011                 if (token == iter.next().first) {
1012                     removed = true;
1013                     iter.remove();
1014                 }
1015             }
1016             if (callbackList.size() == 0) {
1017                 connection.subscriptions.remove(id);
1018             }
1019         }
1020         return removed;
1021     }
1022 
1023     /**
1024      * Call onLoadChildren and then send the results back to the connection.
1025      * <p>
1026      * Callers must make sure that this connection is still connected.
1027      */
performLoadChildren(final String parentId, final ConnectionRecord connection, final Bundle options)1028     private void performLoadChildren(final String parentId, final ConnectionRecord connection,
1029             final Bundle options) {
1030         final Result<List<MediaBrowserCompat.MediaItem>> result
1031                 = new Result<List<MediaBrowserCompat.MediaItem>>(parentId) {
1032             @Override
1033             void onResultSent(List<MediaBrowserCompat.MediaItem> list, @ResultFlags int flags) {
1034                 if (mConnections.get(connection.callbacks.asBinder()) != connection) {
1035                     if (DEBUG) {
1036                         Log.d(TAG, "Not sending onLoadChildren result for connection that has"
1037                                 + " been disconnected. pkg=" + connection.pkg + " id=" + parentId);
1038                     }
1039                     return;
1040                 }
1041 
1042                 List<MediaBrowserCompat.MediaItem> filteredList =
1043                         (flags & RESULT_FLAG_OPTION_NOT_HANDLED) != 0
1044                                 ? applyOptions(list, options) : list;
1045                 try {
1046                     connection.callbacks.onLoadChildren(parentId, filteredList, options);
1047                 } catch (RemoteException ex) {
1048                     // The other side is in the process of crashing.
1049                     Log.w(TAG, "Calling onLoadChildren() failed for id=" + parentId
1050                             + " package=" + connection.pkg);
1051                 }
1052             }
1053         };
1054 
1055         mCurConnection = connection;
1056         if (options == null) {
1057             onLoadChildren(parentId, result);
1058         } else {
1059             onLoadChildren(parentId, result, options);
1060         }
1061         mCurConnection = null;
1062 
1063         if (!result.isDone()) {
1064             throw new IllegalStateException("onLoadChildren must call detach() or sendResult()"
1065                     + " before returning for package=" + connection.pkg + " id=" + parentId);
1066         }
1067     }
1068 
applyOptions(List<MediaBrowserCompat.MediaItem> list, final Bundle options)1069     private List<MediaBrowserCompat.MediaItem> applyOptions(List<MediaBrowserCompat.MediaItem> list,
1070             final Bundle options) {
1071         if (list == null) {
1072             return null;
1073         }
1074         int page = options.getInt(MediaBrowserCompat.EXTRA_PAGE, -1);
1075         int pageSize = options.getInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, -1);
1076         if (page == -1 && pageSize == -1) {
1077             return list;
1078         }
1079         int fromIndex = pageSize * page;
1080         int toIndex = fromIndex + pageSize;
1081         if (page < 0 || pageSize < 1 || fromIndex >= list.size()) {
1082             return Collections.EMPTY_LIST;
1083         }
1084         if (toIndex > list.size()) {
1085             toIndex = list.size();
1086         }
1087         return list.subList(fromIndex, toIndex);
1088     }
1089 
performLoadItem(String itemId, ConnectionRecord connection, final ResultReceiver receiver)1090     private void performLoadItem(String itemId, ConnectionRecord connection,
1091             final ResultReceiver receiver) {
1092         final Result<MediaBrowserCompat.MediaItem> result =
1093                 new Result<MediaBrowserCompat.MediaItem>(itemId) {
1094                     @Override
1095                     void onResultSent(MediaBrowserCompat.MediaItem item, @ResultFlags int flags) {
1096                         Bundle bundle = new Bundle();
1097                         bundle.putParcelable(KEY_MEDIA_ITEM, item);
1098                         receiver.send(0, bundle);
1099                     }
1100                 };
1101 
1102         mCurConnection = connection;
1103         onLoadItem(itemId, result);
1104         mCurConnection = null;
1105 
1106         if (!result.isDone()) {
1107             throw new IllegalStateException("onLoadItem must call detach() or sendResult()"
1108                     + " before returning for id=" + itemId);
1109         }
1110     }
1111 
1112     /**
1113      * Contains information that the browser service needs to send to the client
1114      * when first connected.
1115      */
1116     public static final class BrowserRoot {
1117         /**
1118          * The lookup key for a boolean that indicates whether the browser service should return a
1119          * browser root for recently played media items.
1120          *
1121          * <p>When creating a media browser for a given media browser service, this key can be
1122          * supplied as a root hint for retrieving media items that are recently played.
1123          * If the media browser service can provide such media items, the implementation must return
1124          * the key in the root hint when {@link #onGetRoot(String, int, Bundle)} is called back.
1125          *
1126          * <p>The root hint may contain multiple keys.
1127          *
1128          * @see #EXTRA_OFFLINE
1129          * @see #EXTRA_SUGGESTED
1130          * @see #EXTRA_SUGGESTION_KEYWORDS
1131          */
1132         public static final String EXTRA_RECENT = "android.service.media.extra.RECENT";
1133 
1134         /**
1135          * The lookup key for a boolean that indicates whether the browser service should return a
1136          * browser root for offline media items.
1137          *
1138          * <p>When creating a media browser for a given media browser service, this key can be
1139          * supplied as a root hint for retrieving media items that are can be played without an
1140          * internet connection.
1141          * If the media browser service can provide such media items, the implementation must return
1142          * the key in the root hint when {@link #onGetRoot(String, int, Bundle)} is called back.
1143          *
1144          * <p>The root hint may contain multiple keys.
1145          *
1146          * @see #EXTRA_RECENT
1147          * @see #EXTRA_SUGGESTED
1148          * @see #EXTRA_SUGGESTION_KEYWORDS
1149          */
1150         public static final String EXTRA_OFFLINE = "android.service.media.extra.OFFLINE";
1151 
1152         /**
1153          * The lookup key for a boolean that indicates whether the browser service should return a
1154          * browser root for suggested media items.
1155          *
1156          * <p>When creating a media browser for a given media browser service, this key can be
1157          * supplied as a root hint for retrieving the media items suggested by the media browser
1158          * service. The list of media items passed in {@link android.support.v4.media.MediaBrowserCompat.SubscriptionCallback#onChildrenLoaded(String, List)}
1159          * is considered ordered by relevance, first being the top suggestion.
1160          * If the media browser service can provide such media items, the implementation must return
1161          * the key in the root hint when {@link #onGetRoot(String, int, Bundle)} is called back.
1162          *
1163          * <p>The root hint may contain multiple keys.
1164          *
1165          * @see #EXTRA_RECENT
1166          * @see #EXTRA_OFFLINE
1167          * @see #EXTRA_SUGGESTION_KEYWORDS
1168          */
1169         public static final String EXTRA_SUGGESTED = "android.service.media.extra.SUGGESTED";
1170 
1171         /**
1172          * The lookup key for a string that indicates specific keywords which will be considered
1173          * when the browser service suggests media items.
1174          *
1175          * <p>When creating a media browser for a given media browser service, this key can be
1176          * supplied as a root hint together with {@link #EXTRA_SUGGESTED} for retrieving suggested
1177          * media items related with the keywords. The list of media items passed in
1178          * {@link android.media.browse.MediaBrowser.SubscriptionCallback#onChildrenLoaded(String, List)}
1179          * is considered ordered by relevance, first being the top suggestion.
1180          * If the media browser service can provide such media items, the implementation must return
1181          * the key in the root hint when {@link #onGetRoot(String, int, Bundle)} is called back.
1182          *
1183          * <p>The root hint may contain multiple keys.
1184          *
1185          * @see #EXTRA_RECENT
1186          * @see #EXTRA_OFFLINE
1187          * @see #EXTRA_SUGGESTED
1188          */
1189         public static final String EXTRA_SUGGESTION_KEYWORDS
1190                 = "android.service.media.extra.SUGGESTION_KEYWORDS";
1191 
1192         final private String mRootId;
1193         final private Bundle mExtras;
1194 
1195         /**
1196          * Constructs a browser root.
1197          * @param rootId The root id for browsing.
1198          * @param extras Any extras about the browser service.
1199          */
BrowserRoot(@onNull String rootId, @Nullable Bundle extras)1200         public BrowserRoot(@NonNull String rootId, @Nullable Bundle extras) {
1201             if (rootId == null) {
1202                 throw new IllegalArgumentException("The root id in BrowserRoot cannot be null. " +
1203                         "Use null for BrowserRoot instead.");
1204             }
1205             mRootId = rootId;
1206             mExtras = extras;
1207         }
1208 
1209         /**
1210          * Gets the root id for browsing.
1211          */
getRootId()1212         public String getRootId() {
1213             return mRootId;
1214         }
1215 
1216         /**
1217          * Gets any extras about the browser service.
1218          */
getExtras()1219         public Bundle getExtras() {
1220             return mExtras;
1221         }
1222     }
1223 }
1224