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 * <service android:name=".MyMediaBrowserServiceCompat" 91 * android:label="@string/service_name" > 92 * <intent-filter> 93 * <action android:name="android.media.browse.MediaBrowserService" /> 94 * </intent-filter> 95 * </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