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