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