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