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