1 /* 2 * Copyright 2019 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.media; 18 19 import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; 20 import static com.android.media.flags.Flags.FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES; 21 import static com.android.media.flags.Flags.FLAG_ENABLE_GET_TRANSFERABLE_ROUTES; 22 import static com.android.media.flags.Flags.FLAG_ENABLE_PRIVILEGED_ROUTING_FOR_MEDIA_ROUTING_CONTROL; 23 import static com.android.media.flags.Flags.FLAG_ENABLE_RLP_CALLBACKS_IN_MEDIA_ROUTER2; 24 import static com.android.media.flags.Flags.FLAG_ENABLE_SCREEN_OFF_SCANNING; 25 26 import android.Manifest; 27 import android.annotation.CallbackExecutor; 28 import android.annotation.FlaggedApi; 29 import android.annotation.IntDef; 30 import android.annotation.NonNull; 31 import android.annotation.Nullable; 32 import android.annotation.RequiresPermission; 33 import android.annotation.SystemApi; 34 import android.annotation.TestApi; 35 import android.app.AppOpsManager; 36 import android.content.Context; 37 import android.content.pm.PackageManager; 38 import android.os.Bundle; 39 import android.os.Handler; 40 import android.os.Looper; 41 import android.os.Message; 42 import android.os.Process; 43 import android.os.RemoteException; 44 import android.os.ServiceManager; 45 import android.os.UserHandle; 46 import android.text.TextUtils; 47 import android.util.ArrayMap; 48 import android.util.ArraySet; 49 import android.util.Log; 50 import android.util.SparseArray; 51 52 import com.android.internal.annotations.GuardedBy; 53 import com.android.media.flags.Flags; 54 55 import java.lang.annotation.Retention; 56 import java.lang.annotation.RetentionPolicy; 57 import java.util.ArrayList; 58 import java.util.Collections; 59 import java.util.Comparator; 60 import java.util.HashMap; 61 import java.util.List; 62 import java.util.Map; 63 import java.util.Objects; 64 import java.util.Set; 65 import java.util.concurrent.CopyOnWriteArrayList; 66 import java.util.concurrent.Executor; 67 import java.util.concurrent.atomic.AtomicBoolean; 68 import java.util.concurrent.atomic.AtomicInteger; 69 import java.util.function.Consumer; 70 import java.util.stream.Collectors; 71 72 /** 73 * This API is not generally intended for third party application developers. Use the 74 * <a href="{@docRoot}jetpack/androidx.html">AndroidX</a> 75 * <a href="{@docRoot}reference/androidx/mediarouter/media/package-summary.html">Media Router 76 * Library</a> for consistent behavior across all devices. 77 * 78 * <p>MediaRouter2 allows applications to control the routing of media channels and streams from 79 * the current device to remote speakers and devices. 80 */ 81 // TODO(b/157873330): Add method names at the beginning of log messages. (e.g. selectRoute) 82 // Not only MediaRouter2, but also to service / manager / provider. 83 // TODO: ensure thread-safe and document it 84 public final class MediaRouter2 { 85 86 /** 87 * The state of a router not requesting route scanning. 88 * 89 * @hide 90 */ 91 public static final int SCANNING_STATE_NOT_SCANNING = 0; 92 93 /** 94 * The state of a router requesting scanning only while the user interacts with its owner app. 95 * 96 * <p>The device's screen must be on and the app must be in the foreground to trigger scanning 97 * under this state. 98 * 99 * @hide 100 */ 101 public static final int SCANNING_STATE_WHILE_INTERACTIVE = 1; 102 103 /** 104 * The state of a router requesting unrestricted scanning. 105 * 106 * <p>This state triggers scanning regardless of the restrictions required for {@link 107 * #SCANNING_STATE_WHILE_INTERACTIVE}. 108 * 109 * <p>Routers requesting unrestricted scanning must hold {@link 110 * Manifest.permission#MEDIA_ROUTING_CONTROL} or {@link 111 * Manifest.permission#MEDIA_CONTENT_CONTROL}. 112 * 113 * @hide 114 */ 115 public static final int SCANNING_STATE_SCANNING_FULL = 2; 116 117 /** @hide */ 118 @IntDef( 119 prefix = "SCANNING_STATE", 120 value = { 121 SCANNING_STATE_NOT_SCANNING, 122 SCANNING_STATE_WHILE_INTERACTIVE, 123 SCANNING_STATE_SCANNING_FULL 124 }) 125 @Retention(RetentionPolicy.SOURCE) 126 public @interface ScanningState {} 127 128 private static final String TAG = "MR2"; 129 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 130 private static final Object sSystemRouterLock = new Object(); 131 private static final Object sRouterLock = new Object(); 132 133 // The maximum time for the old routing controller available after transfer. 134 private static final int TRANSFER_TIMEOUT_MS = 30_000; 135 // The manager request ID representing that no manager is involved. 136 private static final long MANAGER_REQUEST_ID_NONE = MediaRoute2ProviderService.REQUEST_ID_NONE; 137 PackageNameUserHandlePair(String packageName, UserHandle user)138 private record PackageNameUserHandlePair(String packageName, UserHandle user) {} 139 InstanceInvalidatedCallbackRecord(Executor executor, Runnable runnable)140 private record InstanceInvalidatedCallbackRecord(Executor executor, Runnable runnable) {} 141 142 @GuardedBy("sSystemRouterLock") 143 private static final Map<PackageNameUserHandlePair, MediaRouter2> sAppToProxyRouterMap = 144 new ArrayMap<>(); 145 146 @GuardedBy("sRouterLock") 147 private static MediaRouter2 sInstance; 148 149 private final Context mContext; 150 private final IMediaRouterService mMediaRouterService; 151 private final Object mLock = new Object(); 152 private final MediaRouter2Impl mImpl; 153 154 private final CopyOnWriteArrayList<RouteCallbackRecord> mRouteCallbackRecords = 155 new CopyOnWriteArrayList<>(); 156 private final CopyOnWriteArrayList<RouteListingPreferenceCallbackRecord> 157 mListingPreferenceCallbackRecords = new CopyOnWriteArrayList<>(); 158 private final CopyOnWriteArrayList<TransferCallbackRecord> mTransferCallbackRecords = 159 new CopyOnWriteArrayList<>(); 160 private final CopyOnWriteArrayList<ControllerCallbackRecord> mControllerCallbackRecords = 161 new CopyOnWriteArrayList<>(); 162 163 private final CopyOnWriteArrayList<ControllerCreationRequest> mControllerCreationRequests = 164 new CopyOnWriteArrayList<>(); 165 166 /** 167 * Stores the latest copy of all routes received from the system server, without any filtering, 168 * sorting, or deduplication. 169 * 170 * <p>Uses {@link MediaRoute2Info#getId()} to set each entry's key. 171 */ 172 @GuardedBy("mLock") 173 private final Map<String, MediaRoute2Info> mRoutes = new ArrayMap<>(); 174 175 private final RoutingController mSystemController; 176 177 @GuardedBy("mLock") 178 private final Map<String, RoutingController> mNonSystemRoutingControllers = new ArrayMap<>(); 179 180 @GuardedBy("mLock") 181 private int mScreenOffScanRequestCount = 0; 182 183 @GuardedBy("mLock") 184 private int mScreenOnScanRequestCount = 0; 185 186 private final SparseArray<ScanRequest> mScanRequestsMap = new SparseArray<>(); 187 private final AtomicInteger mNextRequestId = new AtomicInteger(1); 188 private final Handler mHandler; 189 190 @GuardedBy("mLock") 191 private RouteDiscoveryPreference mDiscoveryPreference = RouteDiscoveryPreference.EMPTY; 192 193 // TODO: Make MediaRouter2 is always connected to the MediaRouterService. 194 @GuardedBy("mLock") 195 private MediaRouter2Stub mStub; 196 197 @GuardedBy("mLock") 198 @Nullable 199 private RouteListingPreference mRouteListingPreference; 200 201 /** 202 * Stores an auxiliary copy of {@link #mFilteredRoutes} at the time of the last route callback 203 * dispatch. This is only used to determine what callback a route should be assigned to (added, 204 * removed, changed) in {@link #dispatchFilteredRoutesUpdatedOnHandler(List)}. 205 */ 206 private volatile ArrayMap<String, MediaRoute2Info> mPreviousFilteredRoutes = new ArrayMap<>(); 207 208 private final Map<String, MediaRoute2Info> mPreviousUnfilteredRoutes = new ArrayMap<>(); 209 210 /** 211 * Stores the latest copy of exposed routes after filtering, sorting, and deduplication. Can be 212 * accessed through {@link #getRoutes()}. 213 * 214 * <p>This list is a copy of {@link #mRoutes} which has undergone filtering, sorting, and 215 * deduplication using criteria in {@link #mDiscoveryPreference}. 216 * 217 * @see #filterRoutesWithCompositePreferenceLocked(List) 218 */ 219 private volatile List<MediaRoute2Info> mFilteredRoutes = Collections.emptyList(); 220 private volatile OnGetControllerHintsListener mOnGetControllerHintsListener; 221 222 /** Gets an instance of the media router associated with the context. */ 223 @NonNull getInstance(@onNull Context context)224 public static MediaRouter2 getInstance(@NonNull Context context) { 225 Objects.requireNonNull(context, "context must not be null"); 226 synchronized (sRouterLock) { 227 if (sInstance == null) { 228 sInstance = new MediaRouter2(context.getApplicationContext()); 229 } 230 return sInstance; 231 } 232 } 233 234 /** 235 * Returns a proxy MediaRouter2 instance that allows you to control the routing of an app 236 * specified by {@code clientPackageName}. Returns {@code null} if the specified package name 237 * does not exist. 238 * 239 * <p>Proxy MediaRouter2 instances operate differently than regular MediaRouter2 instances: 240 * 241 * <ul> 242 * <li> 243 * <p>{@link #registerRouteCallback} ignores any {@link RouteDiscoveryPreference discovery 244 * preference} passed by a proxy router. Use {@link RouteDiscoveryPreference#EMPTY} when 245 * setting a route callback. 246 * <li> 247 * <p>Methods returning non-system {@link RoutingController controllers} always return new 248 * instances with the latest data. Do not attempt to compare or store them. Instead, use 249 * {@link #getController(String)} or {@link #getControllers()} to query the most 250 * up-to-date state. 251 * <li> 252 * <p>Calls to {@link #setOnGetControllerHintsListener} are ignored. 253 * </ul> 254 * 255 * <p>Callers that only hold the revocable form of {@link 256 * Manifest.permission#MEDIA_ROUTING_CONTROL} must use {@link #getInstance(Context, String, 257 * Executor, Runnable)} instead of this method. 258 * 259 * @param clientPackageName the package name of the app to control 260 * @return a proxy MediaRouter2 instance if {@code clientPackageName} exists or {@code null}. 261 * @throws IllegalStateException if the caller only holds a revocable version of {@link 262 * Manifest.permission#MEDIA_ROUTING_CONTROL}. 263 * @hide 264 */ 265 @SuppressWarnings("RequiresPermission") 266 @RequiresPermission( 267 anyOf = { 268 Manifest.permission.MEDIA_CONTENT_CONTROL, 269 Manifest.permission.MEDIA_ROUTING_CONTROL 270 }) 271 @SystemApi 272 @Nullable getInstance( @onNull Context context, @NonNull String clientPackageName)273 public static MediaRouter2 getInstance( 274 @NonNull Context context, @NonNull String clientPackageName) { 275 // Capturing the IAE here to not break nullability. 276 try { 277 return findOrCreateProxyInstanceForCallingUser( 278 context, 279 clientPackageName, 280 context.getUser(), 281 /* executor */ null, 282 /* onInstanceInvalidatedListener */ null); 283 } catch (IllegalArgumentException ex) { 284 Log.e(TAG, "Package " + clientPackageName + " not found. Ignoring."); 285 return null; 286 } 287 } 288 289 /** 290 * Returns a proxy MediaRouter2 instance that allows you to control the routing of an app 291 * specified by {@code clientPackageName}. Returns {@code null} if the specified package name 292 * does not exist. 293 * 294 * <p>Proxy MediaRouter2 instances operate differently than regular MediaRouter2 instances: 295 * 296 * <ul> 297 * <li> 298 * <p>{@link #registerRouteCallback} ignores any {@link RouteDiscoveryPreference discovery 299 * preference} passed by a proxy router. Use {@link RouteDiscoveryPreference#EMPTY} when 300 * setting a route callback. 301 * <li> 302 * <p>Methods returning non-system {@link RoutingController controllers} always return new 303 * instances with the latest data. Do not attempt to compare or store them. Instead, use 304 * {@link #getController(String)} or {@link #getControllers()} to query the most 305 * up-to-date state. 306 * <li> 307 * <p>Calls to {@link #setOnGetControllerHintsListener} are ignored. 308 * </ul> 309 * 310 * <p>Use this method when you only hold a revocable version of {@link 311 * Manifest.permission#MEDIA_ROUTING_CONTROL} (e.g. acquired via the {@link AppOpsManager}). 312 * Otherwise, use {@link #getInstance(Context, String)}. 313 * 314 * <p>{@code onInstanceInvalidatedListener} is called when the instance is invalidated because 315 * the calling app has lost {@link Manifest.permission#MEDIA_ROUTING_CONTROL} and does not hold 316 * {@link Manifest.permission#MEDIA_CONTENT_CONTROL}. Do not use the invalidated instance after 317 * receiving this callback, as the system will ignore all operations. Call {@link 318 * #getInstance(Context, String, Executor, Runnable)} again after reacquiring the relevant 319 * permissions. 320 * 321 * @param context The {@link Context} of the caller. 322 * @param clientPackageName The package name of the app you want to control the routing of. 323 * @param executor The {@link Executor} on which to invoke {@code 324 * onInstanceInvalidatedListener}. 325 * @param onInstanceInvalidatedListener Callback for when the {@link MediaRouter2} instance is 326 * invalidated due to lost permissions. 327 * @throws IllegalArgumentException if {@code clientPackageName} does not exist in the calling 328 * user. 329 */ 330 @SuppressWarnings("RequiresPermission") 331 @FlaggedApi(FLAG_ENABLE_PRIVILEGED_ROUTING_FOR_MEDIA_ROUTING_CONTROL) 332 @RequiresPermission( 333 anyOf = { 334 Manifest.permission.MEDIA_CONTENT_CONTROL, 335 Manifest.permission.MEDIA_ROUTING_CONTROL 336 }) 337 @NonNull getInstance( @onNull Context context, @NonNull String clientPackageName, @NonNull Executor executor, @NonNull Runnable onInstanceInvalidatedListener)338 public static MediaRouter2 getInstance( 339 @NonNull Context context, 340 @NonNull String clientPackageName, 341 @NonNull Executor executor, 342 @NonNull Runnable onInstanceInvalidatedListener) { 343 Objects.requireNonNull(executor, "Executor must not be null"); 344 Objects.requireNonNull( 345 onInstanceInvalidatedListener, "onInstanceInvalidatedListener must not be null."); 346 347 return findOrCreateProxyInstanceForCallingUser( 348 context, 349 clientPackageName, 350 context.getUser(), 351 executor, 352 onInstanceInvalidatedListener); 353 } 354 355 /** 356 * Returns a proxy MediaRouter2 instance that allows you to control the routing of an app 357 * specified by {@code clientPackageName} and {@code user}. 358 * 359 * <p>Proxy MediaRouter2 instances operate differently than regular MediaRouter2 instances: 360 * 361 * <ul> 362 * <li> 363 * <p>{@link #registerRouteCallback} ignores any {@link RouteDiscoveryPreference discovery 364 * preference} passed by a proxy router. Use a {@link RouteDiscoveryPreference} with empty 365 * {@link RouteDiscoveryPreference.Builder#setPreferredFeatures(List) preferred features} 366 * when setting a route callback. 367 * <li> 368 * <p>Methods returning non-system {@link RoutingController controllers} always return new 369 * instances with the latest data. Do not attempt to compare or store them. Instead, use 370 * {@link #getController(String)} or {@link #getControllers()} to query the most 371 * up-to-date state. 372 * <li> 373 * <p>Calls to {@link #setOnGetControllerHintsListener} are ignored. 374 * </ul> 375 * 376 * @param context The {@link Context} of the caller. 377 * @param clientPackageName The package name of the app you want to control the routing of. 378 * @param user The {@link UserHandle} of the user running the app for which to get the proxy 379 * router instance. Must match {@link Process#myUserHandle()} if the caller doesn't hold 380 * {@code Manifest.permission#INTERACT_ACROSS_USERS_FULL}. 381 * @throws SecurityException if {@code user} does not match {@link Process#myUserHandle()} and 382 * the caller does not hold {@code Manifest.permission#INTERACT_ACROSS_USERS_FULL}. 383 * @throws IllegalArgumentException if {@code clientPackageName} does not exist in {@code user}. 384 * @hide 385 */ 386 @RequiresPermission( 387 anyOf = { 388 Manifest.permission.MEDIA_CONTENT_CONTROL, 389 Manifest.permission.MEDIA_ROUTING_CONTROL 390 }) 391 @NonNull getInstance( @onNull Context context, @NonNull String clientPackageName, @NonNull UserHandle user)392 public static MediaRouter2 getInstance( 393 @NonNull Context context, @NonNull String clientPackageName, @NonNull UserHandle user) { 394 return findOrCreateProxyInstanceForCallingUser( 395 context, 396 clientPackageName, 397 user, 398 /* executor */ null, 399 /* onInstanceInvalidatedListener */ null); 400 } 401 402 /** 403 * Returns the per-process singleton proxy router instance for the {@code clientPackageName} and 404 * {@code user} if it exists, or otherwise it creates the appropriate instance. 405 * 406 * <p>If no instance has been created previously, the method will create an instance via {@link 407 * #MediaRouter2(Context, Looper, String, UserHandle)}. 408 */ 409 @NonNull findOrCreateProxyInstanceForCallingUser( Context context, String clientPackageName, UserHandle user, @Nullable Executor executor, @Nullable Runnable onInstanceInvalidatedListener)410 private static MediaRouter2 findOrCreateProxyInstanceForCallingUser( 411 Context context, 412 String clientPackageName, 413 UserHandle user, 414 @Nullable Executor executor, 415 @Nullable Runnable onInstanceInvalidatedListener) { 416 Objects.requireNonNull(context, "context must not be null"); 417 Objects.requireNonNull(user, "user must not be null"); 418 419 if (TextUtils.isEmpty(clientPackageName)) { 420 throw new IllegalArgumentException("clientPackageName must not be null or empty"); 421 } 422 423 if (executor == null || onInstanceInvalidatedListener == null) { 424 if (checkCallerHasOnlyRevocablePermissions(context)) { 425 throw new IllegalStateException( 426 "Use getInstance(Context, String, Executor, Runnable) to obtain a proxy" 427 + " MediaRouter2 instance."); 428 } 429 } 430 431 PackageNameUserHandlePair key = new PackageNameUserHandlePair(clientPackageName, user); 432 433 synchronized (sSystemRouterLock) { 434 MediaRouter2 instance = sAppToProxyRouterMap.get(key); 435 if (instance == null) { 436 instance = 437 new MediaRouter2(context, Looper.getMainLooper(), clientPackageName, user); 438 // Register proxy router after instantiation to avoid race condition. 439 ((ProxyMediaRouter2Impl) instance.mImpl).registerProxyRouter(); 440 sAppToProxyRouterMap.put(key, instance); 441 } 442 ((ProxyMediaRouter2Impl) instance.mImpl) 443 .registerInstanceInvalidatedCallback(executor, onInstanceInvalidatedListener); 444 return instance; 445 } 446 } 447 checkCallerHasOnlyRevocablePermissions(@onNull Context context)448 private static boolean checkCallerHasOnlyRevocablePermissions(@NonNull Context context) { 449 boolean hasMediaContentControl = 450 context.checkSelfPermission(Manifest.permission.MEDIA_CONTENT_CONTROL) 451 == PackageManager.PERMISSION_GRANTED; 452 boolean hasRegularMediaRoutingControl = 453 context.checkSelfPermission(Manifest.permission.MEDIA_ROUTING_CONTROL) 454 == PackageManager.PERMISSION_GRANTED; 455 AppOpsManager appOpsManager = context.getSystemService(AppOpsManager.class); 456 boolean hasAppOpMediaRoutingControl = 457 appOpsManager.unsafeCheckOp( 458 AppOpsManager.OPSTR_MEDIA_ROUTING_CONTROL, 459 context.getApplicationInfo().uid, 460 context.getOpPackageName()) 461 == AppOpsManager.MODE_ALLOWED; 462 463 return !hasMediaContentControl 464 && !hasRegularMediaRoutingControl 465 && hasAppOpMediaRoutingControl; 466 } 467 468 /** 469 * Starts scanning remote routes. 470 * 471 * <p>Route discovery can happen even when the {@link #startScan()} is not called. This is 472 * because the scanning could be started before by other apps. Therefore, calling this method 473 * after calling {@link #stopScan()} does not necessarily mean that the routes found before are 474 * removed and added again. 475 * 476 * <p>Use {@link RouteCallback} to get the route related events. 477 * 478 * <p>Note that calling start/stopScan is applied to all system routers in the same process. 479 * 480 * <p>This will be no-op for non-system media routers. 481 * 482 * @see #stopScan() 483 * @see #getInstance(Context, String) 484 * @hide 485 */ 486 @SystemApi 487 @RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL) startScan()488 public void startScan() { 489 mImpl.startScan(); 490 } 491 492 /** 493 * Stops scanning remote routes to reduce resource consumption. 494 * 495 * <p>Route discovery can be continued even after this method is called. This is because the 496 * scanning is only turned off when all the apps stop scanning. Therefore, calling this method 497 * does not necessarily mean the routes are removed. Also, for the same reason it does not mean 498 * that {@link RouteCallback#onRoutesAdded(List)} is not called afterwards. 499 * 500 * <p>Use {@link RouteCallback} to get the route related events. 501 * 502 * <p>Note that calling start/stopScan is applied to all system routers in the same process. 503 * 504 * <p>This will be no-op for non-system media routers. 505 * 506 * @see #startScan() 507 * @see #getInstance(Context, String) 508 * @hide 509 */ 510 @SystemApi 511 @RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL) stopScan()512 public void stopScan() { 513 mImpl.stopScan(); 514 } 515 516 /** 517 * Requests the system to actively scan for routes based on the router's {@link 518 * RouteDiscoveryPreference route discovery preference}. 519 * 520 * <p>You must call {@link #cancelScanRequest(ScanToken)} promptly to preserve system resources 521 * like battery. Avoid scanning unless there is clear intention from the user to start routing 522 * their media. 523 * 524 * <p>{@code scanRequest} specifies relevant scanning options, like whether the system should 525 * scan with the screen off. Screen off scanning requires {@link 526 * Manifest.permission#MEDIA_ROUTING_CONTROL} or {@link 527 * Manifest.permission#MEDIA_CONTENT_CONTROL}. 528 * 529 * <p>Proxy routers use the registered {@link RouteDiscoveryPreference} of their target routers. 530 * 531 * @return A unique {@link ScanToken} that identifies the scan request. 532 * @throws SecurityException If a {@link ScanRequest} with {@link 533 * ScanRequest.Builder#setScreenOffScan} true is passed, while not holding {@link 534 * Manifest.permission#MEDIA_ROUTING_CONTROL} or {@link 535 * Manifest.permission#MEDIA_CONTENT_CONTROL}. 536 */ 537 @FlaggedApi(FLAG_ENABLE_SCREEN_OFF_SCANNING) 538 @NonNull requestScan(@onNull ScanRequest scanRequest)539 public ScanToken requestScan(@NonNull ScanRequest scanRequest) { 540 Objects.requireNonNull(scanRequest, "scanRequest must not be null."); 541 ScanToken token = new ScanToken(mNextRequestId.getAndIncrement()); 542 543 synchronized (mLock) { 544 boolean shouldUpdate = 545 mScreenOffScanRequestCount == 0 546 && (scanRequest.isScreenOffScan() || mScreenOnScanRequestCount == 0); 547 548 if (shouldUpdate) { 549 try { 550 mImpl.updateScanningState( 551 scanRequest.isScreenOffScan() 552 ? SCANNING_STATE_SCANNING_FULL 553 : SCANNING_STATE_WHILE_INTERACTIVE); 554 555 } catch (RemoteException ex) { 556 throw ex.rethrowFromSystemServer(); 557 } 558 } 559 560 if (scanRequest.isScreenOffScan()) { 561 mScreenOffScanRequestCount++; 562 } else { 563 mScreenOnScanRequestCount++; 564 } 565 566 mScanRequestsMap.put(token.mId, scanRequest); 567 return token; 568 } 569 } 570 571 /** 572 * Releases the active scan request linked to the provided {@link ScanToken}. 573 * 574 * @see #requestScan(ScanRequest) 575 * @param token {@link ScanToken} of the {@link ScanRequest} to release. 576 * @throws IllegalArgumentException if the token does not match any active scan request. 577 */ 578 @FlaggedApi(FLAG_ENABLE_SCREEN_OFF_SCANNING) cancelScanRequest(@onNull ScanToken token)579 public void cancelScanRequest(@NonNull ScanToken token) { 580 Objects.requireNonNull(token, "token must not be null"); 581 582 synchronized (mLock) { 583 ScanRequest request = mScanRequestsMap.get(token.mId); 584 585 if (request == null) { 586 throw new IllegalArgumentException( 587 "The token does not match any active scan request"); 588 } 589 590 boolean shouldUpdate = 591 request.isScreenOffScan() 592 ? mScreenOffScanRequestCount == 1 593 : mScreenOnScanRequestCount == 1 && mScreenOffScanRequestCount == 0; 594 595 if (shouldUpdate) { 596 try { 597 if (!request.isScreenOffScan() || mScreenOnScanRequestCount == 0) { 598 mImpl.updateScanningState(SCANNING_STATE_NOT_SCANNING); 599 } else { 600 mImpl.updateScanningState(SCANNING_STATE_WHILE_INTERACTIVE); 601 } 602 603 } catch (RemoteException ex) { 604 ex.rethrowFromSystemServer(); 605 } 606 } 607 608 if (request.isScreenOffScan()) { 609 mScreenOffScanRequestCount--; 610 } else { 611 mScreenOnScanRequestCount--; 612 } 613 614 mScanRequestsMap.remove(token.mId); 615 } 616 } 617 MediaRouter2(Context appContext)618 private MediaRouter2(Context appContext) { 619 mContext = appContext; 620 mMediaRouterService = 621 IMediaRouterService.Stub.asInterface( 622 ServiceManager.getService(Context.MEDIA_ROUTER_SERVICE)); 623 mImpl = new LocalMediaRouter2Impl(mContext.getPackageName()); 624 mHandler = new Handler(Looper.getMainLooper()); 625 626 loadSystemRoutes(/* isProxyRouter */ false); 627 628 RoutingSessionInfo currentSystemSessionInfo = mImpl.getSystemSessionInfo(); 629 if (currentSystemSessionInfo == null) { 630 throw new RuntimeException("Null currentSystemSessionInfo. Something is wrong."); 631 } 632 633 mSystemController = new SystemRoutingController(currentSystemSessionInfo); 634 } 635 MediaRouter2( Context context, Looper looper, String clientPackageName, UserHandle user)636 private MediaRouter2( 637 Context context, Looper looper, String clientPackageName, UserHandle user) { 638 mContext = context; 639 mHandler = new Handler(looper); 640 mMediaRouterService = 641 IMediaRouterService.Stub.asInterface( 642 ServiceManager.getService(Context.MEDIA_ROUTER_SERVICE)); 643 644 loadSystemRoutes(/* isProxyRouter */ true); 645 646 mSystemController = 647 new SystemRoutingController( 648 ProxyMediaRouter2Impl.getSystemSessionInfoImpl( 649 mMediaRouterService, mContext.getPackageName(), clientPackageName)); 650 651 mImpl = new ProxyMediaRouter2Impl(context, clientPackageName, user); 652 } 653 654 @GuardedBy("mLock") loadSystemRoutes(boolean isProxyRouter)655 private void loadSystemRoutes(boolean isProxyRouter) { 656 List<MediaRoute2Info> currentSystemRoutes = null; 657 try { 658 currentSystemRoutes = mMediaRouterService.getSystemRoutes(mContext.getPackageName(), 659 isProxyRouter); 660 } catch (RemoteException ex) { 661 ex.rethrowFromSystemServer(); 662 } 663 664 if (currentSystemRoutes == null || currentSystemRoutes.isEmpty()) { 665 throw new RuntimeException("Null or empty currentSystemRoutes. Something is wrong."); 666 } 667 668 for (MediaRoute2Info route : currentSystemRoutes) { 669 mRoutes.put(route.getId(), route); 670 } 671 } 672 673 /** 674 * Gets the client package name of the app which this media router controls. 675 * 676 * <p>This will return null for non-system media routers. 677 * 678 * @see #getInstance(Context, String) 679 * @hide 680 */ 681 @SystemApi 682 @Nullable getClientPackageName()683 public String getClientPackageName() { 684 return mImpl.getClientPackageName(); 685 } 686 687 /** 688 * Registers a callback to discover routes and to receive events when they change. 689 * 690 * <p>Clients can register multiple callbacks, as long as the {@link RouteCallback} instances 691 * are different. Each callback can provide a unique {@link RouteDiscoveryPreference preference} 692 * and will only receive updates related to that set preference. 693 * 694 * <p>If the specified callback is already registered, its registration will be updated for the 695 * given {@link Executor executor} and {@link RouteDiscoveryPreference discovery preference}. 696 * 697 * <p>{@link #getInstance(Context) Local routers} must register a route callback to register in 698 * the system and start receiving updates. Otherwise, all operations will be no-ops. 699 * 700 * <p>Any discovery preference passed by a {@link #getInstance(Context, String) proxy router} 701 * will be ignored and will receive route updates based on the preference set by its matching 702 * local router. 703 */ registerRouteCallback( @onNull @allbackExecutor Executor executor, @NonNull RouteCallback routeCallback, @NonNull RouteDiscoveryPreference preference)704 public void registerRouteCallback( 705 @NonNull @CallbackExecutor Executor executor, 706 @NonNull RouteCallback routeCallback, 707 @NonNull RouteDiscoveryPreference preference) { 708 Objects.requireNonNull(executor, "executor must not be null"); 709 Objects.requireNonNull(routeCallback, "callback must not be null"); 710 Objects.requireNonNull(preference, "preference must not be null"); 711 712 RouteCallbackRecord record = 713 mImpl.createRouteCallbackRecord(executor, routeCallback, preference); 714 715 mRouteCallbackRecords.remove(record); 716 // It can fail to add the callback record if another registration with the same callback 717 // is happening but it's okay because either this or the other registration should be done. 718 mRouteCallbackRecords.addIfAbsent(record); 719 720 mImpl.registerRouteCallback(); 721 } 722 723 /** 724 * Unregisters the given callback. The callback will no longer receive events. If the callback 725 * has not been added or been removed already, it is ignored. 726 * 727 * @param routeCallback the callback to unregister 728 * @see #registerRouteCallback 729 */ unregisterRouteCallback(@onNull RouteCallback routeCallback)730 public void unregisterRouteCallback(@NonNull RouteCallback routeCallback) { 731 Objects.requireNonNull(routeCallback, "callback must not be null"); 732 733 if (!mRouteCallbackRecords.remove(new RouteCallbackRecord(null, routeCallback, null))) { 734 Log.w(TAG, "unregisterRouteCallback: Ignoring unknown callback"); 735 return; 736 } 737 738 mImpl.unregisterRouteCallback(); 739 } 740 741 /** 742 * Registers the given callback to be invoked when the {@link RouteListingPreference} of the 743 * target router changes. 744 * 745 * <p>Calls using a previously registered callback will overwrite the previous executor. 746 * 747 * @see #setRouteListingPreference(RouteListingPreference) 748 */ 749 @FlaggedApi(FLAG_ENABLE_RLP_CALLBACKS_IN_MEDIA_ROUTER2) registerRouteListingPreferenceUpdatedCallback( @onNull @allbackExecutor Executor executor, @NonNull Consumer<RouteListingPreference> routeListingPreferenceCallback)750 public void registerRouteListingPreferenceUpdatedCallback( 751 @NonNull @CallbackExecutor Executor executor, 752 @NonNull Consumer<RouteListingPreference> routeListingPreferenceCallback) { 753 Objects.requireNonNull(executor, "executor must not be null"); 754 Objects.requireNonNull(routeListingPreferenceCallback, "callback must not be null"); 755 756 RouteListingPreferenceCallbackRecord record = 757 new RouteListingPreferenceCallbackRecord(executor, routeListingPreferenceCallback); 758 759 mListingPreferenceCallbackRecords.remove(record); 760 mListingPreferenceCallbackRecords.add(record); 761 } 762 763 /** 764 * Unregisters the given callback to not receive {@link RouteListingPreference} change events. 765 * 766 * @see #registerRouteListingPreferenceUpdatedCallback(Executor, Consumer) 767 */ 768 @FlaggedApi(FLAG_ENABLE_RLP_CALLBACKS_IN_MEDIA_ROUTER2) unregisterRouteListingPreferenceUpdatedCallback( @onNull Consumer<RouteListingPreference> callback)769 public void unregisterRouteListingPreferenceUpdatedCallback( 770 @NonNull Consumer<RouteListingPreference> callback) { 771 Objects.requireNonNull(callback, "callback must not be null"); 772 773 if (!mListingPreferenceCallbackRecords.remove( 774 new RouteListingPreferenceCallbackRecord(/* executor */ null, callback))) { 775 Log.w( 776 TAG, 777 "unregisterRouteListingPreferenceUpdatedCallback: Ignoring an unknown" 778 + " callback"); 779 } 780 } 781 782 /** 783 * Shows the system output switcher dialog. 784 * 785 * <p>Should only be called when the context of MediaRouter2 is in the foreground and visible on 786 * the screen. 787 * 788 * <p>The appearance and precise behaviour of the system output switcher dialog may vary across 789 * different devices, OS versions, and form factors, but the basic functionality stays the same. 790 * 791 * <p>See <a 792 * href="https://developer.android.com/guide/topics/media/media-routing#output-switcher">Output 793 * Switcher documentation</a> for more details. 794 * 795 * @return {@code true} if the output switcher dialog is being shown, or {@code false} if the 796 * call is ignored because the app is in the background. 797 */ showSystemOutputSwitcher()798 public boolean showSystemOutputSwitcher() { 799 return mImpl.showSystemOutputSwitcher(); 800 } 801 802 /** 803 * Sets the {@link RouteListingPreference} of the app associated to this media router. 804 * 805 * <p>Use this method to inform the system UI of the routes that you would like to list for 806 * media routing, via the Output Switcher. 807 * 808 * <p>You should call this method before {@link #registerRouteCallback registering any route 809 * callbacks} and immediately after receiving any {@link RouteCallback#onRoutesUpdated route 810 * updates} in order to keep the system UI in a consistent state. You can also call this method 811 * at any other point to update the listing preference dynamically. 812 * 813 * <p>Any calls to this method from a privileged router will throw an {@link 814 * UnsupportedOperationException}. 815 * 816 * <p>Notes: 817 * 818 * <ol> 819 * <li>You should not include the ids of two or more routes with a match in their {@link 820 * MediaRoute2Info#getDeduplicationIds() deduplication ids}. If you do, the system will 821 * deduplicate them using its own criteria. 822 * <li>You can use this method to rank routes in the output switcher, placing the more 823 * important routes first. The system might override the proposed ranking. 824 * <li>You can use this method to avoid listing routes using dynamic criteria. For example, 825 * you can limit access to a specific type of device according to runtime criteria. 826 * </ol> 827 * 828 * @param routeListingPreference The {@link RouteListingPreference} for the system to use for 829 * route listing. When null, the system uses its default listing criteria. 830 */ setRouteListingPreference(@ullable RouteListingPreference routeListingPreference)831 public void setRouteListingPreference(@Nullable RouteListingPreference routeListingPreference) { 832 mImpl.setRouteListingPreference(routeListingPreference); 833 } 834 835 /** 836 * Returns the current {@link RouteListingPreference} of the target router. 837 * 838 * <p>If this instance was created using {@code #getInstance(Context, String)}, then it returns 839 * the last {@link RouteListingPreference} set by the process this router was created for. 840 * 841 * @see #setRouteListingPreference(RouteListingPreference) 842 */ 843 @FlaggedApi(FLAG_ENABLE_RLP_CALLBACKS_IN_MEDIA_ROUTER2) 844 @Nullable getRouteListingPreference()845 public RouteListingPreference getRouteListingPreference() { 846 synchronized (mLock) { 847 return mRouteListingPreference; 848 } 849 } 850 851 @GuardedBy("mLock") updateDiscoveryPreferenceIfNeededLocked()852 private boolean updateDiscoveryPreferenceIfNeededLocked() { 853 RouteDiscoveryPreference newDiscoveryPreference = new RouteDiscoveryPreference.Builder( 854 mRouteCallbackRecords.stream().map(record -> record.mPreference).collect( 855 Collectors.toList())).build(); 856 857 if (Objects.equals(mDiscoveryPreference, newDiscoveryPreference)) { 858 return false; 859 } 860 mDiscoveryPreference = newDiscoveryPreference; 861 updateFilteredRoutesLocked(); 862 return true; 863 } 864 865 /** 866 * Gets the list of all discovered routes. This list includes the routes that are not related to 867 * the client app. 868 * 869 * <p>This will return an empty list for non-system media routers. 870 * 871 * @hide 872 */ 873 @SystemApi 874 @NonNull getAllRoutes()875 public List<MediaRoute2Info> getAllRoutes() { 876 return mImpl.getAllRoutes(); 877 } 878 879 /** 880 * Gets the unmodifiable list of {@link MediaRoute2Info routes} currently known to the media 881 * router. 882 * 883 * <p>Please note that the list can be changed before callbacks are invoked. 884 * 885 * @return the list of routes that contains at least one of the route features in discovery 886 * preferences registered by the application 887 */ 888 @NonNull getRoutes()889 public List<MediaRoute2Info> getRoutes() { 890 synchronized (mLock) { 891 return mFilteredRoutes; 892 } 893 } 894 895 /** 896 * Registers a callback to get the result of {@link #transferTo(MediaRoute2Info)}. 897 * If you register the same callback twice or more, it will be ignored. 898 * 899 * @param executor the executor to execute the callback on 900 * @param callback the callback to register 901 * @see #unregisterTransferCallback 902 */ registerTransferCallback( @onNull @allbackExecutor Executor executor, @NonNull TransferCallback callback)903 public void registerTransferCallback( 904 @NonNull @CallbackExecutor Executor executor, @NonNull TransferCallback callback) { 905 Objects.requireNonNull(executor, "executor must not be null"); 906 Objects.requireNonNull(callback, "callback must not be null"); 907 908 TransferCallbackRecord record = new TransferCallbackRecord(executor, callback); 909 if (!mTransferCallbackRecords.addIfAbsent(record)) { 910 Log.w(TAG, "registerTransferCallback: Ignoring the same callback"); 911 } 912 } 913 914 /** 915 * Unregisters the given callback. The callback will no longer receive events. 916 * If the callback has not been added or been removed already, it is ignored. 917 * 918 * @param callback the callback to unregister 919 * @see #registerTransferCallback 920 */ unregisterTransferCallback(@onNull TransferCallback callback)921 public void unregisterTransferCallback(@NonNull TransferCallback callback) { 922 Objects.requireNonNull(callback, "callback must not be null"); 923 924 if (!mTransferCallbackRecords.remove(new TransferCallbackRecord(null, callback))) { 925 Log.w(TAG, "unregisterTransferCallback: Ignoring an unknown callback"); 926 } 927 } 928 929 /** 930 * Registers a {@link ControllerCallback}. If you register the same callback twice or more, it 931 * will be ignored. 932 * 933 * @see #unregisterControllerCallback(ControllerCallback) 934 */ registerControllerCallback( @onNull @allbackExecutor Executor executor, @NonNull ControllerCallback callback)935 public void registerControllerCallback( 936 @NonNull @CallbackExecutor Executor executor, @NonNull ControllerCallback callback) { 937 Objects.requireNonNull(executor, "executor must not be null"); 938 Objects.requireNonNull(callback, "callback must not be null"); 939 940 ControllerCallbackRecord record = new ControllerCallbackRecord(executor, callback); 941 if (!mControllerCallbackRecords.addIfAbsent(record)) { 942 Log.w(TAG, "registerControllerCallback: Ignoring the same callback"); 943 } 944 } 945 946 /** 947 * Unregisters a {@link ControllerCallback}. The callback will no longer receive events. 948 * If the callback has not been added or been removed already, it is ignored. 949 * 950 * @see #registerControllerCallback(Executor, ControllerCallback) 951 */ unregisterControllerCallback(@onNull ControllerCallback callback)952 public void unregisterControllerCallback(@NonNull ControllerCallback callback) { 953 Objects.requireNonNull(callback, "callback must not be null"); 954 955 if (!mControllerCallbackRecords.remove(new ControllerCallbackRecord(null, callback))) { 956 Log.w(TAG, "unregisterControllerCallback: Ignoring an unknown callback"); 957 } 958 } 959 960 /** 961 * Sets an {@link OnGetControllerHintsListener} to send hints when creating a 962 * {@link RoutingController}. To send the hints, listener should be set <em>BEFORE</em> calling 963 * {@link #transferTo(MediaRoute2Info)}. 964 * 965 * @param listener A listener to send optional app-specific hints when creating a controller. 966 * {@code null} for unset. 967 */ setOnGetControllerHintsListener(@ullable OnGetControllerHintsListener listener)968 public void setOnGetControllerHintsListener(@Nullable OnGetControllerHintsListener listener) { 969 mImpl.setOnGetControllerHintsListener(listener); 970 } 971 972 /** 973 * Transfers the current media to the given route. If it's necessary a new 974 * {@link RoutingController} is created or it is handled within the current routing controller. 975 * 976 * @param route the route you want to transfer the current media to. Pass {@code null} to 977 * stop routing of the current media. 978 * @see TransferCallback#onTransfer 979 * @see TransferCallback#onTransferFailure 980 */ transferTo(@onNull MediaRoute2Info route)981 public void transferTo(@NonNull MediaRoute2Info route) { 982 mImpl.transferTo(route); 983 } 984 985 /** 986 * Stops the current media routing. If the {@link #getSystemController() system controller} 987 * controls the media routing, this method is a no-op. 988 */ stop()989 public void stop() { 990 mImpl.stop(); 991 } 992 993 /** 994 * Transfers the media of a routing controller to the given route. 995 * 996 * <p>This will be no-op for non-system media routers. 997 * 998 * @param controller a routing controller controlling media routing. 999 * @param route the route you want to transfer the media to. 1000 * @hide 1001 */ 1002 @SystemApi 1003 @RequiresPermission(android.Manifest.permission.MEDIA_CONTENT_CONTROL) transfer(@onNull RoutingController controller, @NonNull MediaRoute2Info route)1004 public void transfer(@NonNull RoutingController controller, @NonNull MediaRoute2Info route) { 1005 mImpl.transfer(controller.getRoutingSessionInfo(), route); 1006 } 1007 requestCreateController( @onNull RoutingController controller, @NonNull MediaRoute2Info route, long managerRequestId)1008 void requestCreateController( 1009 @NonNull RoutingController controller, 1010 @NonNull MediaRoute2Info route, 1011 long managerRequestId) { 1012 1013 final int requestId = mNextRequestId.getAndIncrement(); 1014 1015 ControllerCreationRequest request = 1016 new ControllerCreationRequest(requestId, managerRequestId, route, controller); 1017 mControllerCreationRequests.add(request); 1018 1019 OnGetControllerHintsListener listener = mOnGetControllerHintsListener; 1020 Bundle controllerHints = null; 1021 if (listener != null) { 1022 controllerHints = listener.onGetControllerHints(route); 1023 if (controllerHints != null) { 1024 controllerHints = new Bundle(controllerHints); 1025 } 1026 } 1027 1028 MediaRouter2Stub stub; 1029 synchronized (mLock) { 1030 stub = mStub; 1031 } 1032 if (stub != null) { 1033 try { 1034 mMediaRouterService.requestCreateSessionWithRouter2( 1035 stub, 1036 requestId, 1037 managerRequestId, 1038 controller.getRoutingSessionInfo(), 1039 route, 1040 controllerHints); 1041 } catch (RemoteException ex) { 1042 Log.e(TAG, "createControllerForTransfer: " 1043 + "Failed to request for creating a controller.", ex); 1044 mControllerCreationRequests.remove(request); 1045 if (managerRequestId == MANAGER_REQUEST_ID_NONE) { 1046 notifyTransferFailure(route); 1047 } 1048 } 1049 } 1050 } 1051 1052 @NonNull getCurrentController()1053 private RoutingController getCurrentController() { 1054 List<RoutingController> controllers = getControllers(); 1055 return controllers.get(controllers.size() - 1); 1056 } 1057 1058 /** 1059 * Gets a {@link RoutingController} which can control the routes provided by system. 1060 * e.g. Phone speaker, wired headset, Bluetooth, etc. 1061 * 1062 * <p>Note: The system controller can't be released. Calling {@link RoutingController#release()} 1063 * will be ignored. 1064 * 1065 * <p>This method always returns the same instance. 1066 */ 1067 @NonNull getSystemController()1068 public RoutingController getSystemController() { 1069 return mSystemController; 1070 } 1071 1072 /** 1073 * Gets a {@link RoutingController} whose ID is equal to the given ID. 1074 * Returns {@code null} if there is no matching controller. 1075 */ 1076 @Nullable getController(@onNull String id)1077 public RoutingController getController(@NonNull String id) { 1078 Objects.requireNonNull(id, "id must not be null"); 1079 for (RoutingController controller : getControllers()) { 1080 if (TextUtils.equals(id, controller.getId())) { 1081 return controller; 1082 } 1083 } 1084 return null; 1085 } 1086 1087 /** 1088 * Gets the list of currently active {@link RoutingController routing controllers} on which 1089 * media can be played. 1090 * 1091 * <p>Note: The list returned here will never be empty. The first element in the list is 1092 * always the {@link #getSystemController() system controller}. 1093 */ 1094 @NonNull getControllers()1095 public List<RoutingController> getControllers() { 1096 return mImpl.getControllers(); 1097 } 1098 1099 /** 1100 * Sets the volume for a specific route. 1101 * 1102 * <p>The call may have no effect if the route is currently not selected. 1103 * 1104 * <p>This method is only supported by {@link #getInstance(Context, String) proxy MediaRouter2 1105 * instances}. Use {@link RoutingController#setVolume(int) RoutingController#setVolume(int)} 1106 * instead for {@link #getInstance(Context) local MediaRouter2 instances}.</p> 1107 * 1108 * @param volume The new volume value between 0 and {@link MediaRoute2Info#getVolumeMax}. 1109 * @throws UnsupportedOperationException If called on a {@link #getInstance(Context) local 1110 * router instance}. 1111 */ 1112 @FlaggedApi(FLAG_ENABLE_PRIVILEGED_ROUTING_FOR_MEDIA_ROUTING_CONTROL) 1113 @RequiresPermission( 1114 anyOf = { 1115 Manifest.permission.MEDIA_CONTENT_CONTROL, 1116 Manifest.permission.MEDIA_ROUTING_CONTROL 1117 }) setRouteVolume(@onNull MediaRoute2Info route, int volume)1118 public void setRouteVolume(@NonNull MediaRoute2Info route, int volume) { 1119 Objects.requireNonNull(route, "route must not be null"); 1120 1121 mImpl.setRouteVolume(route, volume); 1122 } 1123 syncRoutesOnHandler( List<MediaRoute2Info> currentRoutes, RoutingSessionInfo currentSystemSessionInfo)1124 void syncRoutesOnHandler( 1125 List<MediaRoute2Info> currentRoutes, RoutingSessionInfo currentSystemSessionInfo) { 1126 if (currentRoutes == null || currentRoutes.isEmpty() || currentSystemSessionInfo == null) { 1127 Log.e(TAG, "syncRoutesOnHandler: Received wrong data. currentRoutes=" + currentRoutes 1128 + ", currentSystemSessionInfo=" + currentSystemSessionInfo); 1129 return; 1130 } 1131 1132 updateRoutesOnHandler(currentRoutes); 1133 1134 RoutingSessionInfo oldInfo = mSystemController.getRoutingSessionInfo(); 1135 mSystemController.setRoutingSessionInfo(ensureClientPackageNameForSystemSession( 1136 currentSystemSessionInfo, mContext.getPackageName())); 1137 if (!oldInfo.equals(currentSystemSessionInfo)) { 1138 notifyControllerUpdated(mSystemController); 1139 } 1140 } 1141 dispatchFilteredRoutesUpdatedOnHandler(List<MediaRoute2Info> newRoutes)1142 void dispatchFilteredRoutesUpdatedOnHandler(List<MediaRoute2Info> newRoutes) { 1143 List<MediaRoute2Info> addedRoutes = new ArrayList<>(); 1144 List<MediaRoute2Info> removedRoutes = new ArrayList<>(); 1145 List<MediaRoute2Info> changedRoutes = new ArrayList<>(); 1146 1147 Set<String> newRouteIds = 1148 newRoutes.stream().map(MediaRoute2Info::getId).collect(Collectors.toSet()); 1149 1150 for (MediaRoute2Info route : newRoutes) { 1151 MediaRoute2Info prevRoute = mPreviousFilteredRoutes.get(route.getId()); 1152 if (prevRoute == null) { 1153 addedRoutes.add(route); 1154 } else if (!prevRoute.equals(route)) { 1155 changedRoutes.add(route); 1156 } 1157 } 1158 1159 for (int i = 0; i < mPreviousFilteredRoutes.size(); i++) { 1160 if (!newRouteIds.contains(mPreviousFilteredRoutes.keyAt(i))) { 1161 removedRoutes.add(mPreviousFilteredRoutes.valueAt(i)); 1162 } 1163 } 1164 1165 // update previous routes 1166 for (MediaRoute2Info route : removedRoutes) { 1167 mPreviousFilteredRoutes.remove(route.getId()); 1168 } 1169 for (MediaRoute2Info route : addedRoutes) { 1170 mPreviousFilteredRoutes.put(route.getId(), route); 1171 } 1172 for (MediaRoute2Info route : changedRoutes) { 1173 mPreviousFilteredRoutes.put(route.getId(), route); 1174 } 1175 1176 if (!addedRoutes.isEmpty()) { 1177 notifyRoutesAdded(addedRoutes); 1178 } 1179 if (!removedRoutes.isEmpty()) { 1180 notifyRoutesRemoved(removedRoutes); 1181 } 1182 if (!changedRoutes.isEmpty()) { 1183 notifyRoutesChanged(changedRoutes); 1184 } 1185 1186 // Note: We don't notify clients of changes in route ordering. 1187 if (!addedRoutes.isEmpty() || !removedRoutes.isEmpty() || !changedRoutes.isEmpty()) { 1188 notifyRoutesUpdated(newRoutes); 1189 } 1190 } 1191 dispatchControllerUpdatedIfNeededOnHandler(Map<String, MediaRoute2Info> routesMap)1192 void dispatchControllerUpdatedIfNeededOnHandler(Map<String, MediaRoute2Info> routesMap) { 1193 List<RoutingController> controllers = getControllers(); 1194 for (RoutingController controller : controllers) { 1195 1196 for (String selectedRoute : controller.getRoutingSessionInfo().getSelectedRoutes()) { 1197 if (routesMap.containsKey(selectedRoute) 1198 && mPreviousUnfilteredRoutes.containsKey(selectedRoute)) { 1199 MediaRoute2Info currentRoute = routesMap.get(selectedRoute); 1200 MediaRoute2Info oldRoute = mPreviousUnfilteredRoutes.get(selectedRoute); 1201 if (!currentRoute.equals(oldRoute)) { 1202 notifyControllerUpdated(controller); 1203 break; 1204 } 1205 } 1206 } 1207 } 1208 1209 mPreviousUnfilteredRoutes.clear(); 1210 mPreviousUnfilteredRoutes.putAll(routesMap); 1211 } 1212 updateRoutesOnHandler(List<MediaRoute2Info> newRoutes)1213 void updateRoutesOnHandler(List<MediaRoute2Info> newRoutes) { 1214 synchronized (mLock) { 1215 mRoutes.clear(); 1216 for (MediaRoute2Info route : newRoutes) { 1217 mRoutes.put(route.getId(), route); 1218 } 1219 updateFilteredRoutesLocked(); 1220 } 1221 } 1222 1223 /** Updates filtered routes and dispatch callbacks */ 1224 @GuardedBy("mLock") updateFilteredRoutesLocked()1225 void updateFilteredRoutesLocked() { 1226 mFilteredRoutes = 1227 Collections.unmodifiableList( 1228 filterRoutesWithCompositePreferenceLocked(List.copyOf(mRoutes.values()))); 1229 mHandler.sendMessage( 1230 obtainMessage( 1231 MediaRouter2::dispatchFilteredRoutesUpdatedOnHandler, 1232 this, 1233 mFilteredRoutes)); 1234 mHandler.sendMessage( 1235 obtainMessage( 1236 MediaRouter2::dispatchControllerUpdatedIfNeededOnHandler, 1237 this, 1238 new HashMap<>(mRoutes))); 1239 } 1240 1241 /** 1242 * Creates a controller and calls the {@link TransferCallback#onTransfer}. If the controller 1243 * creation has failed, then it calls {@link TransferCallback#onTransferFailure}. 1244 * 1245 * <p>Pass {@code null} to sessionInfo for the failure case. 1246 */ createControllerOnHandler(int requestId, @Nullable RoutingSessionInfo sessionInfo)1247 void createControllerOnHandler(int requestId, @Nullable RoutingSessionInfo sessionInfo) { 1248 ControllerCreationRequest matchingRequest = null; 1249 for (ControllerCreationRequest request : mControllerCreationRequests) { 1250 if (request.mRequestId == requestId) { 1251 matchingRequest = request; 1252 break; 1253 } 1254 } 1255 1256 if (matchingRequest == null) { 1257 Log.w(TAG, "createControllerOnHandler: Ignoring an unknown request."); 1258 return; 1259 } 1260 1261 mControllerCreationRequests.remove(matchingRequest); 1262 MediaRoute2Info requestedRoute = matchingRequest.mRoute; 1263 1264 // TODO: Notify the reason for failure. 1265 if (sessionInfo == null) { 1266 notifyTransferFailure(requestedRoute); 1267 return; 1268 } else if (!TextUtils.equals(requestedRoute.getProviderId(), sessionInfo.getProviderId())) { 1269 Log.w( 1270 TAG, 1271 "The session's provider ID does not match the requested route's. " 1272 + "(requested route's providerId=" 1273 + requestedRoute.getProviderId() 1274 + ", actual providerId=" 1275 + sessionInfo.getProviderId() 1276 + ")"); 1277 notifyTransferFailure(requestedRoute); 1278 return; 1279 } 1280 1281 RoutingController oldController = matchingRequest.mOldController; 1282 // When the old controller is released before transferred, treat it as a failure. 1283 // This could also happen when transfer is requested twice or more. 1284 if (!oldController.scheduleRelease()) { 1285 Log.w( 1286 TAG, 1287 "createControllerOnHandler: " 1288 + "Ignoring controller creation for released old controller. " 1289 + "oldController=" 1290 + oldController); 1291 if (!sessionInfo.isSystemSession()) { 1292 new RoutingController(sessionInfo).release(); 1293 } 1294 notifyTransferFailure(requestedRoute); 1295 return; 1296 } 1297 1298 RoutingController newController = addRoutingController(sessionInfo); 1299 notifyTransfer(oldController, newController); 1300 } 1301 1302 @NonNull addRoutingController(@onNull RoutingSessionInfo session)1303 private RoutingController addRoutingController(@NonNull RoutingSessionInfo session) { 1304 RoutingController controller; 1305 if (session.isSystemSession()) { 1306 // mSystemController is never released, so we only need to update its status. 1307 mSystemController.setRoutingSessionInfo(session); 1308 controller = mSystemController; 1309 } else { 1310 controller = new RoutingController(session); 1311 synchronized (mLock) { 1312 mNonSystemRoutingControllers.put(controller.getId(), controller); 1313 } 1314 } 1315 return controller; 1316 } 1317 updateControllerOnHandler(RoutingSessionInfo sessionInfo)1318 void updateControllerOnHandler(RoutingSessionInfo sessionInfo) { 1319 if (sessionInfo == null) { 1320 Log.w(TAG, "updateControllerOnHandler: Ignoring null sessionInfo."); 1321 return; 1322 } 1323 1324 RoutingController controller = 1325 getMatchingController(sessionInfo, /* logPrefix */ "updateControllerOnHandler"); 1326 if (controller != null) { 1327 controller.setRoutingSessionInfo(sessionInfo); 1328 notifyControllerUpdated(controller); 1329 } 1330 } 1331 releaseControllerOnHandler(RoutingSessionInfo sessionInfo)1332 void releaseControllerOnHandler(RoutingSessionInfo sessionInfo) { 1333 if (sessionInfo == null) { 1334 Log.w(TAG, "releaseControllerOnHandler: Ignoring null sessionInfo."); 1335 return; 1336 } 1337 1338 RoutingController matchingController = 1339 getMatchingController(sessionInfo, /* logPrefix */ "releaseControllerOnHandler"); 1340 1341 if (matchingController != null) { 1342 matchingController.releaseInternal(/* shouldReleaseSession= */ false); 1343 } 1344 } 1345 1346 @Nullable getMatchingController( RoutingSessionInfo sessionInfo, String logPrefix)1347 private RoutingController getMatchingController( 1348 RoutingSessionInfo sessionInfo, String logPrefix) { 1349 if (sessionInfo.isSystemSession()) { 1350 return getSystemController(); 1351 } else { 1352 RoutingController controller; 1353 synchronized (mLock) { 1354 controller = mNonSystemRoutingControllers.get(sessionInfo.getId()); 1355 } 1356 1357 if (controller == null) { 1358 Log.w( 1359 TAG, 1360 logPrefix 1361 + ": Matching controller not found. uniqueSessionId=" 1362 + sessionInfo.getId()); 1363 return null; 1364 } 1365 1366 RoutingSessionInfo oldInfo = controller.getRoutingSessionInfo(); 1367 if (!TextUtils.equals(oldInfo.getProviderId(), sessionInfo.getProviderId())) { 1368 Log.w( 1369 TAG, 1370 logPrefix 1371 + ": Provider IDs are not matched. old=" 1372 + oldInfo.getProviderId() 1373 + ", new=" 1374 + sessionInfo.getProviderId()); 1375 return null; 1376 } 1377 return controller; 1378 } 1379 } 1380 onRequestCreateControllerByManagerOnHandler( RoutingSessionInfo oldSession, MediaRoute2Info route, long managerRequestId)1381 void onRequestCreateControllerByManagerOnHandler( 1382 RoutingSessionInfo oldSession, MediaRoute2Info route, long managerRequestId) { 1383 Log.i( 1384 TAG, 1385 TextUtils.formatSimple( 1386 "requestCreateSessionByManager | requestId: %d, oldSession: %s, route: %s", 1387 managerRequestId, oldSession, route)); 1388 RoutingController controller; 1389 if (oldSession.isSystemSession()) { 1390 controller = getSystemController(); 1391 } else { 1392 synchronized (mLock) { 1393 controller = mNonSystemRoutingControllers.get(oldSession.getId()); 1394 } 1395 } 1396 if (controller == null) { 1397 return; 1398 } 1399 requestCreateController(controller, route, managerRequestId); 1400 } 1401 getSortedRoutes( List<MediaRoute2Info> routes, List<String> packageOrder)1402 private List<MediaRoute2Info> getSortedRoutes( 1403 List<MediaRoute2Info> routes, List<String> packageOrder) { 1404 if (packageOrder.isEmpty()) { 1405 return routes; 1406 } 1407 Map<String, Integer> packagePriority = new ArrayMap<>(); 1408 int count = packageOrder.size(); 1409 for (int i = 0; i < count; i++) { 1410 // the last package will have 1 as the priority 1411 packagePriority.put(packageOrder.get(i), count - i); 1412 } 1413 ArrayList<MediaRoute2Info> sortedRoutes = new ArrayList<>(routes); 1414 // take the negative for descending order 1415 sortedRoutes.sort( 1416 Comparator.comparingInt(r -> -packagePriority.getOrDefault(r.getPackageName(), 0))); 1417 return sortedRoutes; 1418 } 1419 1420 @GuardedBy("mLock") filterRoutesWithCompositePreferenceLocked( List<MediaRoute2Info> routes)1421 private List<MediaRoute2Info> filterRoutesWithCompositePreferenceLocked( 1422 List<MediaRoute2Info> routes) { 1423 1424 Set<String> deduplicationIdSet = new ArraySet<>(); 1425 1426 List<MediaRoute2Info> filteredRoutes = new ArrayList<>(); 1427 for (MediaRoute2Info route : 1428 getSortedRoutes(routes, mDiscoveryPreference.getDeduplicationPackageOrder())) { 1429 if (!route.hasAnyFeatures(mDiscoveryPreference.getPreferredFeatures())) { 1430 continue; 1431 } 1432 if (!mDiscoveryPreference.getAllowedPackages().isEmpty() 1433 && (route.getPackageName() == null 1434 || !mDiscoveryPreference 1435 .getAllowedPackages() 1436 .contains(route.getPackageName()))) { 1437 continue; 1438 } 1439 if (mDiscoveryPreference.shouldRemoveDuplicates()) { 1440 if (!Collections.disjoint(deduplicationIdSet, route.getDeduplicationIds())) { 1441 continue; 1442 } 1443 deduplicationIdSet.addAll(route.getDeduplicationIds()); 1444 } 1445 filteredRoutes.add(route); 1446 } 1447 return filteredRoutes; 1448 } 1449 1450 @NonNull getRoutesWithIds(@onNull List<String> routeIds)1451 private List<MediaRoute2Info> getRoutesWithIds(@NonNull List<String> routeIds) { 1452 synchronized (mLock) { 1453 return routeIds.stream() 1454 .map(mRoutes::get) 1455 .filter(Objects::nonNull) 1456 .collect(Collectors.toList()); 1457 } 1458 } 1459 notifyRoutesAdded(List<MediaRoute2Info> routes)1460 private void notifyRoutesAdded(List<MediaRoute2Info> routes) { 1461 for (RouteCallbackRecord record : mRouteCallbackRecords) { 1462 List<MediaRoute2Info> filteredRoutes = 1463 mImpl.filterRoutesWithIndividualPreference(routes, record.mPreference); 1464 if (!filteredRoutes.isEmpty()) { 1465 record.mExecutor.execute(() -> record.mRouteCallback.onRoutesAdded(filteredRoutes)); 1466 } 1467 } 1468 } 1469 notifyRoutesRemoved(List<MediaRoute2Info> routes)1470 private void notifyRoutesRemoved(List<MediaRoute2Info> routes) { 1471 for (RouteCallbackRecord record : mRouteCallbackRecords) { 1472 List<MediaRoute2Info> filteredRoutes = 1473 mImpl.filterRoutesWithIndividualPreference(routes, record.mPreference); 1474 if (!filteredRoutes.isEmpty()) { 1475 record.mExecutor.execute( 1476 () -> record.mRouteCallback.onRoutesRemoved(filteredRoutes)); 1477 } 1478 } 1479 } 1480 notifyRoutesChanged(List<MediaRoute2Info> routes)1481 private void notifyRoutesChanged(List<MediaRoute2Info> routes) { 1482 for (RouteCallbackRecord record : mRouteCallbackRecords) { 1483 List<MediaRoute2Info> filteredRoutes = 1484 mImpl.filterRoutesWithIndividualPreference(routes, record.mPreference); 1485 if (!filteredRoutes.isEmpty()) { 1486 record.mExecutor.execute( 1487 () -> record.mRouteCallback.onRoutesChanged(filteredRoutes)); 1488 } 1489 } 1490 } 1491 notifyRoutesUpdated(List<MediaRoute2Info> routes)1492 private void notifyRoutesUpdated(List<MediaRoute2Info> routes) { 1493 for (RouteCallbackRecord record : mRouteCallbackRecords) { 1494 List<MediaRoute2Info> filteredRoutes = 1495 mImpl.filterRoutesWithIndividualPreference(routes, record.mPreference); 1496 record.mExecutor.execute(() -> record.mRouteCallback.onRoutesUpdated(filteredRoutes)); 1497 } 1498 } 1499 notifyPreferredFeaturesChanged(List<String> features)1500 private void notifyPreferredFeaturesChanged(List<String> features) { 1501 for (RouteCallbackRecord record : mRouteCallbackRecords) { 1502 record.mExecutor.execute( 1503 () -> record.mRouteCallback.onPreferredFeaturesChanged(features)); 1504 } 1505 } 1506 notifyRouteListingPreferenceUpdated(@ullable RouteListingPreference preference)1507 private void notifyRouteListingPreferenceUpdated(@Nullable RouteListingPreference preference) { 1508 for (RouteListingPreferenceCallbackRecord record : mListingPreferenceCallbackRecords) { 1509 record.mExecutor.execute( 1510 () -> record.mRouteListingPreferenceCallback.accept(preference)); 1511 } 1512 } 1513 notifyTransfer(RoutingController oldController, RoutingController newController)1514 private void notifyTransfer(RoutingController oldController, RoutingController newController) { 1515 for (TransferCallbackRecord record : mTransferCallbackRecords) { 1516 record.mExecutor.execute( 1517 () -> record.mTransferCallback.onTransfer(oldController, newController)); 1518 } 1519 } 1520 notifyTransferFailure(MediaRoute2Info route)1521 private void notifyTransferFailure(MediaRoute2Info route) { 1522 for (TransferCallbackRecord record : mTransferCallbackRecords) { 1523 record.mExecutor.execute(() -> record.mTransferCallback.onTransferFailure(route)); 1524 } 1525 } 1526 notifyRequestFailed(int reason)1527 private void notifyRequestFailed(int reason) { 1528 for (TransferCallbackRecord record : mTransferCallbackRecords) { 1529 record.mExecutor.execute(() -> record.mTransferCallback.onRequestFailed(reason)); 1530 } 1531 } 1532 notifyStop(RoutingController controller)1533 private void notifyStop(RoutingController controller) { 1534 for (TransferCallbackRecord record : mTransferCallbackRecords) { 1535 record.mExecutor.execute(() -> record.mTransferCallback.onStop(controller)); 1536 } 1537 } 1538 notifyControllerUpdated(RoutingController controller)1539 private void notifyControllerUpdated(RoutingController controller) { 1540 for (ControllerCallbackRecord record : mControllerCallbackRecords) { 1541 record.mExecutor.execute(() -> record.mCallback.onControllerUpdated(controller)); 1542 } 1543 } 1544 1545 /** 1546 * Sets the routing session's {@linkplain RoutingSessionInfo#getClientPackageName() client 1547 * package name} to {@code packageName} if empty and returns the session. 1548 * 1549 * <p>This method must only be used for {@linkplain RoutingSessionInfo#isSystemSession() 1550 * system routing sessions}. 1551 */ ensureClientPackageNameForSystemSession( @onNull RoutingSessionInfo sessionInfo, @NonNull String packageName)1552 private static RoutingSessionInfo ensureClientPackageNameForSystemSession( 1553 @NonNull RoutingSessionInfo sessionInfo, @NonNull String packageName) { 1554 if (!sessionInfo.isSystemSession() 1555 || !TextUtils.isEmpty(sessionInfo.getClientPackageName())) { 1556 return sessionInfo; 1557 } 1558 1559 return new RoutingSessionInfo.Builder(sessionInfo) 1560 .setClientPackageName(packageName) 1561 .build(); 1562 } 1563 1564 /** Callback for receiving events about media route discovery. */ 1565 public abstract static class RouteCallback { 1566 /** 1567 * Called when routes are added. Whenever you register a callback, this will be invoked with 1568 * known routes. 1569 * 1570 * @param routes the list of routes that have been added. It's never empty. 1571 * @deprecated Use {@link #onRoutesUpdated(List)} instead. 1572 */ 1573 @Deprecated onRoutesAdded(@onNull List<MediaRoute2Info> routes)1574 public void onRoutesAdded(@NonNull List<MediaRoute2Info> routes) {} 1575 1576 /** 1577 * Called when routes are removed. 1578 * 1579 * @param routes the list of routes that have been removed. It's never empty. 1580 * @deprecated Use {@link #onRoutesUpdated(List)} instead. 1581 */ 1582 @Deprecated onRoutesRemoved(@onNull List<MediaRoute2Info> routes)1583 public void onRoutesRemoved(@NonNull List<MediaRoute2Info> routes) {} 1584 1585 /** 1586 * Called when the properties of one or more existing routes are changed. For example, it is 1587 * called when a route's name or volume have changed. 1588 * 1589 * @param routes the list of routes that have been changed. It's never empty. 1590 * @deprecated Use {@link #onRoutesUpdated(List)} instead. 1591 */ 1592 @Deprecated onRoutesChanged(@onNull List<MediaRoute2Info> routes)1593 public void onRoutesChanged(@NonNull List<MediaRoute2Info> routes) {} 1594 1595 /** 1596 * Called when the route list is updated, which can happen when routes are added, removed, 1597 * or modified. It will also be called when a route callback is registered. 1598 * 1599 * @param routes the updated list of routes filtered by the callback's individual discovery 1600 * preferences. 1601 */ onRoutesUpdated(@onNull List<MediaRoute2Info> routes)1602 public void onRoutesUpdated(@NonNull List<MediaRoute2Info> routes) {} 1603 1604 /** 1605 * Called when the client app's preferred features are changed. When this is called, it is 1606 * recommended to {@link #getRoutes()} to get the routes that are currently available to the 1607 * app. 1608 * 1609 * @param preferredFeatures the new preferred features set by the application 1610 * @hide 1611 */ 1612 @SystemApi onPreferredFeaturesChanged(@onNull List<String> preferredFeatures)1613 public void onPreferredFeaturesChanged(@NonNull List<String> preferredFeatures) {} 1614 } 1615 1616 /** Callback for receiving events on media transfer. */ 1617 public abstract static class TransferCallback { 1618 /** 1619 * Called when a media is transferred between two different routing controllers. This can 1620 * happen by calling {@link #transferTo(MediaRoute2Info)}. 1621 * 1622 * <p>Override this to start playback with {@code newController}. You may want to get the 1623 * status of the media that is being played with {@code oldController} and resume it 1624 * continuously with {@code newController}. After this is called, any callbacks with {@code 1625 * oldController} will not be invoked unless {@code oldController} is the {@link 1626 * #getSystemController() system controller}. You need to {@link RoutingController#release() 1627 * release} {@code oldController} before playing the media with {@code newController}. 1628 * 1629 * @param oldController the previous controller that controlled routing 1630 * @param newController the new controller to control routing 1631 * @see #transferTo(MediaRoute2Info) 1632 */ onTransfer( @onNull RoutingController oldController, @NonNull RoutingController newController)1633 public void onTransfer( 1634 @NonNull RoutingController oldController, 1635 @NonNull RoutingController newController) {} 1636 1637 /** 1638 * Called when {@link #transferTo(MediaRoute2Info)} failed. 1639 * 1640 * @param requestedRoute the route info which was used for the transfer 1641 */ onTransferFailure(@onNull MediaRoute2Info requestedRoute)1642 public void onTransferFailure(@NonNull MediaRoute2Info requestedRoute) {} 1643 1644 /** 1645 * Called when a media routing stops. It can be stopped by a user or a provider. App should 1646 * not continue playing media locally when this method is called. The {@code controller} is 1647 * released before this method is called. 1648 * 1649 * @param controller the controller that controlled the stopped media routing 1650 */ onStop(@onNull RoutingController controller)1651 public void onStop(@NonNull RoutingController controller) {} 1652 1653 /** 1654 * Called when a routing request fails. 1655 * 1656 * @param reason Reason for failure as per {@link 1657 * android.media.MediaRoute2ProviderService.Reason} 1658 * @hide 1659 */ onRequestFailed(int reason)1660 public void onRequestFailed(int reason) {} 1661 } 1662 1663 /** 1664 * A listener interface to send optional app-specific hints when creating a {@link 1665 * RoutingController}. 1666 */ 1667 public interface OnGetControllerHintsListener { 1668 /** 1669 * Called when the {@link MediaRouter2} or the system is about to request a media route 1670 * provider service to create a controller with the given route. The {@link Bundle} returned 1671 * here will be sent to media route provider service as a hint. 1672 * 1673 * <p>Since controller creation can be requested by the {@link MediaRouter2} and the system, 1674 * set the listener as soon as possible after acquiring {@link MediaRouter2} instance. The 1675 * method will be called on the same thread that calls {@link #transferTo(MediaRoute2Info)} 1676 * or the main thread if it is requested by the system. 1677 * 1678 * @param route the route to create a controller with 1679 * @return An optional bundle of app-specific arguments to send to the provider, or {@code 1680 * null} if none. The contents of this bundle may affect the result of controller 1681 * creation. 1682 * @see MediaRoute2ProviderService#onCreateSession(long, String, String, Bundle) 1683 */ 1684 @Nullable onGetControllerHints(@onNull MediaRoute2Info route)1685 Bundle onGetControllerHints(@NonNull MediaRoute2Info route); 1686 } 1687 1688 /** Callback for receiving {@link RoutingController} updates. */ 1689 public abstract static class ControllerCallback { 1690 /** 1691 * Called when a controller is updated. (e.g., when the selected routes of the controller is 1692 * changed or when the volume of the controller is changed.) 1693 * 1694 * @param controller the updated controller. It may be the {@link #getSystemController() 1695 * system controller}. 1696 * @see #getSystemController() 1697 */ onControllerUpdated(@onNull RoutingController controller)1698 public void onControllerUpdated(@NonNull RoutingController controller) {} 1699 } 1700 1701 /** 1702 * Represents an active scan request registered in the system. 1703 * 1704 * <p>See {@link #requestScan(ScanRequest)} for more information. 1705 */ 1706 @FlaggedApi(FLAG_ENABLE_SCREEN_OFF_SCANNING) 1707 public static final class ScanToken { 1708 private final int mId; 1709 ScanToken(int id)1710 private ScanToken(int id) { 1711 mId = id; 1712 } 1713 } 1714 1715 /** 1716 * Represents a set of parameters for scanning requests. 1717 * 1718 * <p>See {@link #requestScan(ScanRequest)} for more details. 1719 */ 1720 @FlaggedApi(FLAG_ENABLE_SCREEN_OFF_SCANNING) 1721 public static final class ScanRequest { 1722 private final boolean mIsScreenOffScan; 1723 ScanRequest(boolean isScreenOffScan)1724 private ScanRequest(boolean isScreenOffScan) { 1725 mIsScreenOffScan = isScreenOffScan; 1726 } 1727 1728 /** 1729 * Returns whether the scan request corresponds to a screen-off scan. 1730 * 1731 * @see #requestScan(ScanRequest) 1732 */ isScreenOffScan()1733 public boolean isScreenOffScan() { 1734 return mIsScreenOffScan; 1735 } 1736 1737 /** 1738 * Builder class for {@link ScanRequest}. 1739 * 1740 * @see #requestScan(ScanRequest) 1741 */ 1742 public static final class Builder { 1743 boolean mIsScreenOffScan; 1744 1745 /** 1746 * Creates a builder for a {@link ScanRequest} instance. 1747 * 1748 * @see #requestScan(ScanRequest) 1749 */ Builder()1750 public Builder() {} 1751 1752 /** 1753 * Sets whether the app is requesting to scan even while the screen is off, bypassing 1754 * default scanning restrictions. Only apps holding {@link 1755 * Manifest.permission#MEDIA_ROUTING_CONTROL} or {@link 1756 * Manifest.permission#MEDIA_CONTENT_CONTROL} should set this to {@code true}. 1757 * 1758 * @see #requestScan(ScanRequest) 1759 */ 1760 @NonNull setScreenOffScan(boolean isScreenOffScan)1761 public Builder setScreenOffScan(boolean isScreenOffScan) { 1762 mIsScreenOffScan = isScreenOffScan; 1763 return this; 1764 } 1765 1766 /** Returns a new {@link ScanRequest} instance. */ 1767 @NonNull build()1768 public ScanRequest build() { 1769 return new ScanRequest(mIsScreenOffScan); 1770 } 1771 } 1772 } 1773 1774 /** 1775 * A class to control media routing session in media route provider. For example, 1776 * selecting/deselecting/transferring to routes of a session can be done through this. Instances 1777 * are created when {@link TransferCallback#onTransfer(RoutingController, RoutingController)} is 1778 * called, which is invoked after {@link #transferTo(MediaRoute2Info)} is called. 1779 */ 1780 public class RoutingController { 1781 private final Object mControllerLock = new Object(); 1782 1783 private static final int CONTROLLER_STATE_UNKNOWN = 0; 1784 private static final int CONTROLLER_STATE_ACTIVE = 1; 1785 private static final int CONTROLLER_STATE_RELEASING = 2; 1786 private static final int CONTROLLER_STATE_RELEASED = 3; 1787 1788 @GuardedBy("mControllerLock") 1789 private RoutingSessionInfo mSessionInfo; 1790 1791 @GuardedBy("mControllerLock") 1792 private int mState; 1793 RoutingController(@onNull RoutingSessionInfo sessionInfo)1794 RoutingController(@NonNull RoutingSessionInfo sessionInfo) { 1795 mSessionInfo = sessionInfo; 1796 mState = CONTROLLER_STATE_ACTIVE; 1797 } 1798 RoutingController(@onNull RoutingSessionInfo sessionInfo, int state)1799 RoutingController(@NonNull RoutingSessionInfo sessionInfo, int state) { 1800 mSessionInfo = sessionInfo; 1801 mState = state; 1802 } 1803 1804 /** 1805 * @return the ID of the controller. It is globally unique. 1806 */ 1807 @NonNull getId()1808 public String getId() { 1809 synchronized (mControllerLock) { 1810 return mSessionInfo.getId(); 1811 } 1812 } 1813 1814 /** 1815 * Gets the original session ID set by {@link RoutingSessionInfo.Builder#Builder(String, 1816 * String)}. 1817 * 1818 * @hide 1819 */ 1820 @NonNull 1821 @TestApi getOriginalId()1822 public String getOriginalId() { 1823 synchronized (mControllerLock) { 1824 return mSessionInfo.getOriginalId(); 1825 } 1826 } 1827 1828 /** 1829 * Gets the control hints used to control routing session if available. It is set by the 1830 * media route provider. 1831 */ 1832 @Nullable getControlHints()1833 public Bundle getControlHints() { 1834 synchronized (mControllerLock) { 1835 return mSessionInfo.getControlHints(); 1836 } 1837 } 1838 1839 /** 1840 * @return the unmodifiable list of currently selected routes 1841 */ 1842 @NonNull getSelectedRoutes()1843 public List<MediaRoute2Info> getSelectedRoutes() { 1844 List<String> selectedRouteIds; 1845 synchronized (mControllerLock) { 1846 selectedRouteIds = mSessionInfo.getSelectedRoutes(); 1847 } 1848 return getRoutesWithIds(selectedRouteIds); 1849 } 1850 1851 /** 1852 * @return the unmodifiable list of selectable routes for the session. 1853 */ 1854 @NonNull getSelectableRoutes()1855 public List<MediaRoute2Info> getSelectableRoutes() { 1856 List<String> selectableRouteIds; 1857 synchronized (mControllerLock) { 1858 selectableRouteIds = mSessionInfo.getSelectableRoutes(); 1859 } 1860 return getRoutesWithIds(selectableRouteIds); 1861 } 1862 1863 /** 1864 * @return the unmodifiable list of deselectable routes for the session. 1865 */ 1866 @NonNull getDeselectableRoutes()1867 public List<MediaRoute2Info> getDeselectableRoutes() { 1868 List<String> deselectableRouteIds; 1869 synchronized (mControllerLock) { 1870 deselectableRouteIds = mSessionInfo.getDeselectableRoutes(); 1871 } 1872 return getRoutesWithIds(deselectableRouteIds); 1873 } 1874 1875 /** 1876 * Returns the unmodifiable list of transferable routes for the session. 1877 * 1878 * @see RoutingSessionInfo#getTransferableRoutes() 1879 */ 1880 @FlaggedApi(FLAG_ENABLE_GET_TRANSFERABLE_ROUTES) 1881 @NonNull getTransferableRoutes()1882 public List<MediaRoute2Info> getTransferableRoutes() { 1883 List<String> transferableRoutes; 1884 synchronized (mControllerLock) { 1885 transferableRoutes = mSessionInfo.getTransferableRoutes(); 1886 } 1887 return getRoutesWithIds(transferableRoutes); 1888 } 1889 1890 /** 1891 * Returns whether the transfer was initiated by the calling app (as determined by comparing 1892 * {@link UserHandle} and package name). 1893 */ 1894 @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES) wasTransferInitiatedBySelf()1895 public boolean wasTransferInitiatedBySelf() { 1896 return mImpl.wasTransferredBySelf(getRoutingSessionInfo()); 1897 } 1898 1899 /** 1900 * Returns the current {@link RoutingSessionInfo} associated to this controller. 1901 */ 1902 @NonNull getRoutingSessionInfo()1903 public RoutingSessionInfo getRoutingSessionInfo() { 1904 synchronized (mControllerLock) { 1905 return mSessionInfo; 1906 } 1907 } 1908 1909 /** 1910 * Gets the information about how volume is handled on the session. 1911 * 1912 * <p>Please note that you may not control the volume of the session even when you can 1913 * control the volume of each selected route in the session. 1914 * 1915 * @return {@link MediaRoute2Info#PLAYBACK_VOLUME_FIXED} or {@link 1916 * MediaRoute2Info#PLAYBACK_VOLUME_VARIABLE} 1917 */ 1918 @MediaRoute2Info.PlaybackVolume getVolumeHandling()1919 public int getVolumeHandling() { 1920 synchronized (mControllerLock) { 1921 return mSessionInfo.getVolumeHandling(); 1922 } 1923 } 1924 1925 /** Gets the maximum volume of the session. */ getVolumeMax()1926 public int getVolumeMax() { 1927 synchronized (mControllerLock) { 1928 return mSessionInfo.getVolumeMax(); 1929 } 1930 } 1931 1932 /** 1933 * Gets the current volume of the session. 1934 * 1935 * <p>When it's available, it represents the volume of routing session, which is a group of 1936 * selected routes. Use {@link MediaRoute2Info#getVolume()} to get the volume of a route, 1937 * 1938 * @see MediaRoute2Info#getVolume() 1939 */ getVolume()1940 public int getVolume() { 1941 synchronized (mControllerLock) { 1942 return mSessionInfo.getVolume(); 1943 } 1944 } 1945 1946 /** 1947 * Returns true if this controller is released, false otherwise. If it is released, then all 1948 * other getters from this instance may return invalid values. Also, any operations to this 1949 * instance will be ignored once released. 1950 * 1951 * @see #release 1952 */ isReleased()1953 public boolean isReleased() { 1954 synchronized (mControllerLock) { 1955 return mState == CONTROLLER_STATE_RELEASED; 1956 } 1957 } 1958 1959 /** 1960 * Selects a route for the remote session. After a route is selected, the media is expected 1961 * to be played to the all the selected routes. This is different from {@link 1962 * MediaRouter2#transferTo(MediaRoute2Info) transferring to a route}, where the media is 1963 * expected to 'move' from one route to another. 1964 * 1965 * <p>The given route must satisfy all of the following conditions: 1966 * 1967 * <ul> 1968 * <li>It should not be included in {@link #getSelectedRoutes()} 1969 * <li>It should be included in {@link #getSelectableRoutes()} 1970 * </ul> 1971 * 1972 * If the route doesn't meet any of above conditions, it will be ignored. 1973 * 1974 * @see #deselectRoute(MediaRoute2Info) 1975 * @see #getSelectedRoutes() 1976 * @see #getSelectableRoutes() 1977 * @see ControllerCallback#onControllerUpdated 1978 */ selectRoute(@onNull MediaRoute2Info route)1979 public void selectRoute(@NonNull MediaRoute2Info route) { 1980 Objects.requireNonNull(route, "route must not be null"); 1981 if (isReleased()) { 1982 Log.w(TAG, "selectRoute: Called on released controller. Ignoring."); 1983 return; 1984 } 1985 1986 List<MediaRoute2Info> selectedRoutes = getSelectedRoutes(); 1987 if (containsRouteInfoWithId(selectedRoutes, route.getId())) { 1988 Log.w(TAG, "Ignoring selecting a route that is already selected. route=" + route); 1989 return; 1990 } 1991 1992 List<MediaRoute2Info> selectableRoutes = getSelectableRoutes(); 1993 if (!containsRouteInfoWithId(selectableRoutes, route.getId())) { 1994 Log.w(TAG, "Ignoring selecting a non-selectable route=" + route); 1995 return; 1996 } 1997 1998 mImpl.selectRoute(route, getRoutingSessionInfo()); 1999 } 2000 2001 /** 2002 * Deselects a route from the remote session. After a route is deselected, the media is 2003 * expected to be stopped on the deselected route. 2004 * 2005 * <p>The given route must satisfy all of the following conditions: 2006 * 2007 * <ul> 2008 * <li>It should be included in {@link #getSelectedRoutes()} 2009 * <li>It should be included in {@link #getDeselectableRoutes()} 2010 * </ul> 2011 * 2012 * If the route doesn't meet any of above conditions, it will be ignored. 2013 * 2014 * @see #getSelectedRoutes() 2015 * @see #getDeselectableRoutes() 2016 * @see ControllerCallback#onControllerUpdated 2017 */ deselectRoute(@onNull MediaRoute2Info route)2018 public void deselectRoute(@NonNull MediaRoute2Info route) { 2019 Objects.requireNonNull(route, "route must not be null"); 2020 if (isReleased()) { 2021 Log.w(TAG, "deselectRoute: called on released controller. Ignoring."); 2022 return; 2023 } 2024 2025 List<MediaRoute2Info> selectedRoutes = getSelectedRoutes(); 2026 if (!containsRouteInfoWithId(selectedRoutes, route.getId())) { 2027 Log.w(TAG, "Ignoring deselecting a route that is not selected. route=" + route); 2028 return; 2029 } 2030 2031 List<MediaRoute2Info> deselectableRoutes = getDeselectableRoutes(); 2032 if (!containsRouteInfoWithId(deselectableRoutes, route.getId())) { 2033 Log.w(TAG, "Ignoring deselecting a non-deselectable route=" + route); 2034 return; 2035 } 2036 2037 mImpl.deselectRoute(route, getRoutingSessionInfo()); 2038 } 2039 2040 /** 2041 * Attempts a transfer to a {@link RoutingSessionInfo#getTransferableRoutes() transferable 2042 * route}. 2043 * 2044 * <p>Transferring to a transferable route does not require the app to transfer the playback 2045 * state from one route to the other. The route provider completely manages the transfer. An 2046 * example of provider-managed transfers are the switches between the system's routes, like 2047 * the built-in speakers and a BT headset. 2048 * 2049 * @return True if the transfer is handled by this controller, or false if a new controller 2050 * should be created instead. 2051 * @see RoutingSessionInfo#getSelectedRoutes() 2052 * @see RoutingSessionInfo#getTransferableRoutes() 2053 * @see ControllerCallback#onControllerUpdated 2054 */ tryTransferWithinProvider(@onNull MediaRoute2Info route)2055 boolean tryTransferWithinProvider(@NonNull MediaRoute2Info route) { 2056 Objects.requireNonNull(route, "route must not be null"); 2057 synchronized (mControllerLock) { 2058 if (isReleased()) { 2059 Log.w( 2060 TAG, 2061 "tryTransferWithinProvider: Called on released controller. Ignoring."); 2062 return true; 2063 } 2064 2065 // If this call is trying to transfer to a selected system route, we let them 2066 // through as a provider driven transfer in order to update the transfer reason and 2067 // initiator data. 2068 boolean isSystemRouteReselection = 2069 Flags.enableBuiltInSpeakerRouteSuitabilityStatuses() 2070 && mSessionInfo.isSystemSession() 2071 && route.isSystemRoute() 2072 && mSessionInfo.getSelectedRoutes().contains(route.getId()); 2073 if (!isSystemRouteReselection 2074 && !mSessionInfo.getTransferableRoutes().contains(route.getId())) { 2075 Log.i( 2076 TAG, 2077 "Transferring to a non-transferable route=" 2078 + route 2079 + " session= " 2080 + mSessionInfo.getId()); 2081 return false; 2082 } 2083 } 2084 2085 MediaRouter2Stub stub; 2086 synchronized (mLock) { 2087 stub = mStub; 2088 } 2089 if (stub != null) { 2090 try { 2091 mMediaRouterService.transferToRouteWithRouter2(stub, getId(), route); 2092 } catch (RemoteException ex) { 2093 Log.e(TAG, "Unable to transfer to route for session.", ex); 2094 } 2095 } 2096 return true; 2097 } 2098 2099 /** 2100 * Requests a volume change for the remote session asynchronously. 2101 * 2102 * @param volume The new volume value between 0 and {@link RoutingController#getVolumeMax} 2103 * (inclusive). 2104 * @see #getVolume() 2105 */ setVolume(int volume)2106 public void setVolume(int volume) { 2107 if (getVolumeHandling() == MediaRoute2Info.PLAYBACK_VOLUME_FIXED) { 2108 Log.w(TAG, "setVolume: The routing session has fixed volume. Ignoring."); 2109 return; 2110 } 2111 if (volume < 0 || volume > getVolumeMax()) { 2112 Log.w(TAG, "setVolume: The target volume is out of range. Ignoring"); 2113 return; 2114 } 2115 2116 if (isReleased()) { 2117 Log.w(TAG, "setVolume: Called on released controller. Ignoring."); 2118 return; 2119 } 2120 2121 mImpl.setSessionVolume(volume, getRoutingSessionInfo()); 2122 } 2123 2124 /** 2125 * Releases this controller and the corresponding session. Any operations on this controller 2126 * after calling this method will be ignored. The devices that are playing media will stop 2127 * playing it. 2128 */ release()2129 public void release() { 2130 releaseInternal(/* shouldReleaseSession= */ true); 2131 } 2132 2133 /** 2134 * Schedules release of the controller. 2135 * 2136 * @return {@code true} if it's successfully scheduled, {@code false} if it's already 2137 * scheduled to be released or released. 2138 */ scheduleRelease()2139 boolean scheduleRelease() { 2140 synchronized (mControllerLock) { 2141 if (mState != CONTROLLER_STATE_ACTIVE) { 2142 return false; 2143 } 2144 mState = CONTROLLER_STATE_RELEASING; 2145 } 2146 2147 synchronized (mLock) { 2148 // It could happen if the controller is released by the another thread 2149 // in between two locks 2150 if (!mNonSystemRoutingControllers.remove(getId(), this)) { 2151 // In that case, onStop isn't called so we return true to call onTransfer. 2152 // It's also consistent with that the another thread acquires the lock later. 2153 return true; 2154 } 2155 } 2156 2157 mHandler.postDelayed(this::release, TRANSFER_TIMEOUT_MS); 2158 2159 return true; 2160 } 2161 releaseInternal(boolean shouldReleaseSession)2162 void releaseInternal(boolean shouldReleaseSession) { 2163 boolean shouldNotifyStop; 2164 2165 synchronized (mControllerLock) { 2166 if (mState == CONTROLLER_STATE_RELEASED) { 2167 if (DEBUG) { 2168 Log.d(TAG, "releaseInternal: Called on released controller. Ignoring."); 2169 } 2170 return; 2171 } 2172 shouldNotifyStop = (mState == CONTROLLER_STATE_ACTIVE); 2173 mState = CONTROLLER_STATE_RELEASED; 2174 } 2175 2176 mImpl.releaseSession(shouldReleaseSession, shouldNotifyStop, this); 2177 } 2178 2179 @Override toString()2180 public String toString() { 2181 // To prevent logging spam, we only print the ID of each route. 2182 List<String> selectedRoutes = 2183 getSelectedRoutes().stream() 2184 .map(MediaRoute2Info::getId) 2185 .collect(Collectors.toList()); 2186 List<String> selectableRoutes = 2187 getSelectableRoutes().stream() 2188 .map(MediaRoute2Info::getId) 2189 .collect(Collectors.toList()); 2190 List<String> deselectableRoutes = 2191 getDeselectableRoutes().stream() 2192 .map(MediaRoute2Info::getId) 2193 .collect(Collectors.toList()); 2194 2195 StringBuilder result = 2196 new StringBuilder() 2197 .append("RoutingController{ ") 2198 .append("id=") 2199 .append(getId()) 2200 .append(", selectedRoutes={") 2201 .append(selectedRoutes) 2202 .append("}") 2203 .append(", selectableRoutes={") 2204 .append(selectableRoutes) 2205 .append("}") 2206 .append(", deselectableRoutes={") 2207 .append(deselectableRoutes) 2208 .append("}") 2209 .append(" }"); 2210 return result.toString(); 2211 } 2212 setRoutingSessionInfo(@onNull RoutingSessionInfo info)2213 void setRoutingSessionInfo(@NonNull RoutingSessionInfo info) { 2214 synchronized (mControllerLock) { 2215 mSessionInfo = info; 2216 } 2217 } 2218 2219 /** Returns whether any route in {@code routeList} has a same unique ID with given route. */ containsRouteInfoWithId( @onNull List<MediaRoute2Info> routeList, @NonNull String routeId)2220 private static boolean containsRouteInfoWithId( 2221 @NonNull List<MediaRoute2Info> routeList, @NonNull String routeId) { 2222 for (MediaRoute2Info info : routeList) { 2223 if (TextUtils.equals(routeId, info.getId())) { 2224 return true; 2225 } 2226 } 2227 return false; 2228 } 2229 } 2230 2231 class SystemRoutingController extends RoutingController { SystemRoutingController(@onNull RoutingSessionInfo sessionInfo)2232 SystemRoutingController(@NonNull RoutingSessionInfo sessionInfo) { 2233 super(sessionInfo); 2234 } 2235 2236 @Override isReleased()2237 public boolean isReleased() { 2238 // SystemRoutingController will never be released 2239 return false; 2240 } 2241 2242 @Override scheduleRelease()2243 boolean scheduleRelease() { 2244 // SystemRoutingController can be always transferred 2245 return true; 2246 } 2247 2248 @Override releaseInternal(boolean shouldReleaseSession)2249 void releaseInternal(boolean shouldReleaseSession) { 2250 // Do nothing. SystemRoutingController will never be released 2251 } 2252 } 2253 2254 static final class RouteCallbackRecord { 2255 public final Executor mExecutor; 2256 public final RouteCallback mRouteCallback; 2257 public final RouteDiscoveryPreference mPreference; 2258 RouteCallbackRecord( @ullable Executor executor, @NonNull RouteCallback routeCallback, @Nullable RouteDiscoveryPreference preference)2259 RouteCallbackRecord( 2260 @Nullable Executor executor, 2261 @NonNull RouteCallback routeCallback, 2262 @Nullable RouteDiscoveryPreference preference) { 2263 mRouteCallback = routeCallback; 2264 mExecutor = executor; 2265 mPreference = preference; 2266 } 2267 2268 @Override equals(Object obj)2269 public boolean equals(Object obj) { 2270 if (this == obj) { 2271 return true; 2272 } 2273 if (!(obj instanceof RouteCallbackRecord)) { 2274 return false; 2275 } 2276 return mRouteCallback == ((RouteCallbackRecord) obj).mRouteCallback; 2277 } 2278 2279 @Override hashCode()2280 public int hashCode() { 2281 return mRouteCallback.hashCode(); 2282 } 2283 } 2284 2285 private static final class RouteListingPreferenceCallbackRecord { 2286 public final Executor mExecutor; 2287 public final Consumer<RouteListingPreference> mRouteListingPreferenceCallback; 2288 RouteListingPreferenceCallbackRecord( @onNull Executor executor, @NonNull Consumer<RouteListingPreference> routeListingPreferenceCallback)2289 /* package */ RouteListingPreferenceCallbackRecord( 2290 @NonNull Executor executor, 2291 @NonNull Consumer<RouteListingPreference> routeListingPreferenceCallback) { 2292 mExecutor = executor; 2293 mRouteListingPreferenceCallback = routeListingPreferenceCallback; 2294 } 2295 2296 @Override equals(Object obj)2297 public boolean equals(Object obj) { 2298 if (this == obj) { 2299 return true; 2300 } 2301 if (!(obj instanceof RouteListingPreferenceCallbackRecord)) { 2302 return false; 2303 } 2304 return mRouteListingPreferenceCallback 2305 == ((RouteListingPreferenceCallbackRecord) obj).mRouteListingPreferenceCallback; 2306 } 2307 2308 @Override hashCode()2309 public int hashCode() { 2310 return mRouteListingPreferenceCallback.hashCode(); 2311 } 2312 } 2313 2314 static final class TransferCallbackRecord { 2315 public final Executor mExecutor; 2316 public final TransferCallback mTransferCallback; 2317 TransferCallbackRecord( @onNull Executor executor, @NonNull TransferCallback transferCallback)2318 TransferCallbackRecord( 2319 @NonNull Executor executor, @NonNull TransferCallback transferCallback) { 2320 mTransferCallback = transferCallback; 2321 mExecutor = executor; 2322 } 2323 2324 @Override equals(Object obj)2325 public boolean equals(Object obj) { 2326 if (this == obj) { 2327 return true; 2328 } 2329 if (!(obj instanceof TransferCallbackRecord)) { 2330 return false; 2331 } 2332 return mTransferCallback == ((TransferCallbackRecord) obj).mTransferCallback; 2333 } 2334 2335 @Override hashCode()2336 public int hashCode() { 2337 return mTransferCallback.hashCode(); 2338 } 2339 } 2340 2341 static final class ControllerCallbackRecord { 2342 public final Executor mExecutor; 2343 public final ControllerCallback mCallback; 2344 ControllerCallbackRecord( @ullable Executor executor, @NonNull ControllerCallback callback)2345 ControllerCallbackRecord( 2346 @Nullable Executor executor, @NonNull ControllerCallback callback) { 2347 mCallback = callback; 2348 mExecutor = executor; 2349 } 2350 2351 @Override equals(Object obj)2352 public boolean equals(Object obj) { 2353 if (this == obj) { 2354 return true; 2355 } 2356 if (!(obj instanceof ControllerCallbackRecord)) { 2357 return false; 2358 } 2359 return mCallback == ((ControllerCallbackRecord) obj).mCallback; 2360 } 2361 2362 @Override hashCode()2363 public int hashCode() { 2364 return mCallback.hashCode(); 2365 } 2366 } 2367 2368 static final class ControllerCreationRequest { 2369 public final int mRequestId; 2370 public final long mManagerRequestId; 2371 public final MediaRoute2Info mRoute; 2372 public final RoutingController mOldController; 2373 ControllerCreationRequest( int requestId, long managerRequestId, @NonNull MediaRoute2Info route, @NonNull RoutingController oldController)2374 ControllerCreationRequest( 2375 int requestId, 2376 long managerRequestId, 2377 @NonNull MediaRoute2Info route, 2378 @NonNull RoutingController oldController) { 2379 mRequestId = requestId; 2380 mManagerRequestId = managerRequestId; 2381 mRoute = Objects.requireNonNull(route, "route must not be null"); 2382 mOldController = 2383 Objects.requireNonNull(oldController, "oldController must not be null"); 2384 } 2385 } 2386 2387 class MediaRouter2Stub extends IMediaRouter2.Stub { 2388 @Override notifyRouterRegistered( List<MediaRoute2Info> currentRoutes, RoutingSessionInfo currentSystemSessionInfo)2389 public void notifyRouterRegistered( 2390 List<MediaRoute2Info> currentRoutes, RoutingSessionInfo currentSystemSessionInfo) { 2391 mHandler.sendMessage( 2392 obtainMessage( 2393 MediaRouter2::syncRoutesOnHandler, 2394 MediaRouter2.this, 2395 currentRoutes, 2396 currentSystemSessionInfo)); 2397 } 2398 2399 @Override notifyRoutesUpdated(List<MediaRoute2Info> routes)2400 public void notifyRoutesUpdated(List<MediaRoute2Info> routes) { 2401 mHandler.sendMessage( 2402 obtainMessage(MediaRouter2::updateRoutesOnHandler, MediaRouter2.this, routes)); 2403 } 2404 2405 @Override notifySessionCreated(int requestId, @Nullable RoutingSessionInfo sessionInfo)2406 public void notifySessionCreated(int requestId, @Nullable RoutingSessionInfo sessionInfo) { 2407 mHandler.sendMessage( 2408 obtainMessage( 2409 MediaRouter2::createControllerOnHandler, 2410 MediaRouter2.this, 2411 requestId, 2412 sessionInfo)); 2413 } 2414 2415 @Override notifySessionInfoChanged(@ullable RoutingSessionInfo sessionInfo)2416 public void notifySessionInfoChanged(@Nullable RoutingSessionInfo sessionInfo) { 2417 mHandler.sendMessage( 2418 obtainMessage( 2419 MediaRouter2::updateControllerOnHandler, 2420 MediaRouter2.this, 2421 sessionInfo)); 2422 } 2423 2424 @Override notifySessionReleased(RoutingSessionInfo sessionInfo)2425 public void notifySessionReleased(RoutingSessionInfo sessionInfo) { 2426 mHandler.sendMessage( 2427 obtainMessage( 2428 MediaRouter2::releaseControllerOnHandler, 2429 MediaRouter2.this, 2430 sessionInfo)); 2431 } 2432 2433 @Override requestCreateSessionByManager( long managerRequestId, RoutingSessionInfo oldSession, MediaRoute2Info route)2434 public void requestCreateSessionByManager( 2435 long managerRequestId, RoutingSessionInfo oldSession, MediaRoute2Info route) { 2436 mHandler.sendMessage( 2437 obtainMessage( 2438 MediaRouter2::onRequestCreateControllerByManagerOnHandler, 2439 MediaRouter2.this, 2440 oldSession, 2441 route, 2442 managerRequestId)); 2443 } 2444 } 2445 2446 /** 2447 * Provides a common interface for separating {@link LocalMediaRouter2Impl local} and {@link 2448 * ProxyMediaRouter2Impl proxy} {@link MediaRouter2} instances. 2449 */ 2450 private interface MediaRouter2Impl { 2451 updateScanningState(@canningState int scanningState)2452 void updateScanningState(@ScanningState int scanningState) throws RemoteException; 2453 startScan()2454 void startScan(); 2455 stopScan()2456 void stopScan(); 2457 getClientPackageName()2458 String getClientPackageName(); 2459 getPackageName()2460 String getPackageName(); 2461 getSystemSessionInfo()2462 RoutingSessionInfo getSystemSessionInfo(); 2463 createRouteCallbackRecord( @onNull @allbackExecutor Executor executor, @NonNull RouteCallback routeCallback, @NonNull RouteDiscoveryPreference preference)2464 RouteCallbackRecord createRouteCallbackRecord( 2465 @NonNull @CallbackExecutor Executor executor, 2466 @NonNull RouteCallback routeCallback, 2467 @NonNull RouteDiscoveryPreference preference); 2468 registerRouteCallback()2469 void registerRouteCallback(); 2470 unregisterRouteCallback()2471 void unregisterRouteCallback(); 2472 setRouteListingPreference(@ullable RouteListingPreference preference)2473 void setRouteListingPreference(@Nullable RouteListingPreference preference); 2474 showSystemOutputSwitcher()2475 boolean showSystemOutputSwitcher(); 2476 getAllRoutes()2477 List<MediaRoute2Info> getAllRoutes(); 2478 setOnGetControllerHintsListener(OnGetControllerHintsListener listener)2479 void setOnGetControllerHintsListener(OnGetControllerHintsListener listener); 2480 transferTo(MediaRoute2Info route)2481 void transferTo(MediaRoute2Info route); 2482 stop()2483 void stop(); 2484 transfer(@onNull RoutingSessionInfo sessionInfo, @NonNull MediaRoute2Info route)2485 void transfer(@NonNull RoutingSessionInfo sessionInfo, @NonNull MediaRoute2Info route); 2486 getControllers()2487 List<RoutingController> getControllers(); 2488 setRouteVolume(MediaRoute2Info route, int volume)2489 void setRouteVolume(MediaRoute2Info route, int volume); 2490 filterRoutesWithIndividualPreference( List<MediaRoute2Info> routes, RouteDiscoveryPreference discoveryPreference)2491 List<MediaRoute2Info> filterRoutesWithIndividualPreference( 2492 List<MediaRoute2Info> routes, RouteDiscoveryPreference discoveryPreference); 2493 2494 // RoutingController methods. setSessionVolume(int volume, RoutingSessionInfo sessionInfo)2495 void setSessionVolume(int volume, RoutingSessionInfo sessionInfo); 2496 selectRoute(MediaRoute2Info route, RoutingSessionInfo sessionInfo)2497 void selectRoute(MediaRoute2Info route, RoutingSessionInfo sessionInfo); 2498 deselectRoute(MediaRoute2Info route, RoutingSessionInfo sessionInfo)2499 void deselectRoute(MediaRoute2Info route, RoutingSessionInfo sessionInfo); 2500 releaseSession( boolean shouldReleaseSession, boolean shouldNotifyStop, RoutingController controller)2501 void releaseSession( 2502 boolean shouldReleaseSession, 2503 boolean shouldNotifyStop, 2504 RoutingController controller); 2505 2506 /** 2507 * Returns the value of {@link RoutingController#wasTransferInitiatedBySelf()} for the app 2508 * associated with this router. 2509 */ wasTransferredBySelf(RoutingSessionInfo sessionInfo)2510 boolean wasTransferredBySelf(RoutingSessionInfo sessionInfo); 2511 } 2512 2513 /** 2514 * Implements logic specific to proxy {@link MediaRouter2} instances. 2515 * 2516 * <p>A proxy {@link MediaRouter2} instance controls the routing of a different package and can 2517 * be obtained by calling {@link #getInstance(Context, String)}. This requires {@link 2518 * Manifest.permission#MEDIA_CONTENT_CONTROL MEDIA_CONTENT_CONTROL} permission. 2519 * 2520 * <p>Proxy routers behave differently than local routers. See {@link #getInstance(Context, 2521 * String)} for more details. 2522 */ 2523 private class ProxyMediaRouter2Impl implements MediaRouter2Impl { 2524 // Fields originating from MediaRouter2Manager. 2525 private final IMediaRouter2Manager.Stub mClient; 2526 private final CopyOnWriteArrayList<MediaRouter2Manager.TransferRequest> 2527 mTransferRequests = new CopyOnWriteArrayList<>(); 2528 private final AtomicInteger mScanRequestCount = new AtomicInteger(/* initialValue= */ 0); 2529 2530 // Fields originating from MediaRouter2. 2531 @NonNull private final String mClientPackageName; 2532 @NonNull private final UserHandle mClientUser; 2533 private final AtomicBoolean mIsScanning = new AtomicBoolean(/* initialValue= */ false); 2534 2535 @GuardedBy("mLock") 2536 private final List<InstanceInvalidatedCallbackRecord> mInstanceInvalidatedCallbackRecords = 2537 new ArrayList<>(); 2538 ProxyMediaRouter2Impl( @onNull Context context, @NonNull String clientPackageName, @NonNull UserHandle user)2539 ProxyMediaRouter2Impl( 2540 @NonNull Context context, 2541 @NonNull String clientPackageName, 2542 @NonNull UserHandle user) { 2543 mClientUser = user; 2544 mClientPackageName = clientPackageName; 2545 mClient = new Client(); 2546 mDiscoveryPreference = RouteDiscoveryPreference.EMPTY; 2547 } 2548 registerProxyRouter()2549 public void registerProxyRouter() { 2550 try { 2551 mMediaRouterService.registerProxyRouter( 2552 mClient, 2553 mContext.getApplicationContext().getPackageName(), 2554 mClientPackageName, 2555 mClientUser); 2556 } catch (RemoteException ex) { 2557 throw ex.rethrowFromSystemServer(); 2558 } 2559 } 2560 registerInstanceInvalidatedCallback( @ullable Executor executor, @Nullable Runnable onInstanceInvalidatedListener)2561 public void registerInstanceInvalidatedCallback( 2562 @Nullable Executor executor, @Nullable Runnable onInstanceInvalidatedListener) { 2563 if (executor == null || onInstanceInvalidatedListener == null) { 2564 return; 2565 } 2566 2567 InstanceInvalidatedCallbackRecord record = 2568 new InstanceInvalidatedCallbackRecord(executor, onInstanceInvalidatedListener); 2569 synchronized (mLock) { 2570 if (!mInstanceInvalidatedCallbackRecords.contains(record)) { 2571 mInstanceInvalidatedCallbackRecords.add(record); 2572 } 2573 } 2574 } 2575 2576 @Override updateScanningState(int scanningState)2577 public void updateScanningState(int scanningState) throws RemoteException { 2578 mMediaRouterService.updateScanningState(mClient, scanningState); 2579 } 2580 2581 @Override startScan()2582 public void startScan() { 2583 if (!mIsScanning.getAndSet(true)) { 2584 if (mScanRequestCount.getAndIncrement() == 0) { 2585 try { 2586 mMediaRouterService.updateScanningState( 2587 mClient, SCANNING_STATE_WHILE_INTERACTIVE); 2588 } catch (RemoteException ex) { 2589 throw ex.rethrowFromSystemServer(); 2590 } 2591 } 2592 } 2593 } 2594 2595 @Override stopScan()2596 public void stopScan() { 2597 if (mIsScanning.getAndSet(false)) { 2598 if (mScanRequestCount.updateAndGet( 2599 count -> { 2600 if (count == 0) { 2601 throw new IllegalStateException( 2602 "No active scan requests to unregister."); 2603 } else { 2604 return --count; 2605 } 2606 }) 2607 == 0) { 2608 try { 2609 mMediaRouterService.updateScanningState( 2610 mClient, SCANNING_STATE_NOT_SCANNING); 2611 } catch (RemoteException ex) { 2612 throw ex.rethrowFromSystemServer(); 2613 } 2614 } 2615 } 2616 } 2617 2618 @Override getClientPackageName()2619 public String getClientPackageName() { 2620 return mClientPackageName; 2621 } 2622 2623 /** 2624 * Returns {@code null}. This refers to the package name of the caller app, which is only 2625 * relevant for local routers. 2626 */ 2627 @Override getPackageName()2628 public String getPackageName() { 2629 return null; 2630 } 2631 2632 @Override getSystemSessionInfo()2633 public RoutingSessionInfo getSystemSessionInfo() { 2634 return getSystemSessionInfoImpl( 2635 mMediaRouterService, mContext.getPackageName(), mClientPackageName); 2636 } 2637 2638 /** 2639 * {@link RouteDiscoveryPreference Discovery preferences} are ignored for proxy routers, as 2640 * their callbacks should receive events related to the media app's preferences. This is 2641 * equivalent to setting {@link RouteDiscoveryPreference#EMPTY empty preferences}. 2642 */ 2643 @Override createRouteCallbackRecord( Executor executor, RouteCallback routeCallback, RouteDiscoveryPreference preference)2644 public RouteCallbackRecord createRouteCallbackRecord( 2645 Executor executor, 2646 RouteCallback routeCallback, 2647 RouteDiscoveryPreference preference) { 2648 return new RouteCallbackRecord(executor, routeCallback, RouteDiscoveryPreference.EMPTY); 2649 } 2650 2651 /** 2652 * No-op. Only local routers communicate directly with {@link 2653 * com.android.server.media.MediaRouter2ServiceImpl MediaRouter2ServiceImpl} and modify 2654 * {@link RouteDiscoveryPreference}. Proxy routers receive callbacks from {@link 2655 * MediaRouter2Manager}. 2656 */ 2657 @Override registerRouteCallback()2658 public void registerRouteCallback() { 2659 // Do nothing. 2660 } 2661 2662 /** No-op. See {@link ProxyMediaRouter2Impl#registerRouteCallback()}. */ 2663 @Override unregisterRouteCallback()2664 public void unregisterRouteCallback() { 2665 // Do nothing. 2666 } 2667 2668 @Override setRouteListingPreference(@ullable RouteListingPreference preference)2669 public void setRouteListingPreference(@Nullable RouteListingPreference preference) { 2670 throw new UnsupportedOperationException( 2671 "RouteListingPreference cannot be set by a privileged MediaRouter2 instance."); 2672 } 2673 2674 @Override showSystemOutputSwitcher()2675 public boolean showSystemOutputSwitcher() { 2676 try { 2677 return mMediaRouterService.showMediaOutputSwitcherWithProxyRouter(mClient); 2678 } catch (RemoteException ex) { 2679 throw ex.rethrowFromSystemServer(); 2680 } 2681 } 2682 2683 /** Gets the list of all discovered routes. */ 2684 @Override getAllRoutes()2685 public List<MediaRoute2Info> getAllRoutes() { 2686 synchronized (mLock) { 2687 return new ArrayList<>(mRoutes.values()); 2688 } 2689 } 2690 2691 /** No-op. Controller hints can only be provided by the media app through a local router. */ 2692 @Override setOnGetControllerHintsListener(OnGetControllerHintsListener listener)2693 public void setOnGetControllerHintsListener(OnGetControllerHintsListener listener) { 2694 // Do nothing. 2695 } 2696 2697 /** 2698 * Transfers the current {@link RoutingSessionInfo routing session} associated with the 2699 * router's {@link #mClientPackageName client package name} to a specified {@link 2700 * MediaRoute2Info route}. 2701 * 2702 * <p>This method is equivalent to {@link #transfer(RoutingSessionInfo, MediaRoute2Info)}, 2703 * except that the {@link RoutingSessionInfo routing session} is resolved based on the 2704 * router's {@link #mClientPackageName client package name}. 2705 * 2706 * @param route The route to transfer to. 2707 */ 2708 @Override transferTo(MediaRoute2Info route)2709 public void transferTo(MediaRoute2Info route) { 2710 Objects.requireNonNull(route, "route must not be null"); 2711 2712 List<RoutingSessionInfo> sessionInfos = getRoutingSessions(); 2713 RoutingSessionInfo targetSession = sessionInfos.get(sessionInfos.size() - 1); 2714 transfer(targetSession, route); 2715 } 2716 2717 @Override stop()2718 public void stop() { 2719 List<RoutingSessionInfo> sessionInfos = getRoutingSessions(); 2720 RoutingSessionInfo sessionToRelease = sessionInfos.get(sessionInfos.size() - 1); 2721 releaseSession(sessionToRelease); 2722 } 2723 2724 /** 2725 * Transfers a {@link RoutingSessionInfo routing session} to a {@link MediaRoute2Info 2726 * route}. 2727 * 2728 * <p>{@link #onTransferred} is called on success or {@link #onTransferFailed} is called if 2729 * the request fails. 2730 * 2731 * <p>This method will default for in-session transfer if the {@link MediaRoute2Info route} 2732 * is a {@link RoutingSessionInfo#getTransferableRoutes() transferable route}. Otherwise, it 2733 * will attempt an out-of-session transfer. 2734 * 2735 * @param sessionInfo The {@link RoutingSessionInfo routing session} to transfer. 2736 * @param route The {@link MediaRoute2Info route} to transfer to. 2737 * @see #transferToRoute(RoutingSessionInfo, MediaRoute2Info, UserHandle, String) 2738 * @see #requestCreateSession(RoutingSessionInfo, MediaRoute2Info) 2739 */ 2740 @Override 2741 @SuppressWarnings("AndroidFrameworkRequiresPermission") transfer( @onNull RoutingSessionInfo sessionInfo, @NonNull MediaRoute2Info route)2742 public void transfer( 2743 @NonNull RoutingSessionInfo sessionInfo, @NonNull MediaRoute2Info route) { 2744 Objects.requireNonNull(sessionInfo, "sessionInfo must not be null"); 2745 Objects.requireNonNull(route, "route must not be null"); 2746 2747 Log.v( 2748 TAG, 2749 "Transferring routing session. session= " + sessionInfo + ", route=" + route); 2750 2751 boolean isUnknownRoute; 2752 synchronized (mLock) { 2753 isUnknownRoute = !mRoutes.containsKey(route.getId()); 2754 } 2755 2756 if (isUnknownRoute) { 2757 Log.w(TAG, "transfer: Ignoring an unknown route id=" + route.getId()); 2758 this.onTransferFailed(sessionInfo, route); 2759 return; 2760 } 2761 2762 // If this call is trying to transfer to a selected system route, we let them 2763 // through as a provider driven transfer in order to update the transfer reason and 2764 // initiator data. 2765 boolean isSystemRouteReselection = 2766 Flags.enableBuiltInSpeakerRouteSuitabilityStatuses() 2767 && sessionInfo.isSystemSession() 2768 && route.isSystemRoute() 2769 && sessionInfo.getSelectedRoutes().contains(route.getId()); 2770 if (sessionInfo.getTransferableRoutes().contains(route.getId()) 2771 || isSystemRouteReselection) { 2772 transferToRoute(sessionInfo, route, mClientUser, mClientPackageName); 2773 } else { 2774 requestCreateSession(sessionInfo, route, mClientUser, mClientPackageName); 2775 } 2776 } 2777 2778 /** 2779 * Requests an in-session transfer of a {@link RoutingSessionInfo routing session} to a 2780 * {@link MediaRoute2Info route}. 2781 * 2782 * <p>The provided {@link MediaRoute2Info route} must be listed in the {@link 2783 * RoutingSessionInfo routing session's} {@link RoutingSessionInfo#getTransferableRoutes() 2784 * transferable routes list}. Otherwise, the request will fail. 2785 * 2786 * <p>Use {@link #requestCreateSession(RoutingSessionInfo, MediaRoute2Info)} to request an 2787 * out-of-session transfer. 2788 * 2789 * @param session The {@link RoutingSessionInfo routing session} to transfer. 2790 * @param route The {@link MediaRoute2Info route} to transfer to. Must be one of the {@link 2791 * RoutingSessionInfo routing session's} {@link 2792 * RoutingSessionInfo#getTransferableRoutes() transferable routes}. 2793 */ 2794 @RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL) transferToRoute( @onNull RoutingSessionInfo session, @NonNull MediaRoute2Info route, @NonNull UserHandle transferInitiatorUserHandle, @NonNull String transferInitiatorPackageName)2795 private void transferToRoute( 2796 @NonNull RoutingSessionInfo session, 2797 @NonNull MediaRoute2Info route, 2798 @NonNull UserHandle transferInitiatorUserHandle, 2799 @NonNull String transferInitiatorPackageName) { 2800 int requestId = createTransferRequest(session, route); 2801 2802 try { 2803 mMediaRouterService.transferToRouteWithManager( 2804 mClient, 2805 requestId, 2806 session.getId(), 2807 route, 2808 transferInitiatorUserHandle, 2809 transferInitiatorPackageName); 2810 } catch (RemoteException ex) { 2811 throw ex.rethrowFromSystemServer(); 2812 } 2813 } 2814 2815 /** 2816 * Requests an out-of-session transfer of a {@link RoutingSessionInfo routing session} to a 2817 * {@link MediaRoute2Info route}. 2818 * 2819 * <p>This request creates a new {@link RoutingSessionInfo routing session} regardless of 2820 * whether the {@link MediaRoute2Info route} is one of the {@link RoutingSessionInfo current 2821 * session's} {@link RoutingSessionInfo#getTransferableRoutes() transferable routes}. 2822 * 2823 * <p>Use {@link #transferToRoute(RoutingSessionInfo, MediaRoute2Info)} to request an 2824 * in-session transfer. 2825 * 2826 * @param oldSession The {@link RoutingSessionInfo routing session} to transfer. 2827 * @param route The {@link MediaRoute2Info route} to transfer to. 2828 */ requestCreateSession( @onNull RoutingSessionInfo oldSession, @NonNull MediaRoute2Info route, @NonNull UserHandle transferInitiatorUserHandle, @NonNull String transferInitiatorPackageName)2829 private void requestCreateSession( 2830 @NonNull RoutingSessionInfo oldSession, 2831 @NonNull MediaRoute2Info route, 2832 @NonNull UserHandle transferInitiatorUserHandle, 2833 @NonNull String transferInitiatorPackageName) { 2834 if (TextUtils.isEmpty(oldSession.getClientPackageName())) { 2835 Log.w(TAG, "requestCreateSession: Can't create a session without package name."); 2836 this.onTransferFailed(oldSession, route); 2837 return; 2838 } 2839 2840 int requestId = createTransferRequest(oldSession, route); 2841 2842 try { 2843 mMediaRouterService.requestCreateSessionWithManager( 2844 mClient, 2845 requestId, 2846 oldSession, 2847 route, 2848 transferInitiatorUserHandle, 2849 transferInitiatorPackageName); 2850 } catch (RemoteException ex) { 2851 throw ex.rethrowFromSystemServer(); 2852 } 2853 } 2854 2855 @Override getControllers()2856 public List<RoutingController> getControllers() { 2857 List<RoutingController> result = new ArrayList<>(); 2858 2859 /* Unlike local MediaRouter2 instances, controller instances cannot be kept because 2860 transfer events initiated from other apps will not come through manager.*/ 2861 List<RoutingSessionInfo> sessions = getRoutingSessions(); 2862 for (RoutingSessionInfo session : sessions) { 2863 RoutingController controller; 2864 if (session.isSystemSession()) { 2865 mSystemController.setRoutingSessionInfo(session); 2866 controller = mSystemController; 2867 } else { 2868 controller = new RoutingController(session); 2869 } 2870 result.add(controller); 2871 } 2872 return result; 2873 } 2874 2875 /** 2876 * Requests a volume change for a {@link MediaRoute2Info route}. 2877 * 2878 * <p>It may have no effect if the {@link MediaRoute2Info route} is not currently selected. 2879 * 2880 * @param volume The desired volume value between 0 and {@link 2881 * MediaRoute2Info#getVolumeMax()} (inclusive). 2882 */ 2883 @Override setRouteVolume(@onNull MediaRoute2Info route, int volume)2884 public void setRouteVolume(@NonNull MediaRoute2Info route, int volume) { 2885 if (route.getVolumeHandling() == MediaRoute2Info.PLAYBACK_VOLUME_FIXED) { 2886 Log.w(TAG, "setRouteVolume: the route has fixed volume. Ignoring."); 2887 return; 2888 } 2889 if (volume < 0 || volume > route.getVolumeMax()) { 2890 Log.w(TAG, "setRouteVolume: the target volume is out of range. Ignoring"); 2891 return; 2892 } 2893 2894 try { 2895 int requestId = mNextRequestId.getAndIncrement(); 2896 mMediaRouterService.setRouteVolumeWithManager(mClient, requestId, route, volume); 2897 } catch (RemoteException ex) { 2898 throw ex.rethrowFromSystemServer(); 2899 } 2900 } 2901 2902 /** 2903 * Requests a volume change for a {@link RoutingSessionInfo routing session}. 2904 * 2905 * @param volume The desired volume value between 0 and {@link 2906 * RoutingSessionInfo#getVolumeMax()} (inclusive). 2907 */ 2908 @Override setSessionVolume(int volume, RoutingSessionInfo sessionInfo)2909 public void setSessionVolume(int volume, RoutingSessionInfo sessionInfo) { 2910 Objects.requireNonNull(sessionInfo, "sessionInfo must not be null"); 2911 2912 if (sessionInfo.getVolumeHandling() == MediaRoute2Info.PLAYBACK_VOLUME_FIXED) { 2913 Log.w(TAG, "setSessionVolume: the route has fixed volume. Ignoring."); 2914 return; 2915 } 2916 if (volume < 0 || volume > sessionInfo.getVolumeMax()) { 2917 Log.w(TAG, "setSessionVolume: the target volume is out of range. Ignoring"); 2918 return; 2919 } 2920 2921 try { 2922 int requestId = mNextRequestId.getAndIncrement(); 2923 mMediaRouterService.setSessionVolumeWithManager( 2924 mClient, requestId, sessionInfo.getId(), volume); 2925 } catch (RemoteException ex) { 2926 throw ex.rethrowFromSystemServer(); 2927 } 2928 } 2929 2930 /** 2931 * Returns an exact copy of the routes. Individual {@link RouteDiscoveryPreference 2932 * preferences} do not apply to proxy routers. 2933 */ 2934 @Override filterRoutesWithIndividualPreference( List<MediaRoute2Info> routes, RouteDiscoveryPreference discoveryPreference)2935 public List<MediaRoute2Info> filterRoutesWithIndividualPreference( 2936 List<MediaRoute2Info> routes, RouteDiscoveryPreference discoveryPreference) { 2937 // Individual discovery preferences do not apply for the system router. 2938 return new ArrayList<>(routes); 2939 } 2940 2941 /** 2942 * Adds a {@linkplain MediaRoute2Info route} to the routing session's {@linkplain 2943 * RoutingSessionInfo#getSelectedRoutes() selected route list}. 2944 * 2945 * <p>Upon success, {@link #onSessionUpdated(RoutingSessionInfo)} is invoked. Failed 2946 * requests are silently ignored. 2947 * 2948 * <p>The {@linkplain RoutingSessionInfo#getSelectedRoutes() selected routes list} of a 2949 * routing session contains the group of devices playing media for that {@linkplain 2950 * RoutingSessionInfo session}. 2951 * 2952 * <p>The given route must not be already selected and must be listed in the session's 2953 * {@linkplain RoutingSessionInfo#getSelectableRoutes() selectable routes}. Otherwise, the 2954 * request will be ignored. 2955 * 2956 * <p>This method should not be confused with {@link #transfer(RoutingSessionInfo, 2957 * MediaRoute2Info)}. 2958 * 2959 * @see RoutingSessionInfo#getSelectedRoutes() 2960 * @see RoutingSessionInfo#getSelectableRoutes() 2961 */ 2962 @Override selectRoute(MediaRoute2Info route, RoutingSessionInfo sessionInfo)2963 public void selectRoute(MediaRoute2Info route, RoutingSessionInfo sessionInfo) { 2964 Objects.requireNonNull(sessionInfo, "sessionInfo must not be null"); 2965 Objects.requireNonNull(route, "route must not be null"); 2966 2967 if (sessionInfo.getSelectedRoutes().contains(route.getId())) { 2968 Log.w(TAG, "Ignoring selecting a route that is already selected. route=" + route); 2969 return; 2970 } 2971 2972 if (!sessionInfo.getSelectableRoutes().contains(route.getId())) { 2973 Log.w(TAG, "Ignoring selecting a non-selectable route=" + route); 2974 return; 2975 } 2976 2977 try { 2978 int requestId = mNextRequestId.getAndIncrement(); 2979 mMediaRouterService.selectRouteWithManager( 2980 mClient, requestId, sessionInfo.getId(), route); 2981 } catch (RemoteException ex) { 2982 throw ex.rethrowFromSystemServer(); 2983 } 2984 } 2985 2986 /** 2987 * Removes a route from a session's {@linkplain RoutingSessionInfo#getSelectedRoutes() 2988 * selected routes list}. Calls {@link #onSessionUpdated(RoutingSessionInfo)} on success. 2989 * 2990 * <p>The given route must be selected and must be listed in the session's {@linkplain 2991 * RoutingSessionInfo#getDeselectableRoutes() deselectable route list}. Otherwise, the 2992 * request will be ignored. 2993 * 2994 * @see RoutingSessionInfo#getSelectedRoutes() 2995 * @see RoutingSessionInfo#getDeselectableRoutes() 2996 */ 2997 @Override deselectRoute(MediaRoute2Info route, RoutingSessionInfo sessionInfo)2998 public void deselectRoute(MediaRoute2Info route, RoutingSessionInfo sessionInfo) { 2999 Objects.requireNonNull(sessionInfo, "sessionInfo must not be null"); 3000 Objects.requireNonNull(route, "route must not be null"); 3001 3002 if (!sessionInfo.getSelectedRoutes().contains(route.getId())) { 3003 Log.w(TAG, "Ignoring deselecting a route that is not selected. route=" + route); 3004 return; 3005 } 3006 3007 if (!sessionInfo.getDeselectableRoutes().contains(route.getId())) { 3008 Log.w(TAG, "Ignoring deselecting a non-deselectable route=" + route); 3009 return; 3010 } 3011 3012 try { 3013 int requestId = mNextRequestId.getAndIncrement(); 3014 mMediaRouterService.deselectRouteWithManager( 3015 mClient, requestId, sessionInfo.getId(), route); 3016 } catch (RemoteException ex) { 3017 throw ex.rethrowFromSystemServer(); 3018 } 3019 } 3020 3021 @Override releaseSession( boolean shouldReleaseSession, boolean shouldNotifyStop, RoutingController controller)3022 public void releaseSession( 3023 boolean shouldReleaseSession, 3024 boolean shouldNotifyStop, 3025 RoutingController controller) { 3026 releaseSession(controller.getRoutingSessionInfo()); 3027 } 3028 3029 @Override wasTransferredBySelf(RoutingSessionInfo sessionInfo)3030 public boolean wasTransferredBySelf(RoutingSessionInfo sessionInfo) { 3031 UserHandle transferInitiatorUserHandle = sessionInfo.getTransferInitiatorUserHandle(); 3032 String transferInitiatorPackageName = sessionInfo.getTransferInitiatorPackageName(); 3033 return Objects.equals(mClientUser, transferInitiatorUserHandle) 3034 && Objects.equals(mClientPackageName, transferInitiatorPackageName); 3035 } 3036 3037 /** 3038 * Retrieves the system session info for the given package. 3039 * 3040 * <p>The returned routing session is guaranteed to have a non-null {@link 3041 * RoutingSessionInfo#getClientPackageName() client package name}. 3042 * 3043 * <p>Extracted into a static method to allow calling this from the constructor. 3044 */ getSystemSessionInfoImpl( @onNull IMediaRouterService service, @NonNull String callerPackageName, @NonNull String clientPackageName)3045 /* package */ static RoutingSessionInfo getSystemSessionInfoImpl( 3046 @NonNull IMediaRouterService service, 3047 @NonNull String callerPackageName, 3048 @NonNull String clientPackageName) { 3049 try { 3050 return service.getSystemSessionInfoForPackage(callerPackageName, clientPackageName); 3051 } catch (RemoteException ex) { 3052 throw ex.rethrowFromSystemServer(); 3053 } 3054 } 3055 3056 /** 3057 * Requests the release of a {@linkplain RoutingSessionInfo routing session}. Calls {@link 3058 * #onSessionReleasedOnHandler(RoutingSessionInfo)} on success. 3059 * 3060 * <p>Once released, a routing session ignores incoming requests. 3061 */ releaseSession(@onNull RoutingSessionInfo sessionInfo)3062 private void releaseSession(@NonNull RoutingSessionInfo sessionInfo) { 3063 Objects.requireNonNull(sessionInfo, "sessionInfo must not be null"); 3064 3065 try { 3066 int requestId = mNextRequestId.getAndIncrement(); 3067 mMediaRouterService.releaseSessionWithManager( 3068 mClient, requestId, sessionInfo.getId()); 3069 } catch (RemoteException ex) { 3070 throw ex.rethrowFromSystemServer(); 3071 } 3072 } 3073 createTransferRequest( @onNull RoutingSessionInfo session, @NonNull MediaRoute2Info route)3074 private int createTransferRequest( 3075 @NonNull RoutingSessionInfo session, @NonNull MediaRoute2Info route) { 3076 int requestId = mNextRequestId.getAndIncrement(); 3077 MediaRouter2Manager.TransferRequest transferRequest = 3078 new MediaRouter2Manager.TransferRequest(requestId, session, route); 3079 mTransferRequests.add(transferRequest); 3080 3081 Message timeoutMessage = 3082 obtainMessage( 3083 ProxyMediaRouter2Impl::handleTransferTimeout, this, transferRequest); 3084 mHandler.sendMessageDelayed(timeoutMessage, TRANSFER_TIMEOUT_MS); 3085 return requestId; 3086 } 3087 handleTransferTimeout(MediaRouter2Manager.TransferRequest request)3088 private void handleTransferTimeout(MediaRouter2Manager.TransferRequest request) { 3089 boolean removed = mTransferRequests.remove(request); 3090 if (removed) { 3091 this.onTransferFailed(request.mOldSessionInfo, request.mTargetRoute); 3092 } 3093 } 3094 3095 /** 3096 * Returns the {@linkplain RoutingSessionInfo routing sessions} associated with {@link 3097 * #mClientPackageName}. The first element of the returned list is the {@linkplain 3098 * #getSystemSessionInfo() system routing session}. 3099 * 3100 * @see #getSystemSessionInfo() 3101 */ 3102 @NonNull getRoutingSessions()3103 private List<RoutingSessionInfo> getRoutingSessions() { 3104 List<RoutingSessionInfo> sessions = new ArrayList<>(); 3105 sessions.add(getSystemSessionInfo()); 3106 3107 List<RoutingSessionInfo> remoteSessions; 3108 try { 3109 remoteSessions = mMediaRouterService.getRemoteSessions(mClient); 3110 } catch (RemoteException ex) { 3111 throw ex.rethrowFromSystemServer(); 3112 } 3113 3114 for (RoutingSessionInfo sessionInfo : remoteSessions) { 3115 if (TextUtils.equals(sessionInfo.getClientPackageName(), mClientPackageName)) { 3116 sessions.add(sessionInfo); 3117 } 3118 } 3119 return sessions; 3120 } 3121 onTransferred( @onNull RoutingSessionInfo oldSession, @NonNull RoutingSessionInfo newSession)3122 private void onTransferred( 3123 @NonNull RoutingSessionInfo oldSession, @NonNull RoutingSessionInfo newSession) { 3124 if (!isSessionRelatedToTargetPackageName(oldSession) 3125 || !isSessionRelatedToTargetPackageName(newSession)) { 3126 return; 3127 } 3128 3129 RoutingController oldController; 3130 if (oldSession.isSystemSession()) { 3131 mSystemController.setRoutingSessionInfo( 3132 ensureClientPackageNameForSystemSession(oldSession, mClientPackageName)); 3133 oldController = mSystemController; 3134 } else { 3135 oldController = new RoutingController(oldSession); 3136 } 3137 3138 RoutingController newController; 3139 if (newSession.isSystemSession()) { 3140 mSystemController.setRoutingSessionInfo( 3141 ensureClientPackageNameForSystemSession(newSession, mClientPackageName)); 3142 newController = mSystemController; 3143 } else { 3144 newController = new RoutingController(newSession); 3145 } 3146 3147 notifyTransfer(oldController, newController); 3148 } 3149 onTransferFailed( @onNull RoutingSessionInfo session, @NonNull MediaRoute2Info route)3150 private void onTransferFailed( 3151 @NonNull RoutingSessionInfo session, @NonNull MediaRoute2Info route) { 3152 if (!isSessionRelatedToTargetPackageName(session)) { 3153 return; 3154 } 3155 notifyTransferFailure(route); 3156 } 3157 onSessionUpdated(@onNull RoutingSessionInfo session)3158 private void onSessionUpdated(@NonNull RoutingSessionInfo session) { 3159 if (!isSessionRelatedToTargetPackageName(session)) { 3160 return; 3161 } 3162 3163 RoutingController controller; 3164 if (session.isSystemSession()) { 3165 mSystemController.setRoutingSessionInfo( 3166 ensureClientPackageNameForSystemSession(session, mClientPackageName)); 3167 controller = mSystemController; 3168 } else { 3169 controller = new RoutingController(session); 3170 } 3171 notifyControllerUpdated(controller); 3172 } 3173 3174 /** 3175 * Returns {@code true} if the session is a system session or if its client package name 3176 * matches the proxy router's target package name. 3177 */ isSessionRelatedToTargetPackageName(@onNull RoutingSessionInfo session)3178 private boolean isSessionRelatedToTargetPackageName(@NonNull RoutingSessionInfo session) { 3179 return session.isSystemSession() 3180 || TextUtils.equals(getClientPackageName(), session.getClientPackageName()); 3181 } 3182 onSessionCreatedOnHandler( int requestId, @NonNull RoutingSessionInfo sessionInfo)3183 private void onSessionCreatedOnHandler( 3184 int requestId, @NonNull RoutingSessionInfo sessionInfo) { 3185 MediaRouter2Manager.TransferRequest matchingRequest = null; 3186 for (MediaRouter2Manager.TransferRequest request : mTransferRequests) { 3187 if (request.mRequestId == requestId) { 3188 matchingRequest = request; 3189 break; 3190 } 3191 } 3192 3193 if (matchingRequest == null) { 3194 return; 3195 } 3196 3197 mTransferRequests.remove(matchingRequest); 3198 3199 MediaRoute2Info requestedRoute = matchingRequest.mTargetRoute; 3200 3201 if (!sessionInfo.getSelectedRoutes().contains(requestedRoute.getId())) { 3202 Log.w( 3203 TAG, 3204 "The session does not contain the requested route. " 3205 + "(requestedRouteId=" 3206 + requestedRoute.getId() 3207 + ", actualRoutes=" 3208 + sessionInfo.getSelectedRoutes() 3209 + ")"); 3210 this.onTransferFailed(matchingRequest.mOldSessionInfo, requestedRoute); 3211 } else if (!TextUtils.equals( 3212 requestedRoute.getProviderId(), sessionInfo.getProviderId())) { 3213 Log.w( 3214 TAG, 3215 "The session's provider ID does not match the requested route's. " 3216 + "(requested route's providerId=" 3217 + requestedRoute.getProviderId() 3218 + ", actual providerId=" 3219 + sessionInfo.getProviderId() 3220 + ")"); 3221 this.onTransferFailed(matchingRequest.mOldSessionInfo, requestedRoute); 3222 } else { 3223 this.onTransferred(matchingRequest.mOldSessionInfo, sessionInfo); 3224 } 3225 } 3226 onSessionUpdatedOnHandler(@onNull RoutingSessionInfo updatedSession)3227 private void onSessionUpdatedOnHandler(@NonNull RoutingSessionInfo updatedSession) { 3228 for (MediaRouter2Manager.TransferRequest request : mTransferRequests) { 3229 String sessionId = request.mOldSessionInfo.getId(); 3230 if (!TextUtils.equals(sessionId, updatedSession.getId())) { 3231 continue; 3232 } 3233 3234 if (updatedSession.getSelectedRoutes().contains(request.mTargetRoute.getId())) { 3235 mTransferRequests.remove(request); 3236 break; 3237 } 3238 } 3239 this.onSessionUpdated(updatedSession); 3240 } 3241 onSessionReleasedOnHandler(@onNull RoutingSessionInfo session)3242 private void onSessionReleasedOnHandler(@NonNull RoutingSessionInfo session) { 3243 if (session.isSystemSession()) { 3244 Log.e(TAG, "onSessionReleasedOnHandler: Called on system session. Ignoring."); 3245 return; 3246 } 3247 3248 if (!TextUtils.equals(getClientPackageName(), session.getClientPackageName())) { 3249 return; 3250 } 3251 3252 notifyStop(new RoutingController(session, RoutingController.CONTROLLER_STATE_RELEASED)); 3253 } 3254 onDiscoveryPreferenceChangedOnHandler( @onNull String packageName, @Nullable RouteDiscoveryPreference preference)3255 private void onDiscoveryPreferenceChangedOnHandler( 3256 @NonNull String packageName, @Nullable RouteDiscoveryPreference preference) { 3257 if (!TextUtils.equals(getClientPackageName(), packageName)) { 3258 return; 3259 } 3260 3261 if (preference == null) { 3262 return; 3263 } 3264 synchronized (mLock) { 3265 if (Objects.equals(preference, mDiscoveryPreference)) { 3266 return; 3267 } 3268 mDiscoveryPreference = preference; 3269 updateFilteredRoutesLocked(); 3270 } 3271 notifyPreferredFeaturesChanged(preference.getPreferredFeatures()); 3272 } 3273 onRouteListingPreferenceChangedOnHandler( @onNull String packageName, @Nullable RouteListingPreference routeListingPreference)3274 private void onRouteListingPreferenceChangedOnHandler( 3275 @NonNull String packageName, 3276 @Nullable RouteListingPreference routeListingPreference) { 3277 if (!TextUtils.equals(getClientPackageName(), packageName)) { 3278 return; 3279 } 3280 3281 synchronized (mLock) { 3282 if (Objects.equals(mRouteListingPreference, routeListingPreference)) { 3283 return; 3284 } 3285 3286 mRouteListingPreference = routeListingPreference; 3287 } 3288 3289 notifyRouteListingPreferenceUpdated(routeListingPreference); 3290 } 3291 onRequestFailedOnHandler(int requestId, int reason)3292 private void onRequestFailedOnHandler(int requestId, int reason) { 3293 MediaRouter2Manager.TransferRequest matchingRequest = null; 3294 for (MediaRouter2Manager.TransferRequest request : mTransferRequests) { 3295 if (request.mRequestId == requestId) { 3296 matchingRequest = request; 3297 break; 3298 } 3299 } 3300 3301 if (matchingRequest != null) { 3302 mTransferRequests.remove(matchingRequest); 3303 onTransferFailed(matchingRequest.mOldSessionInfo, matchingRequest.mTargetRoute); 3304 } else { 3305 notifyRequestFailed(reason); 3306 } 3307 } 3308 onInvalidateInstanceOnHandler()3309 private void onInvalidateInstanceOnHandler() { 3310 Log.w( 3311 TAG, 3312 "MEDIA_ROUTING_CONTROL has been revoked for this package. Invalidating" 3313 + " instance."); 3314 // After this block, all following getInstance() calls should throw a SecurityException, 3315 // so no new onInstanceInvalidatedListeners can be registered to this instance. 3316 synchronized (sSystemRouterLock) { 3317 PackageNameUserHandlePair key = 3318 new PackageNameUserHandlePair(mClientPackageName, mClientUser); 3319 sAppToProxyRouterMap.remove(key); 3320 } 3321 3322 synchronized (mLock) { 3323 for (InstanceInvalidatedCallbackRecord record : 3324 mInstanceInvalidatedCallbackRecords) { 3325 record.executor.execute(record.runnable); 3326 } 3327 } 3328 mRouteCallbackRecords.clear(); 3329 mControllerCallbackRecords.clear(); 3330 mTransferCallbackRecords.clear(); 3331 } 3332 3333 private class Client extends IMediaRouter2Manager.Stub { 3334 3335 @Override notifySessionCreated(int requestId, RoutingSessionInfo routingSessionInfo)3336 public void notifySessionCreated(int requestId, RoutingSessionInfo routingSessionInfo) { 3337 mHandler.sendMessage( 3338 obtainMessage( 3339 ProxyMediaRouter2Impl::onSessionCreatedOnHandler, 3340 ProxyMediaRouter2Impl.this, 3341 requestId, 3342 routingSessionInfo)); 3343 } 3344 3345 @Override notifySessionUpdated(RoutingSessionInfo routingSessionInfo)3346 public void notifySessionUpdated(RoutingSessionInfo routingSessionInfo) { 3347 mHandler.sendMessage( 3348 obtainMessage( 3349 ProxyMediaRouter2Impl::onSessionUpdatedOnHandler, 3350 ProxyMediaRouter2Impl.this, 3351 routingSessionInfo)); 3352 } 3353 3354 @Override notifySessionReleased(RoutingSessionInfo routingSessionInfo)3355 public void notifySessionReleased(RoutingSessionInfo routingSessionInfo) { 3356 mHandler.sendMessage( 3357 obtainMessage( 3358 ProxyMediaRouter2Impl::onSessionReleasedOnHandler, 3359 ProxyMediaRouter2Impl.this, 3360 routingSessionInfo)); 3361 } 3362 3363 @Override notifyDiscoveryPreferenceChanged( String packageName, RouteDiscoveryPreference routeDiscoveryPreference)3364 public void notifyDiscoveryPreferenceChanged( 3365 String packageName, RouteDiscoveryPreference routeDiscoveryPreference) { 3366 mHandler.sendMessage( 3367 obtainMessage( 3368 ProxyMediaRouter2Impl::onDiscoveryPreferenceChangedOnHandler, 3369 ProxyMediaRouter2Impl.this, 3370 packageName, 3371 routeDiscoveryPreference)); 3372 } 3373 3374 @Override notifyRouteListingPreferenceChange( String packageName, RouteListingPreference routeListingPreference)3375 public void notifyRouteListingPreferenceChange( 3376 String packageName, RouteListingPreference routeListingPreference) { 3377 mHandler.sendMessage( 3378 obtainMessage( 3379 ProxyMediaRouter2Impl::onRouteListingPreferenceChangedOnHandler, 3380 ProxyMediaRouter2Impl.this, 3381 packageName, 3382 routeListingPreference)); 3383 } 3384 3385 @Override notifyRoutesUpdated(List<MediaRoute2Info> routes)3386 public void notifyRoutesUpdated(List<MediaRoute2Info> routes) { 3387 mHandler.sendMessage( 3388 obtainMessage( 3389 MediaRouter2::updateRoutesOnHandler, MediaRouter2.this, routes)); 3390 } 3391 3392 @Override notifyRequestFailed(int requestId, int reason)3393 public void notifyRequestFailed(int requestId, int reason) { 3394 mHandler.sendMessage( 3395 obtainMessage( 3396 ProxyMediaRouter2Impl::onRequestFailedOnHandler, 3397 ProxyMediaRouter2Impl.this, 3398 requestId, 3399 reason)); 3400 } 3401 3402 @Override invalidateInstance()3403 public void invalidateInstance() { 3404 mHandler.sendMessage( 3405 obtainMessage( 3406 ProxyMediaRouter2Impl::onInvalidateInstanceOnHandler, 3407 ProxyMediaRouter2Impl.this)); 3408 } 3409 } 3410 } 3411 3412 /** 3413 * Implements logic specific to local {@link MediaRouter2} instances. 3414 * 3415 * <p>Local routers allow an app to control its own routing without any special permissions. 3416 * Apps can obtain an instance by calling {@link #getInstance(Context)}. 3417 */ 3418 private class LocalMediaRouter2Impl implements MediaRouter2Impl { 3419 private final String mPackageName; 3420 LocalMediaRouter2Impl(@onNull String packageName)3421 LocalMediaRouter2Impl(@NonNull String packageName) { 3422 mPackageName = packageName; 3423 } 3424 3425 /** 3426 * No-op. Local routers cannot explicitly control route scanning. 3427 * 3428 * <p>Local routers can control scanning indirectly through {@link 3429 * #registerRouteCallback(Executor, RouteCallback, RouteDiscoveryPreference)}. 3430 */ 3431 @Override startScan()3432 public void startScan() { 3433 // Do nothing. 3434 } 3435 3436 /** 3437 * No-op. Local routers cannot explicitly control route scanning. 3438 * 3439 * <p>Local routers can control scanning indirectly through {@link 3440 * #registerRouteCallback(Executor, RouteCallback, RouteDiscoveryPreference)}. 3441 */ 3442 @Override stopScan()3443 public void stopScan() { 3444 // Do nothing. 3445 } 3446 3447 @Override 3448 @GuardedBy("mLock") updateScanningState(int scanningState)3449 public void updateScanningState(int scanningState) throws RemoteException { 3450 if (scanningState != SCANNING_STATE_NOT_SCANNING) { 3451 registerRouterStubIfNeededLocked(); 3452 } 3453 mMediaRouterService.updateScanningStateWithRouter2(mStub, scanningState); 3454 if (scanningState == SCANNING_STATE_NOT_SCANNING) { 3455 unregisterRouterStubIfNeededLocked(/* isScanningStopping */ true); 3456 } 3457 } 3458 3459 /** 3460 * Returns {@code null}. The client package name is only associated to proxy {@link 3461 * MediaRouter2} instances. 3462 */ 3463 @Override getClientPackageName()3464 public String getClientPackageName() { 3465 return null; 3466 } 3467 3468 @Override getPackageName()3469 public String getPackageName() { 3470 return mPackageName; 3471 } 3472 3473 @Override getSystemSessionInfo()3474 public RoutingSessionInfo getSystemSessionInfo() { 3475 RoutingSessionInfo currentSystemSessionInfo = null; 3476 try { 3477 currentSystemSessionInfo = ensureClientPackageNameForSystemSession( 3478 mMediaRouterService.getSystemSessionInfo(), mContext.getPackageName()); 3479 } catch (RemoteException ex) { 3480 ex.rethrowFromSystemServer(); 3481 } 3482 return currentSystemSessionInfo; 3483 } 3484 3485 @Override createRouteCallbackRecord( Executor executor, RouteCallback routeCallback, RouteDiscoveryPreference preference)3486 public RouteCallbackRecord createRouteCallbackRecord( 3487 Executor executor, 3488 RouteCallback routeCallback, 3489 RouteDiscoveryPreference preference) { 3490 return new RouteCallbackRecord(executor, routeCallback, preference); 3491 } 3492 3493 @Override registerRouteCallback()3494 public void registerRouteCallback() { 3495 synchronized (mLock) { 3496 try { 3497 registerRouterStubIfNeededLocked(); 3498 3499 if (updateDiscoveryPreferenceIfNeededLocked()) { 3500 mMediaRouterService.setDiscoveryRequestWithRouter2( 3501 mStub, mDiscoveryPreference); 3502 } 3503 } catch (RemoteException ex) { 3504 ex.rethrowFromSystemServer(); 3505 } 3506 } 3507 } 3508 3509 @Override unregisterRouteCallback()3510 public void unregisterRouteCallback() { 3511 synchronized (mLock) { 3512 if (mStub == null) { 3513 return; 3514 } 3515 3516 try { 3517 if (updateDiscoveryPreferenceIfNeededLocked()) { 3518 mMediaRouterService.setDiscoveryRequestWithRouter2( 3519 mStub, mDiscoveryPreference); 3520 } 3521 3522 unregisterRouterStubIfNeededLocked(/* isScanningStopping */ false); 3523 3524 } catch (RemoteException ex) { 3525 Log.e(TAG, "unregisterRouteCallback: Unable to set discovery request.", ex); 3526 } 3527 } 3528 } 3529 3530 @Override setRouteListingPreference(@ullable RouteListingPreference preference)3531 public void setRouteListingPreference(@Nullable RouteListingPreference preference) { 3532 synchronized (mLock) { 3533 if (Objects.equals(mRouteListingPreference, preference)) { 3534 // Nothing changed. We return early to save a call to the system server. 3535 return; 3536 } 3537 mRouteListingPreference = preference; 3538 try { 3539 registerRouterStubIfNeededLocked(); 3540 mMediaRouterService.setRouteListingPreference(mStub, mRouteListingPreference); 3541 } catch (RemoteException ex) { 3542 ex.rethrowFromSystemServer(); 3543 } 3544 notifyRouteListingPreferenceUpdated(preference); 3545 } 3546 } 3547 3548 @Override showSystemOutputSwitcher()3549 public boolean showSystemOutputSwitcher() { 3550 synchronized (mLock) { 3551 try { 3552 return mMediaRouterService.showMediaOutputSwitcherWithRouter2(mPackageName); 3553 } catch (RemoteException ex) { 3554 ex.rethrowFromSystemServer(); 3555 } 3556 } 3557 return false; 3558 } 3559 3560 /** 3561 * Returns {@link Collections#emptyList()}. Local routes can only access routes related to 3562 * their {@link RouteDiscoveryPreference} through {@link #getRoutes()}. 3563 */ 3564 @Override getAllRoutes()3565 public List<MediaRoute2Info> getAllRoutes() { 3566 return Collections.emptyList(); 3567 } 3568 3569 @Override setOnGetControllerHintsListener(OnGetControllerHintsListener listener)3570 public void setOnGetControllerHintsListener(OnGetControllerHintsListener listener) { 3571 mOnGetControllerHintsListener = listener; 3572 } 3573 3574 @Override transferTo(MediaRoute2Info route)3575 public void transferTo(MediaRoute2Info route) { 3576 Log.v(TAG, "Transferring to route: " + route); 3577 3578 boolean routeFound; 3579 synchronized (mLock) { 3580 // TODO: Check thread-safety 3581 routeFound = mRoutes.containsKey(route.getId()); 3582 } 3583 if (!routeFound) { 3584 notifyTransferFailure(route); 3585 return; 3586 } 3587 3588 RoutingController controller = getCurrentController(); 3589 if (!controller.tryTransferWithinProvider(route)) { 3590 requestCreateController(controller, route, MANAGER_REQUEST_ID_NONE); 3591 } 3592 } 3593 3594 @Override stop()3595 public void stop() { 3596 getCurrentController().release(); 3597 } 3598 3599 /** 3600 * No-op. Local routers cannot request transfers of specific {@link RoutingSessionInfo}. 3601 * This operation is only available to proxy routers. 3602 * 3603 * <p>Local routers can only transfer the current {@link RoutingSessionInfo} using {@link 3604 * #transferTo(MediaRoute2Info)}. 3605 */ 3606 @Override transfer( @onNull RoutingSessionInfo sessionInfo, @NonNull MediaRoute2Info route)3607 public void transfer( 3608 @NonNull RoutingSessionInfo sessionInfo, @NonNull MediaRoute2Info route) { 3609 // Do nothing. 3610 } 3611 3612 @Override getControllers()3613 public List<RoutingController> getControllers() { 3614 List<RoutingController> result = new ArrayList<>(); 3615 3616 result.add(0, mSystemController); 3617 synchronized (mLock) { 3618 result.addAll(mNonSystemRoutingControllers.values()); 3619 } 3620 return result; 3621 } 3622 3623 /** Local routers cannot modify the volume of specific routes. */ 3624 @Override setRouteVolume(MediaRoute2Info route, int volume)3625 public void setRouteVolume(MediaRoute2Info route, int volume) { 3626 throw new UnsupportedOperationException( 3627 "setRouteVolume is only supported by proxy routers. See javadoc."); 3628 // If this API needs to be public, use IMediaRouterService#setRouteVolumeWithRouter2() 3629 } 3630 3631 @Override setSessionVolume(int volume, RoutingSessionInfo sessionInfo)3632 public void setSessionVolume(int volume, RoutingSessionInfo sessionInfo) { 3633 MediaRouter2Stub stub; 3634 synchronized (mLock) { 3635 stub = mStub; 3636 } 3637 if (stub != null) { 3638 try { 3639 mMediaRouterService.setSessionVolumeWithRouter2( 3640 stub, sessionInfo.getId(), volume); 3641 } catch (RemoteException ex) { 3642 Log.e(TAG, "setVolume: Failed to deliver request.", ex); 3643 } 3644 } 3645 } 3646 3647 @Override filterRoutesWithIndividualPreference( List<MediaRoute2Info> routes, RouteDiscoveryPreference discoveryPreference)3648 public List<MediaRoute2Info> filterRoutesWithIndividualPreference( 3649 List<MediaRoute2Info> routes, RouteDiscoveryPreference discoveryPreference) { 3650 List<MediaRoute2Info> filteredRoutes = new ArrayList<>(); 3651 for (MediaRoute2Info route : routes) { 3652 if (!route.hasAnyFeatures(discoveryPreference.getPreferredFeatures())) { 3653 continue; 3654 } 3655 if (!discoveryPreference.getAllowedPackages().isEmpty() 3656 && (route.getPackageName() == null 3657 || !discoveryPreference 3658 .getAllowedPackages() 3659 .contains(route.getPackageName()))) { 3660 continue; 3661 } 3662 filteredRoutes.add(route); 3663 } 3664 return filteredRoutes; 3665 } 3666 3667 @Override selectRoute(MediaRoute2Info route, RoutingSessionInfo sessionInfo)3668 public void selectRoute(MediaRoute2Info route, RoutingSessionInfo sessionInfo) { 3669 MediaRouter2Stub stub; 3670 synchronized (mLock) { 3671 stub = mStub; 3672 } 3673 if (stub != null) { 3674 try { 3675 mMediaRouterService.selectRouteWithRouter2(stub, sessionInfo.getId(), route); 3676 } catch (RemoteException ex) { 3677 Log.e(TAG, "Unable to select route for session.", ex); 3678 } 3679 } 3680 } 3681 3682 @Override deselectRoute(MediaRoute2Info route, RoutingSessionInfo sessionInfo)3683 public void deselectRoute(MediaRoute2Info route, RoutingSessionInfo sessionInfo) { 3684 MediaRouter2Stub stub; 3685 synchronized (mLock) { 3686 stub = mStub; 3687 } 3688 if (stub != null) { 3689 try { 3690 mMediaRouterService.deselectRouteWithRouter2(stub, sessionInfo.getId(), route); 3691 } catch (RemoteException ex) { 3692 Log.e(TAG, "Unable to deselect route from session.", ex); 3693 } 3694 } 3695 } 3696 3697 @Override releaseSession( boolean shouldReleaseSession, boolean shouldNotifyStop, RoutingController controller)3698 public void releaseSession( 3699 boolean shouldReleaseSession, 3700 boolean shouldNotifyStop, 3701 RoutingController controller) { 3702 synchronized (mLock) { 3703 mNonSystemRoutingControllers.remove(controller.getId(), controller); 3704 3705 if (shouldReleaseSession && mStub != null) { 3706 try { 3707 mMediaRouterService.releaseSessionWithRouter2(mStub, controller.getId()); 3708 } catch (RemoteException ex) { 3709 ex.rethrowFromSystemServer(); 3710 } 3711 } 3712 3713 if (shouldNotifyStop) { 3714 mHandler.sendMessage( 3715 obtainMessage(MediaRouter2::notifyStop, MediaRouter2.this, controller)); 3716 } 3717 3718 try { 3719 unregisterRouterStubIfNeededLocked(/* isScanningStopping */ false); 3720 } catch (RemoteException ex) { 3721 ex.rethrowFromSystemServer(); 3722 } 3723 3724 } 3725 } 3726 3727 @Override wasTransferredBySelf(RoutingSessionInfo sessionInfo)3728 public boolean wasTransferredBySelf(RoutingSessionInfo sessionInfo) { 3729 UserHandle transferInitiatorUserHandle = sessionInfo.getTransferInitiatorUserHandle(); 3730 String transferInitiatorPackageName = sessionInfo.getTransferInitiatorPackageName(); 3731 return Objects.equals(Process.myUserHandle(), transferInitiatorUserHandle) 3732 && Objects.equals(mContext.getPackageName(), transferInitiatorPackageName); 3733 } 3734 3735 @GuardedBy("mLock") registerRouterStubIfNeededLocked()3736 private void registerRouterStubIfNeededLocked() throws RemoteException { 3737 if (mStub == null) { 3738 MediaRouter2Stub stub = new MediaRouter2Stub(); 3739 mMediaRouterService.registerRouter2(stub, mPackageName); 3740 mStub = stub; 3741 } 3742 } 3743 3744 @GuardedBy("mLock") unregisterRouterStubIfNeededLocked(boolean isScanningStopping)3745 private void unregisterRouterStubIfNeededLocked(boolean isScanningStopping) 3746 throws RemoteException { 3747 if (mStub != null 3748 && mRouteCallbackRecords.isEmpty() 3749 && mNonSystemRoutingControllers.isEmpty() 3750 && (mScanRequestsMap.size() == 0 || isScanningStopping)) { 3751 mMediaRouterService.unregisterRouter2(mStub); 3752 mStub = null; 3753 } 3754 } 3755 } 3756 } 3757