1/* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package android.service.media; 18 19import android.annotation.IntDef; 20import android.annotation.NonNull; 21import android.annotation.Nullable; 22import android.annotation.SdkConstant; 23import android.annotation.SdkConstant.SdkConstantType; 24import android.app.Service; 25import android.content.Intent; 26import android.content.pm.PackageManager; 27import android.content.pm.ParceledListSlice; 28import android.media.browse.MediaBrowser; 29import android.media.browse.MediaBrowserUtils; 30import android.media.session.MediaSession; 31import android.os.Binder; 32import android.os.Bundle; 33import android.os.Handler; 34import android.os.IBinder; 35import android.os.RemoteException; 36import android.os.ResultReceiver; 37import android.service.media.IMediaBrowserService; 38import android.service.media.IMediaBrowserServiceCallbacks; 39import android.text.TextUtils; 40import android.util.ArrayMap; 41import android.util.Log; 42import android.util.Pair; 43 44import java.io.FileDescriptor; 45import java.io.PrintWriter; 46import java.lang.annotation.Retention; 47import java.lang.annotation.RetentionPolicy; 48import java.util.ArrayList; 49import java.util.Collections; 50import java.util.HashMap; 51import java.util.List; 52 53/** 54 * Base class for media browse services. 55 * <p> 56 * Media browse services enable applications to browse media content provided by an application 57 * and ask the application to start playing it. They may also be used to control content that 58 * is already playing by way of a {@link MediaSession}. 59 * </p> 60 * 61 * To extend this class, you must declare the service in your manifest file with 62 * an intent filter with the {@link #SERVICE_INTERFACE} action. 63 * 64 * For example: 65 * </p><pre> 66 * <service android:name=".MyMediaBrowserService" 67 * android:label="@string/service_name" > 68 * <intent-filter> 69 * <action android:name="android.media.browse.MediaBrowserService" /> 70 * </intent-filter> 71 * </service> 72 * </pre> 73 * 74 */ 75public abstract class MediaBrowserService extends Service { 76 private static final String TAG = "MediaBrowserService"; 77 private static final boolean DBG = false; 78 79 /** 80 * The {@link Intent} that must be declared as handled by the service. 81 */ 82 @SdkConstant(SdkConstantType.SERVICE_ACTION) 83 public static final String SERVICE_INTERFACE = "android.media.browse.MediaBrowserService"; 84 85 /** 86 * A key for passing the MediaItem to the ResultReceiver in getItem. 87 * 88 * @hide 89 */ 90 public static final String KEY_MEDIA_ITEM = "media_item"; 91 92 private static final int RESULT_FLAG_OPTION_NOT_HANDLED = 0x00000001; 93 94 /** @hide */ 95 @Retention(RetentionPolicy.SOURCE) 96 @IntDef(flag=true, value = { RESULT_FLAG_OPTION_NOT_HANDLED }) 97 private @interface ResultFlags { } 98 99 private final ArrayMap<IBinder, ConnectionRecord> mConnections = new ArrayMap<>(); 100 private ConnectionRecord mCurConnection; 101 private final Handler mHandler = new Handler(); 102 private ServiceBinder mBinder; 103 MediaSession.Token mSession; 104 105 /** 106 * All the info about a connection. 107 */ 108 private class ConnectionRecord { 109 String pkg; 110 Bundle rootHints; 111 IMediaBrowserServiceCallbacks callbacks; 112 BrowserRoot root; 113 HashMap<String, List<Pair<IBinder, Bundle>>> subscriptions = new HashMap<>(); 114 } 115 116 /** 117 * Completion handler for asynchronous callback methods in {@link MediaBrowserService}. 118 * <p> 119 * Each of the methods that takes one of these to send the result must call 120 * {@link #sendResult} to respond to the caller with the given results. If those 121 * functions return without calling {@link #sendResult}, they must instead call 122 * {@link #detach} before returning, and then may call {@link #sendResult} when 123 * they are done. If more than one of those methods is called, an exception will 124 * be thrown. 125 * 126 * @see #onLoadChildren 127 * @see #onLoadItem 128 */ 129 public class Result<T> { 130 private Object mDebug; 131 private boolean mDetachCalled; 132 private boolean mSendResultCalled; 133 private int mFlags; 134 135 Result(Object debug) { 136 mDebug = debug; 137 } 138 139 /** 140 * Send the result back to the caller. 141 */ 142 public void sendResult(T result) { 143 if (mSendResultCalled) { 144 throw new IllegalStateException("sendResult() called twice for: " + mDebug); 145 } 146 mSendResultCalled = true; 147 onResultSent(result, mFlags); 148 } 149 150 /** 151 * Detach this message from the current thread and allow the {@link #sendResult} 152 * call to happen later. 153 */ 154 public void detach() { 155 if (mDetachCalled) { 156 throw new IllegalStateException("detach() called when detach() had already" 157 + " been called for: " + mDebug); 158 } 159 if (mSendResultCalled) { 160 throw new IllegalStateException("detach() called when sendResult() had already" 161 + " been called for: " + mDebug); 162 } 163 mDetachCalled = true; 164 } 165 166 boolean isDone() { 167 return mDetachCalled || mSendResultCalled; 168 } 169 170 void setFlags(@ResultFlags int flags) { 171 mFlags = flags; 172 } 173 174 /** 175 * Called when the result is sent, after assertions about not being called twice 176 * have happened. 177 */ 178 void onResultSent(T result, @ResultFlags int flags) { 179 } 180 } 181 182 private class ServiceBinder extends IMediaBrowserService.Stub { 183 @Override 184 public void connect(final String pkg, final Bundle rootHints, 185 final IMediaBrowserServiceCallbacks callbacks) { 186 187 final int uid = Binder.getCallingUid(); 188 if (!isValidPackage(pkg, uid)) { 189 throw new IllegalArgumentException("Package/uid mismatch: uid=" + uid 190 + " package=" + pkg); 191 } 192 193 mHandler.post(new Runnable() { 194 @Override 195 public void run() { 196 final IBinder b = callbacks.asBinder(); 197 198 // Clear out the old subscriptions. We are getting new ones. 199 mConnections.remove(b); 200 201 final ConnectionRecord connection = new ConnectionRecord(); 202 connection.pkg = pkg; 203 connection.rootHints = rootHints; 204 connection.callbacks = callbacks; 205 206 connection.root = MediaBrowserService.this.onGetRoot(pkg, uid, rootHints); 207 208 // If they didn't return something, don't allow this client. 209 if (connection.root == null) { 210 Log.i(TAG, "No root for client " + pkg + " from service " 211 + getClass().getName()); 212 try { 213 callbacks.onConnectFailed(); 214 } catch (RemoteException ex) { 215 Log.w(TAG, "Calling onConnectFailed() failed. Ignoring. " 216 + "pkg=" + pkg); 217 } 218 } else { 219 try { 220 mConnections.put(b, connection); 221 if (mSession != null) { 222 callbacks.onConnect(connection.root.getRootId(), 223 mSession, connection.root.getExtras()); 224 } 225 } catch (RemoteException ex) { 226 Log.w(TAG, "Calling onConnect() failed. Dropping client. " 227 + "pkg=" + pkg); 228 mConnections.remove(b); 229 } 230 } 231 } 232 }); 233 } 234 235 @Override 236 public void disconnect(final IMediaBrowserServiceCallbacks callbacks) { 237 mHandler.post(new Runnable() { 238 @Override 239 public void run() { 240 final IBinder b = callbacks.asBinder(); 241 242 // Clear out the old subscriptions. We are getting new ones. 243 final ConnectionRecord old = mConnections.remove(b); 244 if (old != null) { 245 // TODO 246 } 247 } 248 }); 249 } 250 251 @Override 252 public void addSubscriptionDeprecated(String id, IMediaBrowserServiceCallbacks callbacks) { 253 // do-nothing 254 } 255 256 @Override 257 public void addSubscription(final String id, final IBinder token, final Bundle options, 258 final IMediaBrowserServiceCallbacks callbacks) { 259 mHandler.post(new Runnable() { 260 @Override 261 public void run() { 262 final IBinder b = callbacks.asBinder(); 263 264 // Get the record for the connection 265 final ConnectionRecord connection = mConnections.get(b); 266 if (connection == null) { 267 Log.w(TAG, "addSubscription for callback that isn't registered id=" 268 + id); 269 return; 270 } 271 272 MediaBrowserService.this.addSubscription(id, connection, token, options); 273 } 274 }); 275 } 276 277 @Override 278 public void removeSubscriptionDeprecated(String id, IMediaBrowserServiceCallbacks callbacks) { 279 // do-nothing 280 } 281 282 @Override 283 public void removeSubscription(final String id, final IBinder token, 284 final IMediaBrowserServiceCallbacks callbacks) { 285 mHandler.post(new Runnable() { 286 @Override 287 public void run() { 288 final IBinder b = callbacks.asBinder(); 289 290 ConnectionRecord connection = mConnections.get(b); 291 if (connection == null) { 292 Log.w(TAG, "removeSubscription for callback that isn't registered id=" 293 + id); 294 return; 295 } 296 if (!MediaBrowserService.this.removeSubscription(id, connection, token)) { 297 Log.w(TAG, "removeSubscription called for " + id 298 + " which is not subscribed"); 299 } 300 } 301 }); 302 } 303 304 @Override 305 public void getMediaItem(final String mediaId, final ResultReceiver receiver, 306 final IMediaBrowserServiceCallbacks callbacks) { 307 if (TextUtils.isEmpty(mediaId) || receiver == null) { 308 return; 309 } 310 311 mHandler.post(new Runnable() { 312 @Override 313 public void run() { 314 final IBinder b = callbacks.asBinder(); 315 ConnectionRecord connection = mConnections.get(b); 316 if (connection == null) { 317 Log.w(TAG, "getMediaItem for callback that isn't registered id=" + mediaId); 318 return; 319 } 320 performLoadItem(mediaId, connection, receiver); 321 } 322 }); 323 } 324 } 325 326 @Override 327 public void onCreate() { 328 super.onCreate(); 329 mBinder = new ServiceBinder(); 330 } 331 332 @Override 333 public IBinder onBind(Intent intent) { 334 if (SERVICE_INTERFACE.equals(intent.getAction())) { 335 return mBinder; 336 } 337 return null; 338 } 339 340 @Override 341 public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { 342 } 343 344 /** 345 * Called to get the root information for browsing by a particular client. 346 * <p> 347 * The implementation should verify that the client package has permission 348 * to access browse media information before returning the root id; it 349 * should return null if the client is not allowed to access this 350 * information. 351 * </p> 352 * 353 * @param clientPackageName The package name of the application which is 354 * requesting access to browse media. 355 * @param clientUid The uid of the application which is requesting access to 356 * browse media. 357 * @param rootHints An optional bundle of service-specific arguments to send 358 * to the media browse service when connecting and retrieving the 359 * root id for browsing, or null if none. The contents of this 360 * bundle may affect the information returned when browsing. 361 * @return The {@link BrowserRoot} for accessing this app's content or null. 362 * @see BrowserRoot#EXTRA_RECENT 363 * @see BrowserRoot#EXTRA_OFFLINE 364 * @see BrowserRoot#EXTRA_SUGGESTED 365 */ 366 public abstract @Nullable BrowserRoot onGetRoot(@NonNull String clientPackageName, 367 int clientUid, @Nullable Bundle rootHints); 368 369 /** 370 * Called to get information about the children of a media item. 371 * <p> 372 * Implementations must call {@link Result#sendResult result.sendResult} 373 * with the list of children. If loading the children will be an expensive 374 * operation that should be performed on another thread, 375 * {@link Result#detach result.detach} may be called before returning from 376 * this function, and then {@link Result#sendResult result.sendResult} 377 * called when the loading is complete. 378 * </p><p> 379 * In case the media item does not have any children, call {@link Result#sendResult} 380 * with an empty list. When the given {@code parentId} is invalid, implementations must 381 * call {@link Result#sendResult result.sendResult} with {@code null}, which will invoke 382 * {@link MediaBrowser.SubscriptionCallback#onError}. 383 * </p> 384 * 385 * @param parentId The id of the parent media item whose children are to be 386 * queried. 387 * @param result The Result to send the list of children to. 388 */ 389 public abstract void onLoadChildren(@NonNull String parentId, 390 @NonNull Result<List<MediaBrowser.MediaItem>> result); 391 392 /** 393 * Called to get information about the children of a media item. 394 * <p> 395 * Implementations must call {@link Result#sendResult result.sendResult} 396 * with the list of children. If loading the children will be an expensive 397 * operation that should be performed on another thread, 398 * {@link Result#detach result.detach} may be called before returning from 399 * this function, and then {@link Result#sendResult result.sendResult} 400 * called when the loading is complete. 401 * </p><p> 402 * In case the media item does not have any children, call {@link Result#sendResult} 403 * with an empty list. When the given {@code parentId} is invalid, implementations must 404 * call {@link Result#sendResult result.sendResult} with {@code null}, which will invoke 405 * {@link MediaBrowser.SubscriptionCallback#onError}. 406 * </p> 407 * 408 * @param parentId The id of the parent media item whose children are to be 409 * queried. 410 * @param result The Result to send the list of children to. 411 * @param options A bundle of service-specific arguments sent from the media 412 * browse. The information returned through the result should be 413 * affected by the contents of this bundle. 414 */ 415 public void onLoadChildren(@NonNull String parentId, 416 @NonNull Result<List<MediaBrowser.MediaItem>> result, @NonNull Bundle options) { 417 // To support backward compatibility, when the implementation of MediaBrowserService doesn't 418 // override onLoadChildren() with options, onLoadChildren() without options will be used 419 // instead, and the options will be applied in the implementation of result.onResultSent(). 420 result.setFlags(RESULT_FLAG_OPTION_NOT_HANDLED); 421 onLoadChildren(parentId, result); 422 } 423 424 /** 425 * Called to get information about a specific media item. 426 * <p> 427 * Implementations must call {@link Result#sendResult result.sendResult}. If 428 * loading the item will be an expensive operation {@link Result#detach 429 * result.detach} may be called before returning from this function, and 430 * then {@link Result#sendResult result.sendResult} called when the item has 431 * been loaded. 432 * </p><p> 433 * When the given {@code itemId} is invalid, implementations must call 434 * {@link Result#sendResult result.sendResult} with {@code null}, which will 435 * invoke {@link MediaBrowser.ItemCallback#onError}. 436 * </p><p> 437 * The default implementation calls {@link Result#sendResult result.sendResult} 438 * with {@code null}. 439 * </p> 440 * 441 * @param itemId The id for the specific 442 * {@link android.media.browse.MediaBrowser.MediaItem}. 443 * @param result The Result to send the item to. 444 */ 445 public void onLoadItem(String itemId, Result<MediaBrowser.MediaItem> result) { 446 result.sendResult(null); 447 } 448 449 /** 450 * Call to set the media session. 451 * <p> 452 * This should be called as soon as possible during the service's startup. 453 * It may only be called once. 454 * 455 * @param token The token for the service's {@link MediaSession}. 456 */ 457 public void setSessionToken(final MediaSession.Token token) { 458 if (token == null) { 459 throw new IllegalArgumentException("Session token may not be null."); 460 } 461 if (mSession != null) { 462 throw new IllegalStateException("The session token has already been set."); 463 } 464 mSession = token; 465 mHandler.post(new Runnable() { 466 @Override 467 public void run() { 468 for (IBinder key : mConnections.keySet()) { 469 ConnectionRecord connection = mConnections.get(key); 470 try { 471 connection.callbacks.onConnect(connection.root.getRootId(), token, 472 connection.root.getExtras()); 473 } catch (RemoteException e) { 474 Log.w(TAG, "Connection for " + connection.pkg + " is no longer valid."); 475 mConnections.remove(key); 476 } 477 } 478 } 479 }); 480 } 481 482 /** 483 * Gets the session token, or null if it has not yet been created 484 * or if it has been destroyed. 485 */ 486 public @Nullable MediaSession.Token getSessionToken() { 487 return mSession; 488 } 489 490 /** 491 * Gets the root hints sent from the currently connected {@link MediaBrowser}. 492 * The root hints are service-specific arguments included in an optional bundle sent to the 493 * media browser service when connecting and retrieving the root id for browsing, or null if 494 * none. The contents of this bundle may affect the information returned when browsing. 495 * 496 * @throws IllegalStateException If this method is called outside of {@link #onLoadChildren} 497 * or {@link #onLoadItem} 498 * @see MediaBrowserService.BrowserRoot#EXTRA_RECENT 499 * @see MediaBrowserService.BrowserRoot#EXTRA_OFFLINE 500 * @see MediaBrowserService.BrowserRoot#EXTRA_SUGGESTED 501 */ 502 public final Bundle getBrowserRootHints() { 503 if (mCurConnection == null) { 504 throw new IllegalStateException("This should be called inside of onLoadChildren or" 505 + " onLoadItem methods"); 506 } 507 return mCurConnection.rootHints == null ? null : new Bundle(mCurConnection.rootHints); 508 } 509 510 /** 511 * Notifies all connected media browsers that the children of 512 * the specified parent id have changed in some way. 513 * This will cause browsers to fetch subscribed content again. 514 * 515 * @param parentId The id of the parent media item whose 516 * children changed. 517 */ 518 public void notifyChildrenChanged(@NonNull String parentId) { 519 notifyChildrenChangedInternal(parentId, null); 520 } 521 522 /** 523 * Notifies all connected media browsers that the children of 524 * the specified parent id have changed in some way. 525 * This will cause browsers to fetch subscribed content again. 526 * 527 * @param parentId The id of the parent media item whose 528 * children changed. 529 * @param options A bundle of service-specific arguments to send 530 * to the media browse. The contents of this bundle may 531 * contain the information about the change. 532 */ 533 public void notifyChildrenChanged(@NonNull String parentId, @NonNull Bundle options) { 534 if (options == null) { 535 throw new IllegalArgumentException("options cannot be null in notifyChildrenChanged"); 536 } 537 notifyChildrenChangedInternal(parentId, options); 538 } 539 540 private void notifyChildrenChangedInternal(final String parentId, final Bundle options) { 541 if (parentId == null) { 542 throw new IllegalArgumentException("parentId cannot be null in notifyChildrenChanged"); 543 } 544 mHandler.post(new Runnable() { 545 @Override 546 public void run() { 547 for (IBinder binder : mConnections.keySet()) { 548 ConnectionRecord connection = mConnections.get(binder); 549 List<Pair<IBinder, Bundle>> callbackList = 550 connection.subscriptions.get(parentId); 551 if (callbackList != null) { 552 for (Pair<IBinder, Bundle> callback : callbackList) { 553 if (MediaBrowserUtils.hasDuplicatedItems(options, callback.second)) { 554 performLoadChildren(parentId, connection, callback.second); 555 } 556 } 557 } 558 } 559 } 560 }); 561 } 562 563 /** 564 * Return whether the given package is one of the ones that is owned by the uid. 565 */ 566 private boolean isValidPackage(String pkg, int uid) { 567 if (pkg == null) { 568 return false; 569 } 570 final PackageManager pm = getPackageManager(); 571 final String[] packages = pm.getPackagesForUid(uid); 572 final int N = packages.length; 573 for (int i=0; i<N; i++) { 574 if (packages[i].equals(pkg)) { 575 return true; 576 } 577 } 578 return false; 579 } 580 581 /** 582 * Save the subscription and if it is a new subscription send the results. 583 */ 584 private void addSubscription(String id, ConnectionRecord connection, IBinder token, 585 Bundle options) { 586 // Save the subscription 587 List<Pair<IBinder, Bundle>> callbackList = connection.subscriptions.get(id); 588 if (callbackList == null) { 589 callbackList = new ArrayList<>(); 590 } 591 for (Pair<IBinder, Bundle> callback : callbackList) { 592 if (token == callback.first 593 && MediaBrowserUtils.areSameOptions(options, callback.second)) { 594 return; 595 } 596 } 597 callbackList.add(new Pair<>(token, options)); 598 connection.subscriptions.put(id, callbackList); 599 // send the results 600 performLoadChildren(id, connection, options); 601 } 602 603 /** 604 * Remove the subscription. 605 */ 606 private boolean removeSubscription(String id, ConnectionRecord connection, IBinder token) { 607 if (token == null) { 608 return connection.subscriptions.remove(id) != null; 609 } 610 boolean removed = false; 611 List<Pair<IBinder, Bundle>> callbackList = connection.subscriptions.get(id); 612 if (callbackList != null) { 613 for (Pair<IBinder, Bundle> callback : callbackList) { 614 if (token == callback.first) { 615 removed = true; 616 callbackList.remove(callback); 617 } 618 } 619 if (callbackList.size() == 0) { 620 connection.subscriptions.remove(id); 621 } 622 } 623 return removed; 624 } 625 626 /** 627 * Call onLoadChildren and then send the results back to the connection. 628 * <p> 629 * Callers must make sure that this connection is still connected. 630 */ 631 private void performLoadChildren(final String parentId, final ConnectionRecord connection, 632 final Bundle options) { 633 final Result<List<MediaBrowser.MediaItem>> result 634 = new Result<List<MediaBrowser.MediaItem>>(parentId) { 635 @Override 636 void onResultSent(List<MediaBrowser.MediaItem> list, @ResultFlags int flag) { 637 if (mConnections.get(connection.callbacks.asBinder()) != connection) { 638 if (DBG) { 639 Log.d(TAG, "Not sending onLoadChildren result for connection that has" 640 + " been disconnected. pkg=" + connection.pkg + " id=" + parentId); 641 } 642 return; 643 } 644 645 List<MediaBrowser.MediaItem> filteredList = 646 (flag & RESULT_FLAG_OPTION_NOT_HANDLED) != 0 647 ? applyOptions(list, options) : list; 648 final ParceledListSlice<MediaBrowser.MediaItem> pls = 649 filteredList == null ? null : new ParceledListSlice<>(filteredList); 650 try { 651 connection.callbacks.onLoadChildrenWithOptions(parentId, pls, options); 652 } catch (RemoteException ex) { 653 // The other side is in the process of crashing. 654 Log.w(TAG, "Calling onLoadChildren() failed for id=" + parentId 655 + " package=" + connection.pkg); 656 } 657 } 658 }; 659 660 mCurConnection = connection; 661 if (options == null) { 662 onLoadChildren(parentId, result); 663 } else { 664 onLoadChildren(parentId, result, options); 665 } 666 mCurConnection = null; 667 668 if (!result.isDone()) { 669 throw new IllegalStateException("onLoadChildren must call detach() or sendResult()" 670 + " before returning for package=" + connection.pkg + " id=" + parentId); 671 } 672 } 673 674 private List<MediaBrowser.MediaItem> applyOptions(List<MediaBrowser.MediaItem> list, 675 final Bundle options) { 676 if (list == null) { 677 return null; 678 } 679 int page = options.getInt(MediaBrowser.EXTRA_PAGE, -1); 680 int pageSize = options.getInt(MediaBrowser.EXTRA_PAGE_SIZE, -1); 681 if (page == -1 && pageSize == -1) { 682 return list; 683 } 684 int fromIndex = pageSize * page; 685 int toIndex = fromIndex + pageSize; 686 if (page < 0 || pageSize < 1 || fromIndex >= list.size()) { 687 return Collections.EMPTY_LIST; 688 } 689 if (toIndex > list.size()) { 690 toIndex = list.size(); 691 } 692 return list.subList(fromIndex, toIndex); 693 } 694 695 private void performLoadItem(String itemId, final ConnectionRecord connection, 696 final ResultReceiver receiver) { 697 final Result<MediaBrowser.MediaItem> result = 698 new Result<MediaBrowser.MediaItem>(itemId) { 699 @Override 700 void onResultSent(MediaBrowser.MediaItem item, @ResultFlags int flag) { 701 Bundle bundle = new Bundle(); 702 bundle.putParcelable(KEY_MEDIA_ITEM, item); 703 receiver.send(0, bundle); 704 } 705 }; 706 707 mCurConnection = connection; 708 onLoadItem(itemId, result); 709 mCurConnection = null; 710 711 if (!result.isDone()) { 712 throw new IllegalStateException("onLoadItem must call detach() or sendResult()" 713 + " before returning for id=" + itemId); 714 } 715 } 716 717 /** 718 * Contains information that the browser service needs to send to the client 719 * when first connected. 720 */ 721 public static final class BrowserRoot { 722 /** 723 * The lookup key for a boolean that indicates whether the browser service should return a 724 * browser root for recently played media items. 725 * 726 * <p>When creating a media browser for a given media browser service, this key can be 727 * supplied as a root hint for retrieving media items that are recently played. 728 * If the media browser service can provide such media items, the implementation must return 729 * the key in the root hint when {@link #onGetRoot(String, int, Bundle)} is called back. 730 * 731 * <p>The root hint may contain multiple keys. 732 * 733 * @see #EXTRA_OFFLINE 734 * @see #EXTRA_SUGGESTED 735 */ 736 public static final String EXTRA_RECENT = "android.service.media.extra.RECENT"; 737 738 /** 739 * The lookup key for a boolean that indicates whether the browser service should return a 740 * browser root for offline media items. 741 * 742 * <p>When creating a media browser for a given media browser service, this key can be 743 * supplied as a root hint for retrieving media items that are can be played without an 744 * internet connection. 745 * If the media browser service can provide such media items, the implementation must return 746 * the key in the root hint when {@link #onGetRoot(String, int, Bundle)} is called back. 747 * 748 * <p>The root hint may contain multiple keys. 749 * 750 * @see #EXTRA_RECENT 751 * @see #EXTRA_SUGGESTED 752 */ 753 public static final String EXTRA_OFFLINE = "android.service.media.extra.OFFLINE"; 754 755 /** 756 * The lookup key for a boolean that indicates whether the browser service should return a 757 * browser root for suggested media items. 758 * 759 * <p>When creating a media browser for a given media browser service, this key can be 760 * supplied as a root hint for retrieving the media items suggested by the media browser 761 * service. The list of media items passed in {@link android.media.browse.MediaBrowser.SubscriptionCallback#onChildrenLoaded(String, List)} 762 * is considered ordered by relevance, first being the top suggestion. 763 * If the media browser service can provide such media items, the implementation must return 764 * the key in the root hint when {@link #onGetRoot(String, int, Bundle)} is called back. 765 * 766 * <p>The root hint may contain multiple keys. 767 * 768 * @see #EXTRA_RECENT 769 * @see #EXTRA_OFFLINE 770 */ 771 public static final String EXTRA_SUGGESTED = "android.service.media.extra.SUGGESTED"; 772 773 final private String mRootId; 774 final private Bundle mExtras; 775 776 /** 777 * Constructs a browser root. 778 * @param rootId The root id for browsing. 779 * @param extras Any extras about the browser service. 780 */ 781 public BrowserRoot(@NonNull String rootId, @Nullable Bundle extras) { 782 if (rootId == null) { 783 throw new IllegalArgumentException("The root id in BrowserRoot cannot be null. " + 784 "Use null for BrowserRoot instead."); 785 } 786 mRootId = rootId; 787 mExtras = extras; 788 } 789 790 /** 791 * Gets the root id for browsing. 792 */ 793 public String getRootId() { 794 return mRootId; 795 } 796 797 /** 798 * Gets any extras about the browser service. 799 */ 800 public Bundle getExtras() { 801 return mExtras; 802 } 803 } 804} 805