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