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