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.content.Intent; 26 import android.content.pm.PackageManager; 27 import android.content.pm.ParceledListSlice; 28 import android.media.browse.MediaBrowser; 29 import android.media.browse.MediaBrowserUtils; 30 import android.media.session.MediaSession; 31 import android.os.Binder; 32 import android.os.Bundle; 33 import android.os.Handler; 34 import android.media.session.MediaSessionManager; 35 import android.media.session.MediaSessionManager.RemoteUserInfo; 36 import android.os.IBinder; 37 import android.os.RemoteException; 38 import android.os.ResultReceiver; 39 import android.service.media.IMediaBrowserService; 40 import android.service.media.IMediaBrowserServiceCallbacks; 41 import android.text.TextUtils; 42 import android.util.ArrayMap; 43 import android.util.Log; 44 import android.util.Pair; 45 46 import java.io.FileDescriptor; 47 import java.io.PrintWriter; 48 import java.lang.annotation.Retention; 49 import java.lang.annotation.RetentionPolicy; 50 import java.util.ArrayList; 51 import java.util.Collections; 52 import java.util.HashMap; 53 import java.util.Iterator; 54 import java.util.List; 55 56 /** 57 * Base class for media browser services. 58 * <p> 59 * Media browser services enable applications to browse media content provided by an application 60 * and ask the application to start playing it. They may also be used to control content that 61 * is already playing by way of a {@link MediaSession}. 62 * </p> 63 * 64 * To extend this class, you must declare the service in your manifest file with 65 * an intent filter with the {@link #SERVICE_INTERFACE} action. 66 * 67 * For example: 68 * </p><pre> 69 * <service android:name=".MyMediaBrowserService" 70 * android:label="@string/service_name" > 71 * <intent-filter> 72 * <action android:name="android.media.browse.MediaBrowserService" /> 73 * </intent-filter> 74 * </service> 75 * </pre> 76 * 77 */ 78 public abstract class MediaBrowserService extends Service { 79 private static final String TAG = "MediaBrowserService"; 80 private static final boolean DBG = false; 81 82 /** 83 * The {@link Intent} that must be declared as handled by the service. 84 */ 85 @SdkConstant(SdkConstantType.SERVICE_ACTION) 86 public static final String SERVICE_INTERFACE = "android.media.browse.MediaBrowserService"; 87 88 /** 89 * A key for passing the MediaItem to the ResultReceiver in getItem. 90 * @hide 91 */ 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 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(String id, IMediaBrowserServiceCallbacks callbacks) { 305 // do-nothing 306 } 307 308 @Override removeSubscription(final String id, final IBinder token, final IMediaBrowserServiceCallbacks callbacks)309 public void removeSubscription(final String id, final IBinder token, 310 final IMediaBrowserServiceCallbacks callbacks) { 311 mHandler.post(new Runnable() { 312 @Override 313 public void run() { 314 final IBinder b = callbacks.asBinder(); 315 316 ConnectionRecord connection = mConnections.get(b); 317 if (connection == null) { 318 Log.w(TAG, "removeSubscription for callback that isn't registered id=" 319 + id); 320 return; 321 } 322 if (!MediaBrowserService.this.removeSubscription(id, connection, token)) { 323 Log.w(TAG, "removeSubscription called for " + id 324 + " which is not subscribed"); 325 } 326 } 327 }); 328 } 329 330 @Override getMediaItem(final String mediaId, final ResultReceiver receiver, final IMediaBrowserServiceCallbacks callbacks)331 public void getMediaItem(final String mediaId, final ResultReceiver receiver, 332 final IMediaBrowserServiceCallbacks callbacks) { 333 mHandler.post(new Runnable() { 334 @Override 335 public void run() { 336 final IBinder b = callbacks.asBinder(); 337 ConnectionRecord connection = mConnections.get(b); 338 if (connection == null) { 339 Log.w(TAG, "getMediaItem for callback that isn't registered id=" + mediaId); 340 return; 341 } 342 performLoadItem(mediaId, connection, receiver); 343 } 344 }); 345 } 346 } 347 348 @Override onCreate()349 public void onCreate() { 350 super.onCreate(); 351 mBinder = new ServiceBinder(); 352 } 353 354 @Override onBind(Intent intent)355 public IBinder onBind(Intent intent) { 356 if (SERVICE_INTERFACE.equals(intent.getAction())) { 357 return mBinder; 358 } 359 return null; 360 } 361 362 @Override dump(FileDescriptor fd, PrintWriter writer, String[] args)363 public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { 364 } 365 366 /** 367 * Called to get the root information for browsing by a particular client. 368 * <p> 369 * The implementation should verify that the client package has permission 370 * to access browse media information before returning the root id; it 371 * should return null if the client is not allowed to access this 372 * information. 373 * </p> 374 * 375 * @param clientPackageName The package name of the application which is 376 * requesting access to browse media. 377 * @param clientUid The uid of the application which is requesting access to 378 * browse media. 379 * @param rootHints An optional bundle of service-specific arguments to send 380 * to the media browser service when connecting and retrieving the 381 * root id for browsing, or null if none. The contents of this 382 * bundle may affect the information returned when browsing. 383 * @return The {@link BrowserRoot} for accessing this app's content or null. 384 * @see BrowserRoot#EXTRA_RECENT 385 * @see BrowserRoot#EXTRA_OFFLINE 386 * @see BrowserRoot#EXTRA_SUGGESTED 387 */ onGetRoot(@onNull String clientPackageName, int clientUid, @Nullable Bundle rootHints)388 public abstract @Nullable BrowserRoot onGetRoot(@NonNull String clientPackageName, 389 int clientUid, @Nullable Bundle rootHints); 390 391 /** 392 * Called to get information about the children of a media item. 393 * <p> 394 * Implementations must call {@link Result#sendResult result.sendResult} 395 * with the list of children. If loading the children will be an expensive 396 * operation that should be performed on another thread, 397 * {@link Result#detach result.detach} may be called before returning from 398 * this function, and then {@link Result#sendResult result.sendResult} 399 * called when the loading is complete. 400 * </p><p> 401 * In case the media item does not have any children, call {@link Result#sendResult} 402 * with an empty list. When the given {@code parentId} is invalid, implementations must 403 * call {@link Result#sendResult result.sendResult} with {@code null}, which will invoke 404 * {@link MediaBrowser.SubscriptionCallback#onError}. 405 * </p> 406 * 407 * @param parentId The id of the parent media item whose children are to be 408 * queried. 409 * @param result The Result to send the list of children to. 410 */ onLoadChildren(@onNull String parentId, @NonNull Result<List<MediaBrowser.MediaItem>> result)411 public abstract void onLoadChildren(@NonNull String parentId, 412 @NonNull Result<List<MediaBrowser.MediaItem>> result); 413 414 /** 415 * Called to get information about the children of a media item. 416 * <p> 417 * Implementations must call {@link Result#sendResult result.sendResult} 418 * with the list of children. If loading the children will be an expensive 419 * operation that should be performed on another thread, 420 * {@link Result#detach result.detach} may be called before returning from 421 * this function, and then {@link Result#sendResult result.sendResult} 422 * called when the loading is complete. 423 * </p><p> 424 * In case the media item does not have any children, call {@link Result#sendResult} 425 * with an empty list. When the given {@code parentId} is invalid, implementations must 426 * call {@link Result#sendResult result.sendResult} with {@code null}, which will invoke 427 * {@link MediaBrowser.SubscriptionCallback#onError}. 428 * </p> 429 * 430 * @param parentId The id of the parent media item whose children are to be 431 * queried. 432 * @param result The Result to send the list of children to. 433 * @param options The bundle of service-specific arguments sent from the media 434 * browser. The information returned through the result should be 435 * affected by the contents of this bundle. 436 */ onLoadChildren(@onNull String parentId, @NonNull Result<List<MediaBrowser.MediaItem>> result, @NonNull Bundle options)437 public void onLoadChildren(@NonNull String parentId, 438 @NonNull Result<List<MediaBrowser.MediaItem>> result, @NonNull Bundle options) { 439 // To support backward compatibility, when the implementation of MediaBrowserService doesn't 440 // override onLoadChildren() with options, onLoadChildren() without options will be used 441 // instead, and the options will be applied in the implementation of result.onResultSent(). 442 result.setFlags(RESULT_FLAG_OPTION_NOT_HANDLED); 443 onLoadChildren(parentId, result); 444 } 445 446 /** 447 * Called to get information about a specific media item. 448 * <p> 449 * Implementations must call {@link Result#sendResult result.sendResult}. If 450 * loading the item will be an expensive operation {@link Result#detach 451 * result.detach} may be called before returning from this function, and 452 * then {@link Result#sendResult result.sendResult} called when the item has 453 * been loaded. 454 * </p><p> 455 * When the given {@code itemId} is invalid, implementations must call 456 * {@link Result#sendResult result.sendResult} with {@code null}. 457 * </p><p> 458 * The default implementation will invoke {@link MediaBrowser.ItemCallback#onError}. 459 * </p> 460 * 461 * @param itemId The id for the specific 462 * {@link android.media.browse.MediaBrowser.MediaItem}. 463 * @param result The Result to send the item to. 464 */ onLoadItem(String itemId, Result<MediaBrowser.MediaItem> result)465 public void onLoadItem(String itemId, Result<MediaBrowser.MediaItem> result) { 466 result.setFlags(RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED); 467 result.sendResult(null); 468 } 469 470 /** 471 * Call to set the media session. 472 * <p> 473 * This should be called as soon as possible during the service's startup. 474 * It may only be called once. 475 * 476 * @param token The token for the service's {@link MediaSession}. 477 */ setSessionToken(final MediaSession.Token token)478 public void setSessionToken(final MediaSession.Token token) { 479 if (token == null) { 480 throw new IllegalArgumentException("Session token may not be null."); 481 } 482 if (mSession != null) { 483 throw new IllegalStateException("The session token has already been set."); 484 } 485 mSession = token; 486 mHandler.post(new Runnable() { 487 @Override 488 public void run() { 489 Iterator<ConnectionRecord> iter = mConnections.values().iterator(); 490 while (iter.hasNext()){ 491 ConnectionRecord connection = iter.next(); 492 try { 493 connection.callbacks.onConnect(connection.root.getRootId(), token, 494 connection.root.getExtras()); 495 } catch (RemoteException e) { 496 Log.w(TAG, "Connection for " + connection.pkg + " is no longer valid."); 497 iter.remove(); 498 } 499 } 500 } 501 }); 502 } 503 504 /** 505 * Gets the session token, or null if it has not yet been created 506 * or if it has been destroyed. 507 */ getSessionToken()508 public @Nullable MediaSession.Token getSessionToken() { 509 return mSession; 510 } 511 512 /** 513 * Gets the root hints sent from the currently connected {@link MediaBrowser}. 514 * The root hints are service-specific arguments included in an optional bundle sent to the 515 * media browser service when connecting and retrieving the root id for browsing, or null if 516 * none. The contents of this bundle may affect the information returned when browsing. 517 * 518 * @throws IllegalStateException If this method is called outside of {@link #onGetRoot} or 519 * {@link #onLoadChildren} or {@link #onLoadItem}. 520 * @see MediaBrowserService.BrowserRoot#EXTRA_RECENT 521 * @see MediaBrowserService.BrowserRoot#EXTRA_OFFLINE 522 * @see MediaBrowserService.BrowserRoot#EXTRA_SUGGESTED 523 */ getBrowserRootHints()524 public final Bundle getBrowserRootHints() { 525 if (mCurConnection == null) { 526 throw new IllegalStateException("This should be called inside of onGetRoot or" 527 + " onLoadChildren or onLoadItem methods"); 528 } 529 return mCurConnection.rootHints == null ? null : new Bundle(mCurConnection.rootHints); 530 } 531 532 /** 533 * Gets the browser information who sent the current request. 534 * 535 * @throws IllegalStateException If this method is called outside of {@link #onGetRoot} or 536 * {@link #onLoadChildren} or {@link #onLoadItem}. 537 * @see MediaSessionManager#isTrustedForMediaControl(RemoteUserInfo) 538 */ getCurrentBrowserInfo()539 public final RemoteUserInfo getCurrentBrowserInfo() { 540 if (mCurConnection == null) { 541 throw new IllegalStateException("This should be called inside of onGetRoot or" 542 + " onLoadChildren or onLoadItem methods"); 543 } 544 return new RemoteUserInfo(mCurConnection.pkg, mCurConnection.pid, mCurConnection.uid, 545 mCurConnection.callbacks.asBinder()); 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 final private String mRootId; 824 final private 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