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 21 import android.Manifest; 22 import android.annotation.CallbackExecutor; 23 import android.annotation.NonNull; 24 import android.annotation.Nullable; 25 import android.annotation.RequiresPermission; 26 import android.annotation.SystemApi; 27 import android.annotation.TestApi; 28 import android.content.Context; 29 import android.content.pm.PackageManager; 30 import android.os.Bundle; 31 import android.os.Handler; 32 import android.os.Looper; 33 import android.os.RemoteException; 34 import android.os.ServiceManager; 35 import android.text.TextUtils; 36 import android.util.ArrayMap; 37 import android.util.Log; 38 39 import com.android.internal.annotations.GuardedBy; 40 41 import java.util.ArrayList; 42 import java.util.Collections; 43 import java.util.List; 44 import java.util.Map; 45 import java.util.Objects; 46 import java.util.concurrent.CopyOnWriteArrayList; 47 import java.util.concurrent.Executor; 48 import java.util.concurrent.atomic.AtomicInteger; 49 import java.util.stream.Collectors; 50 51 /** 52 * This API is not generally intended for third party application developers. 53 * Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a> 54 <a href="{@docRoot}reference/androidx/mediarouter/media/package-summary.html">Media Router 55 * Library</a> for consistent behavior across all devices. 56 * 57 * Media Router 2 allows applications to control the routing of media channels 58 * and streams from the current device to remote speakers and devices. 59 */ 60 // TODO(b/157873330): Add method names at the beginning of log messages. (e.g. selectRoute) 61 // Not only MediaRouter2, but also to service / manager / provider. 62 // TODO: ensure thread-safe and document it 63 public final class MediaRouter2 { 64 private static final String TAG = "MR2"; 65 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 66 private static final Object sSystemRouterLock = new Object(); 67 private static final Object sRouterLock = new Object(); 68 69 // The maximum time for the old routing controller available after transfer. 70 private static final int TRANSFER_TIMEOUT_MS = 30_000; 71 // The manager request ID representing that no manager is involved. 72 private static final long MANAGER_REQUEST_ID_NONE = MediaRoute2ProviderService.REQUEST_ID_NONE; 73 74 @GuardedBy("sSystemRouterLock") 75 private static Map<String, MediaRouter2> sSystemMediaRouter2Map = new ArrayMap<>(); 76 private static MediaRouter2Manager sManager; 77 78 @GuardedBy("sRouterLock") 79 private static MediaRouter2 sInstance; 80 81 private final Context mContext; 82 private final IMediaRouterService mMediaRouterService; 83 private final Object mLock = new Object(); 84 85 private final CopyOnWriteArrayList<RouteCallbackRecord> mRouteCallbackRecords = 86 new CopyOnWriteArrayList<>(); 87 private final CopyOnWriteArrayList<TransferCallbackRecord> mTransferCallbackRecords = 88 new CopyOnWriteArrayList<>(); 89 private final CopyOnWriteArrayList<ControllerCallbackRecord> mControllerCallbackRecords = 90 new CopyOnWriteArrayList<>(); 91 92 private final CopyOnWriteArrayList<ControllerCreationRequest> mControllerCreationRequests = 93 new CopyOnWriteArrayList<>(); 94 95 // TODO: Specify the fields that are only used (or not used) by system media router. 96 private final String mClientPackageName; 97 final ManagerCallback mManagerCallback; 98 99 private final String mPackageName; 100 101 @GuardedBy("mLock") 102 final Map<String, MediaRoute2Info> mRoutes = new ArrayMap<>(); 103 104 final RoutingController mSystemController; 105 106 @GuardedBy("mLock") 107 private RouteDiscoveryPreference mDiscoveryPreference = RouteDiscoveryPreference.EMPTY; 108 109 // TODO: Make MediaRouter2 is always connected to the MediaRouterService. 110 @GuardedBy("mLock") 111 MediaRouter2Stub mStub; 112 113 @GuardedBy("mLock") 114 private final Map<String, RoutingController> mNonSystemRoutingControllers = new ArrayMap<>(); 115 116 private final AtomicInteger mNextRequestId = new AtomicInteger(1); 117 118 final Handler mHandler; 119 @GuardedBy("mLock") 120 private boolean mShouldUpdateRoutes = true; 121 private volatile List<MediaRoute2Info> mFilteredRoutes = Collections.emptyList(); 122 private volatile OnGetControllerHintsListener mOnGetControllerHintsListener; 123 124 /** 125 * Gets an instance of the media router associated with the context. 126 */ 127 @NonNull getInstance(@onNull Context context)128 public static MediaRouter2 getInstance(@NonNull Context context) { 129 Objects.requireNonNull(context, "context must not be null"); 130 synchronized (sRouterLock) { 131 if (sInstance == null) { 132 sInstance = new MediaRouter2(context.getApplicationContext()); 133 } 134 return sInstance; 135 } 136 } 137 138 /** 139 * Gets an instance of the system media router which controls the app's media routing. 140 * Returns {@code null} if the given package name is invalid. 141 * There are several things to note when using the media routers created with this method. 142 * <p> 143 * First of all, the discovery preference passed to {@link #registerRouteCallback} 144 * will have no effect. The callback will be called accordingly with the client app's 145 * discovery preference. Therefore, it is recommended to pass 146 * {@link RouteDiscoveryPreference#EMPTY} there. 147 * <p> 148 * Also, do not keep/compare the instances of the {@link RoutingController}, since they are 149 * always newly created with the latest session information whenever below methods are called: 150 * <ul> 151 * <li> {@link #getControllers()} </li> 152 * <li> {@link #getController(String)}} </li> 153 * <li> {@link TransferCallback#onTransfer(RoutingController, RoutingController)} </li> 154 * <li> {@link TransferCallback#onStop(RoutingController)} </li> 155 * <li> {@link ControllerCallback#onControllerUpdated(RoutingController)} </li> 156 * </ul> 157 * Therefore, in order to track the current routing status, keep the controller's ID instead, 158 * and use {@link #getController(String)} and {@link #getSystemController()} for 159 * getting controllers. 160 * <p> 161 * Finally, it will have no effect to call {@link #setOnGetControllerHintsListener}. 162 * 163 * @param clientPackageName the package name of the app to control 164 * @throws SecurityException if the caller doesn't have MODIFY_AUDIO_ROUTING permission. 165 * @hide 166 */ 167 @SystemApi 168 @RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL) 169 @Nullable getInstance(@onNull Context context, @NonNull String clientPackageName)170 public static MediaRouter2 getInstance(@NonNull Context context, 171 @NonNull String clientPackageName) { 172 Objects.requireNonNull(context, "context must not be null"); 173 Objects.requireNonNull(clientPackageName, "clientPackageName must not be null"); 174 175 // Note: Even though this check could be somehow bypassed, the other permission checks 176 // in system server will not allow MediaRouter2Manager to be registered. 177 IMediaRouterService serviceBinder = IMediaRouterService.Stub.asInterface( 178 ServiceManager.getService(Context.MEDIA_ROUTER_SERVICE)); 179 try { 180 // SecurityException will be thrown if there's no permission. 181 serviceBinder.enforceMediaContentControlPermission(); 182 } catch (RemoteException e) { 183 e.rethrowFromSystemServer(); 184 } 185 186 PackageManager pm = context.getPackageManager(); 187 try { 188 pm.getPackageInfo(clientPackageName, 0); 189 } catch (PackageManager.NameNotFoundException ex) { 190 Log.e(TAG, "Package " + clientPackageName + " not found. Ignoring."); 191 return null; 192 } 193 194 synchronized (sSystemRouterLock) { 195 MediaRouter2 instance = sSystemMediaRouter2Map.get(clientPackageName); 196 if (instance == null) { 197 if (sManager == null) { 198 sManager = MediaRouter2Manager.getInstance(context.getApplicationContext()); 199 } 200 instance = new MediaRouter2(context, clientPackageName); 201 sSystemMediaRouter2Map.put(clientPackageName, instance); 202 // Using direct executor here, since MediaRouter2Manager also posts 203 // to the main handler. 204 sManager.registerCallback(Runnable::run, instance.mManagerCallback); 205 } 206 return instance; 207 } 208 } 209 210 /** 211 * Starts scanning remote routes. 212 * <p> 213 * Route discovery can happen even when the {@link #startScan()} is not called. 214 * This is because the scanning could be started before by other apps. 215 * Therefore, calling this method after calling {@link #stopScan()} does not necessarily mean 216 * that the routes found before are removed and added again. 217 * <p> 218 * Use {@link RouteCallback} to get the route related events. 219 * <p> 220 * Note that calling start/stopScan is applied to all system routers in the same process. 221 * <p> 222 * This will be no-op for non-system media routers. 223 * 224 * @see #stopScan() 225 * @see #getInstance(Context, String) 226 * @hide 227 */ 228 @SystemApi 229 @RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL) startScan()230 public void startScan() { 231 if (isSystemRouter()) { 232 sManager.startScan(); 233 } 234 } 235 236 /** 237 * Stops scanning remote routes to reduce resource consumption. 238 * <p> 239 * Route discovery can be continued even after this method is called. 240 * This is because the scanning is only turned off when all the apps stop scanning. 241 * Therefore, calling this method does not necessarily mean the routes are removed. 242 * Also, for the same reason it does not mean that {@link RouteCallback#onRoutesAdded(List)} 243 * is not called afterwards. 244 * <p> 245 * Use {@link RouteCallback} to get the route related events. 246 * <p> 247 * Note that calling start/stopScan is applied to all system routers in the same process. 248 * <p> 249 * This will be no-op for non-system media routers. 250 * 251 * @see #startScan() 252 * @see #getInstance(Context, String) 253 * @hide 254 */ 255 @SystemApi 256 @RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL) stopScan()257 public void stopScan() { 258 if (isSystemRouter()) { 259 sManager.stopScan(); 260 } 261 } 262 MediaRouter2(Context appContext)263 private MediaRouter2(Context appContext) { 264 mContext = appContext; 265 mMediaRouterService = IMediaRouterService.Stub.asInterface( 266 ServiceManager.getService(Context.MEDIA_ROUTER_SERVICE)); 267 mPackageName = mContext.getPackageName(); 268 mHandler = new Handler(Looper.getMainLooper()); 269 270 List<MediaRoute2Info> currentSystemRoutes = null; 271 RoutingSessionInfo currentSystemSessionInfo = null; 272 try { 273 currentSystemRoutes = mMediaRouterService.getSystemRoutes(); 274 currentSystemSessionInfo = mMediaRouterService.getSystemSessionInfo(); 275 } catch (RemoteException ex) { 276 Log.e(TAG, "Unable to get current system's routes / session info", ex); 277 } 278 279 if (currentSystemRoutes == null || currentSystemRoutes.isEmpty()) { 280 throw new RuntimeException("Null or empty currentSystemRoutes. Something is wrong."); 281 } 282 283 if (currentSystemSessionInfo == null) { 284 throw new RuntimeException("Null currentSystemSessionInfo. Something is wrong."); 285 } 286 287 for (MediaRoute2Info route : currentSystemRoutes) { 288 mRoutes.put(route.getId(), route); 289 } 290 mSystemController = new SystemRoutingController(currentSystemSessionInfo); 291 292 // Only used by system MediaRouter2. 293 mClientPackageName = null; 294 mManagerCallback = null; 295 } 296 MediaRouter2(Context context, String clientPackageName)297 private MediaRouter2(Context context, String clientPackageName) { 298 mContext = context; 299 mClientPackageName = clientPackageName; 300 mManagerCallback = new ManagerCallback(); 301 mHandler = new Handler(Looper.getMainLooper()); 302 mSystemController = new SystemRoutingController( 303 ensureClientPackageNameForSystemSession(sManager.getSystemRoutingSession())); 304 mDiscoveryPreference = new RouteDiscoveryPreference.Builder( 305 sManager.getPreferredFeatures(clientPackageName), true).build(); 306 updateAllRoutesFromManager(); 307 308 // Only used by non-system MediaRouter2. 309 mMediaRouterService = null; 310 mPackageName = null; 311 } 312 313 /** 314 * Returns whether any route in {@code routeList} has a same unique ID with given route. 315 * 316 * @hide 317 */ checkRouteListContainsRouteId(@onNull List<MediaRoute2Info> routeList, @NonNull String routeId)318 static boolean checkRouteListContainsRouteId(@NonNull List<MediaRoute2Info> routeList, 319 @NonNull String routeId) { 320 for (MediaRoute2Info info : routeList) { 321 if (TextUtils.equals(routeId, info.getId())) { 322 return true; 323 } 324 } 325 return false; 326 } 327 328 /** 329 * Gets the client package name of the app which this media router controls. 330 * <p> 331 * This will return null for non-system media routers. 332 * 333 * @see #getInstance(Context, String) 334 * @hide 335 */ 336 @SystemApi 337 @Nullable getClientPackageName()338 public String getClientPackageName() { 339 return mClientPackageName; 340 } 341 342 /** 343 * Registers a callback to discover routes and to receive events when they change. 344 * <p> 345 * If the specified callback is already registered, its registration will be updated for the 346 * given {@link Executor executor} and {@link RouteDiscoveryPreference discovery preference}. 347 * </p> 348 */ registerRouteCallback(@onNull @allbackExecutor Executor executor, @NonNull RouteCallback routeCallback, @NonNull RouteDiscoveryPreference preference)349 public void registerRouteCallback(@NonNull @CallbackExecutor Executor executor, 350 @NonNull RouteCallback routeCallback, 351 @NonNull RouteDiscoveryPreference preference) { 352 Objects.requireNonNull(executor, "executor must not be null"); 353 Objects.requireNonNull(routeCallback, "callback must not be null"); 354 Objects.requireNonNull(preference, "preference must not be null"); 355 if (isSystemRouter()) { 356 preference = RouteDiscoveryPreference.EMPTY; 357 } 358 359 RouteCallbackRecord record = new RouteCallbackRecord(executor, routeCallback, preference); 360 361 mRouteCallbackRecords.remove(record); 362 // It can fail to add the callback record if another registration with the same callback 363 // is happening but it's okay because either this or the other registration should be done. 364 mRouteCallbackRecords.addIfAbsent(record); 365 366 if (isSystemRouter()) { 367 return; 368 } 369 370 synchronized (mLock) { 371 if (mStub == null) { 372 MediaRouter2Stub stub = new MediaRouter2Stub(); 373 try { 374 mMediaRouterService.registerRouter2(stub, mPackageName); 375 mStub = stub; 376 } catch (RemoteException ex) { 377 Log.e(TAG, "registerRouteCallback: Unable to register MediaRouter2.", ex); 378 } 379 } 380 if (mStub != null && updateDiscoveryPreferenceIfNeededLocked()) { 381 try { 382 mMediaRouterService.setDiscoveryRequestWithRouter2(mStub, mDiscoveryPreference); 383 } catch (RemoteException ex) { 384 Log.e(TAG, "registerRouteCallback: Unable to set discovery request.", ex); 385 } 386 } 387 } 388 } 389 390 /** 391 * Unregisters the given callback. The callback will no longer receive events. 392 * If the callback has not been added or been removed already, it is ignored. 393 * 394 * @param routeCallback the callback to unregister 395 * @see #registerRouteCallback 396 */ unregisterRouteCallback(@onNull RouteCallback routeCallback)397 public void unregisterRouteCallback(@NonNull RouteCallback routeCallback) { 398 Objects.requireNonNull(routeCallback, "callback must not be null"); 399 400 if (!mRouteCallbackRecords.remove( 401 new RouteCallbackRecord(null, routeCallback, null))) { 402 Log.w(TAG, "unregisterRouteCallback: Ignoring unknown callback"); 403 return; 404 } 405 406 if (isSystemRouter()) { 407 return; 408 } 409 410 synchronized (mLock) { 411 if (mStub == null) { 412 return; 413 } 414 if (updateDiscoveryPreferenceIfNeededLocked()) { 415 try { 416 mMediaRouterService.setDiscoveryRequestWithRouter2( 417 mStub, mDiscoveryPreference); 418 } catch (RemoteException ex) { 419 Log.e(TAG, "unregisterRouteCallback: Unable to set discovery request.", ex); 420 } 421 } 422 if (mRouteCallbackRecords.isEmpty() && mNonSystemRoutingControllers.isEmpty()) { 423 try { 424 mMediaRouterService.unregisterRouter2(mStub); 425 } catch (RemoteException ex) { 426 Log.e(TAG, "Unable to unregister media router.", ex); 427 } 428 mStub = null; 429 } 430 mShouldUpdateRoutes = true; 431 } 432 } 433 updateDiscoveryPreferenceIfNeededLocked()434 private boolean updateDiscoveryPreferenceIfNeededLocked() { 435 RouteDiscoveryPreference newDiscoveryPreference = new RouteDiscoveryPreference.Builder( 436 mRouteCallbackRecords.stream().map(record -> record.mPreference).collect( 437 Collectors.toList())).build(); 438 if (Objects.equals(mDiscoveryPreference, newDiscoveryPreference)) { 439 return false; 440 } 441 mDiscoveryPreference = newDiscoveryPreference; 442 mShouldUpdateRoutes = true; 443 return true; 444 } 445 446 /** 447 * Gets the list of all discovered routes. 448 * This list includes the routes that are not related to the client app. 449 * <p> 450 * This will return an empty list for non-system media routers. 451 * 452 * @hide 453 */ 454 @SystemApi 455 @NonNull getAllRoutes()456 public List<MediaRoute2Info> getAllRoutes() { 457 if (isSystemRouter()) { 458 return sManager.getAllRoutes(); 459 } 460 return Collections.emptyList(); 461 } 462 463 /** 464 * Gets the unmodifiable list of {@link MediaRoute2Info routes} currently 465 * known to the media router. 466 * <p> 467 * Please note that the list can be changed before callbacks are invoked. 468 * </p> 469 * @return the list of routes that contains at least one of the route features in discovery 470 * preferences registered by the application 471 */ 472 @NonNull getRoutes()473 public List<MediaRoute2Info> getRoutes() { 474 synchronized (mLock) { 475 if (mShouldUpdateRoutes) { 476 mShouldUpdateRoutes = false; 477 478 List<MediaRoute2Info> filteredRoutes = new ArrayList<>(); 479 for (MediaRoute2Info route : mRoutes.values()) { 480 if (route.hasAnyFeatures(mDiscoveryPreference.getPreferredFeatures())) { 481 filteredRoutes.add(route); 482 } 483 } 484 mFilteredRoutes = Collections.unmodifiableList(filteredRoutes); 485 } 486 } 487 return mFilteredRoutes; 488 } 489 490 /** 491 * Registers a callback to get the result of {@link #transferTo(MediaRoute2Info)}. 492 * If you register the same callback twice or more, it will be ignored. 493 * 494 * @param executor the executor to execute the callback on 495 * @param callback the callback to register 496 * @see #unregisterTransferCallback 497 */ registerTransferCallback(@onNull @allbackExecutor Executor executor, @NonNull TransferCallback callback)498 public void registerTransferCallback(@NonNull @CallbackExecutor Executor executor, 499 @NonNull TransferCallback callback) { 500 Objects.requireNonNull(executor, "executor must not be null"); 501 Objects.requireNonNull(callback, "callback must not be null"); 502 503 TransferCallbackRecord record = new TransferCallbackRecord(executor, callback); 504 if (!mTransferCallbackRecords.addIfAbsent(record)) { 505 Log.w(TAG, "registerTransferCallback: Ignoring the same callback"); 506 return; 507 } 508 } 509 510 /** 511 * Unregisters the given callback. The callback will no longer receive events. 512 * If the callback has not been added or been removed already, it is ignored. 513 * 514 * @param callback the callback to unregister 515 * @see #registerTransferCallback 516 */ unregisterTransferCallback(@onNull TransferCallback callback)517 public void unregisterTransferCallback(@NonNull TransferCallback callback) { 518 Objects.requireNonNull(callback, "callback must not be null"); 519 520 if (!mTransferCallbackRecords.remove(new TransferCallbackRecord(null, callback))) { 521 Log.w(TAG, "unregisterTransferCallback: Ignoring an unknown callback"); 522 return; 523 } 524 } 525 526 /** 527 * Registers a {@link ControllerCallback}. 528 * If you register the same callback twice or more, it will be ignored. 529 * @see #unregisterControllerCallback(ControllerCallback) 530 */ registerControllerCallback(@onNull @allbackExecutor Executor executor, @NonNull ControllerCallback callback)531 public void registerControllerCallback(@NonNull @CallbackExecutor Executor executor, 532 @NonNull ControllerCallback callback) { 533 Objects.requireNonNull(executor, "executor must not be null"); 534 Objects.requireNonNull(callback, "callback must not be null"); 535 536 ControllerCallbackRecord record = new ControllerCallbackRecord(executor, callback); 537 if (!mControllerCallbackRecords.addIfAbsent(record)) { 538 Log.w(TAG, "registerControllerCallback: Ignoring the same callback"); 539 return; 540 } 541 } 542 543 /** 544 * Unregisters a {@link ControllerCallback}. The callback will no longer receive 545 * events. If the callback has not been added or been removed already, it is ignored. 546 * @see #registerControllerCallback(Executor, ControllerCallback) 547 */ unregisterControllerCallback( @onNull ControllerCallback callback)548 public void unregisterControllerCallback( 549 @NonNull ControllerCallback callback) { 550 Objects.requireNonNull(callback, "callback must not be null"); 551 552 if (!mControllerCallbackRecords.remove(new ControllerCallbackRecord(null, callback))) { 553 Log.w(TAG, "unregisterControllerCallback: Ignoring an unknown callback"); 554 return; 555 } 556 } 557 558 /** 559 * Sets an {@link OnGetControllerHintsListener} to send hints when creating a 560 * {@link RoutingController}. To send the hints, listener should be set <em>BEFORE</em> calling 561 * {@link #transferTo(MediaRoute2Info)}. 562 * 563 * @param listener A listener to send optional app-specific hints when creating a controller. 564 * {@code null} for unset. 565 */ setOnGetControllerHintsListener(@ullable OnGetControllerHintsListener listener)566 public void setOnGetControllerHintsListener(@Nullable OnGetControllerHintsListener listener) { 567 if (isSystemRouter()) { 568 return; 569 } 570 mOnGetControllerHintsListener = listener; 571 } 572 573 /** 574 * Transfers the current media to the given route. 575 * If it's necessary a new {@link RoutingController} is created or it is handled within 576 * the current routing controller. 577 * 578 * @param route the route you want to transfer the current media to. Pass {@code null} to 579 * stop routing of the current media. 580 * 581 * @see TransferCallback#onTransfer 582 * @see TransferCallback#onTransferFailure 583 */ transferTo(@onNull MediaRoute2Info route)584 public void transferTo(@NonNull MediaRoute2Info route) { 585 if (isSystemRouter()) { 586 sManager.selectRoute(mClientPackageName, route); 587 return; 588 } 589 590 Log.v(TAG, "Transferring to route: " + route); 591 592 boolean routeFound; 593 synchronized (mLock) { 594 // TODO: Check thread-safety 595 routeFound = mRoutes.containsKey(route.getId()); 596 } 597 if (!routeFound) { 598 notifyTransferFailure(route); 599 return; 600 } 601 602 RoutingController controller = getCurrentController(); 603 if (controller.getRoutingSessionInfo().getTransferableRoutes().contains(route.getId())) { 604 controller.transferToRoute(route); 605 return; 606 } 607 608 requestCreateController(controller, route, MANAGER_REQUEST_ID_NONE); 609 } 610 611 /** 612 * Stops the current media routing. If the {@link #getSystemController() system controller} 613 * controls the media routing, this method is a no-op. 614 */ stop()615 public void stop() { 616 if (isSystemRouter()) { 617 List<RoutingSessionInfo> sessionInfos = sManager.getRoutingSessions(mClientPackageName); 618 RoutingSessionInfo sessionToRelease = sessionInfos.get(sessionInfos.size() - 1); 619 sManager.releaseSession(sessionToRelease); 620 return; 621 } 622 getCurrentController().release(); 623 } 624 625 /** 626 * Transfers the media of a routing controller to the given route. 627 * <p> 628 * This will be no-op for non-system media routers. 629 * 630 * @param controller a routing controller controlling media routing. 631 * @param route the route you want to transfer the media to. 632 * @hide 633 */ 634 @SystemApi 635 @RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL) transfer(@onNull RoutingController controller, @NonNull MediaRoute2Info route)636 public void transfer(@NonNull RoutingController controller, @NonNull MediaRoute2Info route) { 637 if (isSystemRouter()) { 638 sManager.transfer(controller.getRoutingSessionInfo(), route); 639 return; 640 } 641 } 642 requestCreateController(@onNull RoutingController controller, @NonNull MediaRoute2Info route, long managerRequestId)643 void requestCreateController(@NonNull RoutingController controller, 644 @NonNull MediaRoute2Info route, long managerRequestId) { 645 646 final int requestId = mNextRequestId.getAndIncrement(); 647 648 ControllerCreationRequest request = new ControllerCreationRequest(requestId, 649 managerRequestId, route, controller); 650 mControllerCreationRequests.add(request); 651 652 OnGetControllerHintsListener listener = mOnGetControllerHintsListener; 653 Bundle controllerHints = null; 654 if (listener != null) { 655 controllerHints = listener.onGetControllerHints(route); 656 if (controllerHints != null) { 657 controllerHints = new Bundle(controllerHints); 658 } 659 } 660 661 MediaRouter2Stub stub; 662 synchronized (mLock) { 663 stub = mStub; 664 } 665 if (stub != null) { 666 try { 667 mMediaRouterService.requestCreateSessionWithRouter2( 668 stub, requestId, managerRequestId, 669 controller.getRoutingSessionInfo(), route, controllerHints); 670 } catch (RemoteException ex) { 671 Log.e(TAG, "createControllerForTransfer: " 672 + "Failed to request for creating a controller.", ex); 673 mControllerCreationRequests.remove(request); 674 if (managerRequestId == MANAGER_REQUEST_ID_NONE) { 675 notifyTransferFailure(route); 676 } 677 } 678 } 679 } 680 681 @NonNull getCurrentController()682 private RoutingController getCurrentController() { 683 List<RoutingController> controllers = getControllers(); 684 return controllers.get(controllers.size() - 1); 685 } 686 687 /** 688 * Gets a {@link RoutingController} which can control the routes provided by system. 689 * e.g. Phone speaker, wired headset, Bluetooth, etc. 690 * <p> 691 * Note: The system controller can't be released. Calling {@link RoutingController#release()} 692 * will be ignored. 693 * <p> 694 * This method always returns the same instance. 695 */ 696 @NonNull getSystemController()697 public RoutingController getSystemController() { 698 return mSystemController; 699 } 700 701 /** 702 * Gets a {@link RoutingController} whose ID is equal to the given ID. 703 * Returns {@code null} if there is no matching controller. 704 */ 705 @Nullable getController(@onNull String id)706 public RoutingController getController(@NonNull String id) { 707 Objects.requireNonNull(id, "id must not be null"); 708 for (RoutingController controller : getControllers()) { 709 if (TextUtils.equals(id, controller.getId())) { 710 return controller; 711 } 712 } 713 return null; 714 } 715 716 /** 717 * Gets the list of currently active {@link RoutingController routing controllers} on which 718 * media can be played. 719 * <p> 720 * Note: The list returned here will never be empty. The first element in the list is 721 * always the {@link #getSystemController() system controller}. 722 */ 723 @NonNull getControllers()724 public List<RoutingController> getControllers() { 725 List<RoutingController> result = new ArrayList<>(); 726 727 if (isSystemRouter()) { 728 // Unlike non-system MediaRouter2, controller instances cannot be kept, 729 // since the transfer events initiated from other apps will not come through manager. 730 List<RoutingSessionInfo> sessions = sManager.getRoutingSessions(mClientPackageName); 731 for (RoutingSessionInfo session : sessions) { 732 RoutingController controller; 733 if (session.isSystemSession()) { 734 mSystemController.setRoutingSessionInfo( 735 ensureClientPackageNameForSystemSession(session)); 736 controller = mSystemController; 737 } else { 738 controller = new RoutingController(session); 739 } 740 result.add(controller); 741 } 742 return result; 743 } 744 745 result.add(0, mSystemController); 746 synchronized (mLock) { 747 result.addAll(mNonSystemRoutingControllers.values()); 748 } 749 return result; 750 } 751 752 /** 753 * Requests a volume change for the route asynchronously. 754 * It may have no effect if the route is currently not selected. 755 * <p> 756 * This will be no-op for non-system media routers. 757 * 758 * @param volume The new volume value between 0 and {@link MediaRoute2Info#getVolumeMax}. 759 * @see #getInstance(Context, String) 760 * @hide 761 */ 762 @SystemApi 763 @RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL) setRouteVolume(@onNull MediaRoute2Info route, int volume)764 public void setRouteVolume(@NonNull MediaRoute2Info route, int volume) { 765 Objects.requireNonNull(route, "route must not be null"); 766 767 if (isSystemRouter()) { 768 sManager.setRouteVolume(route, volume); 769 return; 770 } 771 // If this API needs to be public, use IMediaRouterService#setRouteVolumeWithRouter2() 772 } 773 syncRoutesOnHandler(List<MediaRoute2Info> currentRoutes, RoutingSessionInfo currentSystemSessionInfo)774 void syncRoutesOnHandler(List<MediaRoute2Info> currentRoutes, 775 RoutingSessionInfo currentSystemSessionInfo) { 776 if (currentRoutes == null || currentRoutes.isEmpty() || currentSystemSessionInfo == null) { 777 Log.e(TAG, "syncRoutesOnHandler: Received wrong data. currentRoutes=" + currentRoutes 778 + ", currentSystemSessionInfo=" + currentSystemSessionInfo); 779 return; 780 } 781 782 List<MediaRoute2Info> addedRoutes = new ArrayList<>(); 783 List<MediaRoute2Info> removedRoutes = new ArrayList<>(); 784 List<MediaRoute2Info> changedRoutes = new ArrayList<>(); 785 786 synchronized (mLock) { 787 List<String> currentRoutesIds = currentRoutes.stream().map(MediaRoute2Info::getId) 788 .collect(Collectors.toList()); 789 790 for (String routeId : mRoutes.keySet()) { 791 if (!currentRoutesIds.contains(routeId)) { 792 // This route is removed while the callback is unregistered. 793 MediaRoute2Info route = mRoutes.get(routeId); 794 if (route.hasAnyFeatures(mDiscoveryPreference.getPreferredFeatures())) { 795 removedRoutes.add(mRoutes.get(routeId)); 796 } 797 } 798 } 799 800 for (MediaRoute2Info route : currentRoutes) { 801 if (mRoutes.containsKey(route.getId())) { 802 if (!route.equals(mRoutes.get(route.getId()))) { 803 // This route is changed while the callback is unregistered. 804 if (route.hasAnyFeatures( 805 mDiscoveryPreference.getPreferredFeatures())) { 806 changedRoutes.add(route); 807 } 808 } 809 } else { 810 // This route is added while the callback is unregistered. 811 if (route.hasAnyFeatures(mDiscoveryPreference.getPreferredFeatures())) { 812 addedRoutes.add(route); 813 } 814 } 815 } 816 817 mRoutes.clear(); 818 for (MediaRoute2Info route : currentRoutes) { 819 mRoutes.put(route.getId(), route); 820 } 821 822 mShouldUpdateRoutes = true; 823 } 824 825 if (!addedRoutes.isEmpty()) { 826 notifyRoutesAdded(addedRoutes); 827 } 828 if (!removedRoutes.isEmpty()) { 829 notifyRoutesRemoved(removedRoutes); 830 } 831 if (!changedRoutes.isEmpty()) { 832 notifyRoutesChanged(changedRoutes); 833 } 834 835 RoutingSessionInfo oldInfo = mSystemController.getRoutingSessionInfo(); 836 mSystemController.setRoutingSessionInfo(currentSystemSessionInfo); 837 if (!oldInfo.equals(currentSystemSessionInfo)) { 838 notifyControllerUpdated(mSystemController); 839 } 840 } 841 addRoutesOnHandler(List<MediaRoute2Info> routes)842 void addRoutesOnHandler(List<MediaRoute2Info> routes) { 843 List<MediaRoute2Info> addedRoutes = new ArrayList<>(); 844 synchronized (mLock) { 845 for (MediaRoute2Info route : routes) { 846 mRoutes.put(route.getId(), route); 847 if (route.hasAnyFeatures(mDiscoveryPreference.getPreferredFeatures())) { 848 addedRoutes.add(route); 849 } 850 } 851 mShouldUpdateRoutes = true; 852 } 853 if (!addedRoutes.isEmpty()) { 854 notifyRoutesAdded(addedRoutes); 855 } 856 } 857 removeRoutesOnHandler(List<MediaRoute2Info> routes)858 void removeRoutesOnHandler(List<MediaRoute2Info> routes) { 859 List<MediaRoute2Info> removedRoutes = new ArrayList<>(); 860 synchronized (mLock) { 861 for (MediaRoute2Info route : routes) { 862 mRoutes.remove(route.getId()); 863 if (route.hasAnyFeatures(mDiscoveryPreference.getPreferredFeatures())) { 864 removedRoutes.add(route); 865 } 866 } 867 mShouldUpdateRoutes = true; 868 } 869 if (!removedRoutes.isEmpty()) { 870 notifyRoutesRemoved(removedRoutes); 871 } 872 } 873 changeRoutesOnHandler(List<MediaRoute2Info> routes)874 void changeRoutesOnHandler(List<MediaRoute2Info> routes) { 875 List<MediaRoute2Info> changedRoutes = new ArrayList<>(); 876 synchronized (mLock) { 877 for (MediaRoute2Info route : routes) { 878 mRoutes.put(route.getId(), route); 879 if (route.hasAnyFeatures(mDiscoveryPreference.getPreferredFeatures())) { 880 changedRoutes.add(route); 881 } 882 } 883 mShouldUpdateRoutes = true; 884 } 885 if (!changedRoutes.isEmpty()) { 886 notifyRoutesChanged(changedRoutes); 887 } 888 } 889 890 /** 891 * Creates a controller and calls the {@link TransferCallback#onTransfer}. 892 * If the controller creation has failed, then it calls 893 * {@link TransferCallback#onTransferFailure}. 894 * <p> 895 * Pass {@code null} to sessionInfo for the failure case. 896 */ createControllerOnHandler(int requestId, @Nullable RoutingSessionInfo sessionInfo)897 void createControllerOnHandler(int requestId, @Nullable RoutingSessionInfo sessionInfo) { 898 ControllerCreationRequest matchingRequest = null; 899 for (ControllerCreationRequest request : mControllerCreationRequests) { 900 if (request.mRequestId == requestId) { 901 matchingRequest = request; 902 break; 903 } 904 } 905 906 if (matchingRequest == null) { 907 Log.w(TAG, "createControllerOnHandler: Ignoring an unknown request."); 908 return; 909 } 910 911 mControllerCreationRequests.remove(matchingRequest); 912 MediaRoute2Info requestedRoute = matchingRequest.mRoute; 913 914 // TODO: Notify the reason for failure. 915 if (sessionInfo == null) { 916 notifyTransferFailure(requestedRoute); 917 return; 918 } else if (!TextUtils.equals(requestedRoute.getProviderId(), 919 sessionInfo.getProviderId())) { 920 Log.w(TAG, "The session's provider ID does not match the requested route's. " 921 + "(requested route's providerId=" + requestedRoute.getProviderId() 922 + ", actual providerId=" + sessionInfo.getProviderId() 923 + ")"); 924 notifyTransferFailure(requestedRoute); 925 return; 926 } 927 928 RoutingController oldController = matchingRequest.mOldController; 929 // When the old controller is released before transferred, treat it as a failure. 930 // This could also happen when transfer is requested twice or more. 931 if (!oldController.scheduleRelease()) { 932 Log.w(TAG, "createControllerOnHandler: " 933 + "Ignoring controller creation for released old controller. " 934 + "oldController=" + oldController); 935 if (!sessionInfo.isSystemSession()) { 936 new RoutingController(sessionInfo).release(); 937 } 938 notifyTransferFailure(requestedRoute); 939 return; 940 } 941 942 RoutingController newController; 943 if (sessionInfo.isSystemSession()) { 944 newController = getSystemController(); 945 newController.setRoutingSessionInfo(sessionInfo); 946 } else { 947 newController = new RoutingController(sessionInfo); 948 synchronized (mLock) { 949 mNonSystemRoutingControllers.put(newController.getId(), newController); 950 } 951 } 952 953 notifyTransfer(oldController, newController); 954 } 955 updateControllerOnHandler(RoutingSessionInfo sessionInfo)956 void updateControllerOnHandler(RoutingSessionInfo sessionInfo) { 957 if (sessionInfo == null) { 958 Log.w(TAG, "updateControllerOnHandler: Ignoring null sessionInfo."); 959 return; 960 } 961 962 if (sessionInfo.isSystemSession()) { 963 // The session info is sent from SystemMediaRoute2Provider. 964 RoutingController systemController = getSystemController(); 965 systemController.setRoutingSessionInfo(sessionInfo); 966 notifyControllerUpdated(systemController); 967 return; 968 } 969 970 RoutingController matchingController; 971 synchronized (mLock) { 972 matchingController = mNonSystemRoutingControllers.get(sessionInfo.getId()); 973 } 974 975 if (matchingController == null) { 976 Log.w(TAG, "updateControllerOnHandler: Matching controller not found. uniqueSessionId=" 977 + sessionInfo.getId()); 978 return; 979 } 980 981 RoutingSessionInfo oldInfo = matchingController.getRoutingSessionInfo(); 982 if (!TextUtils.equals(oldInfo.getProviderId(), sessionInfo.getProviderId())) { 983 Log.w(TAG, "updateControllerOnHandler: Provider IDs are not matched. old=" 984 + oldInfo.getProviderId() + ", new=" + sessionInfo.getProviderId()); 985 return; 986 } 987 988 matchingController.setRoutingSessionInfo(sessionInfo); 989 notifyControllerUpdated(matchingController); 990 } 991 releaseControllerOnHandler(RoutingSessionInfo sessionInfo)992 void releaseControllerOnHandler(RoutingSessionInfo sessionInfo) { 993 if (sessionInfo == null) { 994 Log.w(TAG, "releaseControllerOnHandler: Ignoring null sessionInfo."); 995 return; 996 } 997 998 RoutingController matchingController; 999 synchronized (mLock) { 1000 matchingController = mNonSystemRoutingControllers.get(sessionInfo.getId()); 1001 } 1002 1003 if (matchingController == null) { 1004 if (DEBUG) { 1005 Log.d(TAG, "releaseControllerOnHandler: Matching controller not found. " 1006 + "uniqueSessionId=" + sessionInfo.getId()); 1007 } 1008 return; 1009 } 1010 1011 RoutingSessionInfo oldInfo = matchingController.getRoutingSessionInfo(); 1012 if (!TextUtils.equals(oldInfo.getProviderId(), sessionInfo.getProviderId())) { 1013 Log.w(TAG, "releaseControllerOnHandler: Provider IDs are not matched. old=" 1014 + oldInfo.getProviderId() + ", new=" + sessionInfo.getProviderId()); 1015 return; 1016 } 1017 1018 matchingController.releaseInternal(/* shouldReleaseSession= */ false); 1019 } 1020 onRequestCreateControllerByManagerOnHandler(RoutingSessionInfo oldSession, MediaRoute2Info route, long managerRequestId)1021 void onRequestCreateControllerByManagerOnHandler(RoutingSessionInfo oldSession, 1022 MediaRoute2Info route, long managerRequestId) { 1023 RoutingController controller; 1024 if (oldSession.isSystemSession()) { 1025 controller = getSystemController(); 1026 } else { 1027 synchronized (mLock) { 1028 controller = mNonSystemRoutingControllers.get(oldSession.getId()); 1029 } 1030 } 1031 if (controller == null) { 1032 return; 1033 } 1034 requestCreateController(controller, route, managerRequestId); 1035 } 1036 1037 /** 1038 * Returns whether this router is created with {@link #getInstance(Context, String)}. 1039 * This kind of router can control the target app's media routing. 1040 */ isSystemRouter()1041 private boolean isSystemRouter() { 1042 return mClientPackageName != null; 1043 } 1044 1045 /** 1046 * Returns a {@link RoutingSessionInfo} which has the client package name. 1047 * The client package name is set only when the given sessionInfo doesn't have it. 1048 * Should only used for system media routers. 1049 */ ensureClientPackageNameForSystemSession( @onNull RoutingSessionInfo sessionInfo)1050 private RoutingSessionInfo ensureClientPackageNameForSystemSession( 1051 @NonNull RoutingSessionInfo sessionInfo) { 1052 if (!sessionInfo.isSystemSession() 1053 || !TextUtils.isEmpty(sessionInfo.getClientPackageName())) { 1054 return sessionInfo; 1055 } 1056 1057 return new RoutingSessionInfo.Builder(sessionInfo) 1058 .setClientPackageName(mClientPackageName) 1059 .build(); 1060 } 1061 filterRoutes(List<MediaRoute2Info> routes, RouteDiscoveryPreference discoveryRequest)1062 private List<MediaRoute2Info> filterRoutes(List<MediaRoute2Info> routes, 1063 RouteDiscoveryPreference discoveryRequest) { 1064 return routes.stream() 1065 .filter(route -> route.hasAnyFeatures(discoveryRequest.getPreferredFeatures())) 1066 .collect(Collectors.toList()); 1067 } 1068 updateAllRoutesFromManager()1069 private void updateAllRoutesFromManager() { 1070 if (!isSystemRouter()) { 1071 return; 1072 } 1073 synchronized (mLock) { 1074 mRoutes.clear(); 1075 for (MediaRoute2Info route : sManager.getAllRoutes()) { 1076 mRoutes.put(route.getId(), route); 1077 } 1078 mShouldUpdateRoutes = true; 1079 } 1080 } 1081 notifyRoutesAdded(List<MediaRoute2Info> routes)1082 private void notifyRoutesAdded(List<MediaRoute2Info> routes) { 1083 for (RouteCallbackRecord record: mRouteCallbackRecords) { 1084 List<MediaRoute2Info> filteredRoutes = filterRoutes(routes, record.mPreference); 1085 if (!filteredRoutes.isEmpty()) { 1086 record.mExecutor.execute( 1087 () -> record.mRouteCallback.onRoutesAdded(filteredRoutes)); 1088 } 1089 } 1090 } 1091 notifyRoutesRemoved(List<MediaRoute2Info> routes)1092 private void notifyRoutesRemoved(List<MediaRoute2Info> routes) { 1093 for (RouteCallbackRecord record: mRouteCallbackRecords) { 1094 List<MediaRoute2Info> filteredRoutes = filterRoutes(routes, record.mPreference); 1095 if (!filteredRoutes.isEmpty()) { 1096 record.mExecutor.execute( 1097 () -> record.mRouteCallback.onRoutesRemoved(filteredRoutes)); 1098 } 1099 } 1100 } 1101 notifyRoutesChanged(List<MediaRoute2Info> routes)1102 private void notifyRoutesChanged(List<MediaRoute2Info> routes) { 1103 for (RouteCallbackRecord record: mRouteCallbackRecords) { 1104 List<MediaRoute2Info> filteredRoutes = filterRoutes(routes, record.mPreference); 1105 if (!filteredRoutes.isEmpty()) { 1106 record.mExecutor.execute( 1107 () -> record.mRouteCallback.onRoutesChanged(filteredRoutes)); 1108 } 1109 } 1110 } 1111 notifyPreferredFeaturesChanged(List<String> features)1112 private void notifyPreferredFeaturesChanged(List<String> features) { 1113 for (RouteCallbackRecord record: mRouteCallbackRecords) { 1114 record.mExecutor.execute( 1115 () -> record.mRouteCallback.onPreferredFeaturesChanged(features)); 1116 } 1117 } 1118 notifyTransfer(RoutingController oldController, RoutingController newController)1119 private void notifyTransfer(RoutingController oldController, RoutingController newController) { 1120 for (TransferCallbackRecord record: mTransferCallbackRecords) { 1121 record.mExecutor.execute( 1122 () -> record.mTransferCallback.onTransfer(oldController, newController)); 1123 } 1124 } 1125 notifyTransferFailure(MediaRoute2Info route)1126 private void notifyTransferFailure(MediaRoute2Info route) { 1127 for (TransferCallbackRecord record: mTransferCallbackRecords) { 1128 record.mExecutor.execute( 1129 () -> record.mTransferCallback.onTransferFailure(route)); 1130 } 1131 } 1132 notifyStop(RoutingController controller)1133 private void notifyStop(RoutingController controller) { 1134 for (TransferCallbackRecord record: mTransferCallbackRecords) { 1135 record.mExecutor.execute( 1136 () -> record.mTransferCallback.onStop(controller)); 1137 } 1138 } 1139 notifyControllerUpdated(RoutingController controller)1140 private void notifyControllerUpdated(RoutingController controller) { 1141 for (ControllerCallbackRecord record: mControllerCallbackRecords) { 1142 record.mExecutor.execute(() -> record.mCallback.onControllerUpdated(controller)); 1143 } 1144 } 1145 1146 /** 1147 * Callback for receiving events about media route discovery. 1148 */ 1149 public abstract static class RouteCallback { 1150 /** 1151 * Called when routes are added. Whenever you registers a callback, this will 1152 * be invoked with known routes. 1153 * 1154 * @param routes the list of routes that have been added. It's never empty. 1155 */ onRoutesAdded(@onNull List<MediaRoute2Info> routes)1156 public void onRoutesAdded(@NonNull List<MediaRoute2Info> routes) {} 1157 1158 /** 1159 * Called when routes are removed. 1160 * 1161 * @param routes the list of routes that have been removed. It's never empty. 1162 */ onRoutesRemoved(@onNull List<MediaRoute2Info> routes)1163 public void onRoutesRemoved(@NonNull List<MediaRoute2Info> routes) {} 1164 1165 /** 1166 * Called when routes are changed. For example, it is called when the route's name 1167 * or volume have been changed. 1168 * 1169 * @param routes the list of routes that have been changed. It's never empty. 1170 */ onRoutesChanged(@onNull List<MediaRoute2Info> routes)1171 public void onRoutesChanged(@NonNull List<MediaRoute2Info> routes) {} 1172 1173 /** 1174 * Called when the client app's preferred features are changed. 1175 * When this is called, it is recommended to {@link #getRoutes()} to get the routes 1176 * that are currently available to the app. 1177 * 1178 * @param preferredFeatures the new preferred features set by the application 1179 * @hide 1180 */ 1181 @SystemApi onPreferredFeaturesChanged(@onNull List<String> preferredFeatures)1182 public void onPreferredFeaturesChanged(@NonNull List<String> preferredFeatures) {} 1183 } 1184 1185 /** 1186 * Callback for receiving events on media transfer. 1187 */ 1188 public abstract static class TransferCallback { 1189 /** 1190 * Called when a media is transferred between two different routing controllers. 1191 * This can happen by calling {@link #transferTo(MediaRoute2Info)}. 1192 * <p> Override this to start playback with {@code newController}. You may want to get 1193 * the status of the media that is being played with {@code oldController} and resume it 1194 * continuously with {@code newController}. 1195 * After this is called, any callbacks with {@code oldController} will not be invoked 1196 * unless {@code oldController} is the {@link #getSystemController() system controller}. 1197 * You need to {@link RoutingController#release() release} {@code oldController} before 1198 * playing the media with {@code newController}. 1199 * 1200 * @param oldController the previous controller that controlled routing 1201 * @param newController the new controller to control routing 1202 * @see #transferTo(MediaRoute2Info) 1203 */ onTransfer(@onNull RoutingController oldController, @NonNull RoutingController newController)1204 public void onTransfer(@NonNull RoutingController oldController, 1205 @NonNull RoutingController newController) {} 1206 1207 /** 1208 * Called when {@link #transferTo(MediaRoute2Info)} failed. 1209 * 1210 * @param requestedRoute the route info which was used for the transfer 1211 */ onTransferFailure(@onNull MediaRoute2Info requestedRoute)1212 public void onTransferFailure(@NonNull MediaRoute2Info requestedRoute) {} 1213 1214 /** 1215 * Called when a media routing stops. It can be stopped by a user or a provider. 1216 * App should not continue playing media locally when this method is called. 1217 * The {@code controller} is released before this method is called. 1218 * 1219 * @param controller the controller that controlled the stopped media routing 1220 */ onStop(@onNull RoutingController controller)1221 public void onStop(@NonNull RoutingController controller) { } 1222 } 1223 1224 /** 1225 * A listener interface to send optional app-specific hints when creating a 1226 * {@link RoutingController}. 1227 */ 1228 public interface OnGetControllerHintsListener { 1229 /** 1230 * Called when the {@link MediaRouter2} or the system is about to request 1231 * a media route provider service to create a controller with the given route. 1232 * The {@link Bundle} returned here will be sent to media route provider service as a hint. 1233 * <p> 1234 * Since controller creation can be requested by the {@link MediaRouter2} and the system, 1235 * set the listener as soon as possible after acquiring {@link MediaRouter2} instance. 1236 * The method will be called on the same thread that calls 1237 * {@link #transferTo(MediaRoute2Info)} or the main thread if it is requested by the system. 1238 * 1239 * @param route the route to create a controller with 1240 * @return An optional bundle of app-specific arguments to send to the provider, 1241 * or {@code null} if none. The contents of this bundle may affect the result of 1242 * controller creation. 1243 * @see MediaRoute2ProviderService#onCreateSession(long, String, String, Bundle) 1244 */ 1245 @Nullable onGetControllerHints(@onNull MediaRoute2Info route)1246 Bundle onGetControllerHints(@NonNull MediaRoute2Info route); 1247 } 1248 1249 /** 1250 * Callback for receiving {@link RoutingController} updates. 1251 */ 1252 public abstract static class ControllerCallback { 1253 /** 1254 * Called when a controller is updated. (e.g., when the selected routes of the 1255 * controller is changed or when the volume of the controller is changed.) 1256 * 1257 * @param controller the updated controller. It may be the 1258 * {@link #getSystemController() system controller}. 1259 * @see #getSystemController() 1260 */ onControllerUpdated(@onNull RoutingController controller)1261 public void onControllerUpdated(@NonNull RoutingController controller) { } 1262 } 1263 1264 /** 1265 * A class to control media routing session in media route provider. 1266 * For example, selecting/deselecting/transferring to routes of a session can be done through 1267 * this. Instances are created when 1268 * {@link TransferCallback#onTransfer(RoutingController, RoutingController)} is called, 1269 * which is invoked after {@link #transferTo(MediaRoute2Info)} is called. 1270 */ 1271 public class RoutingController { 1272 private final Object mControllerLock = new Object(); 1273 1274 private static final int CONTROLLER_STATE_UNKNOWN = 0; 1275 private static final int CONTROLLER_STATE_ACTIVE = 1; 1276 private static final int CONTROLLER_STATE_RELEASING = 2; 1277 private static final int CONTROLLER_STATE_RELEASED = 3; 1278 1279 @GuardedBy("mControllerLock") 1280 private RoutingSessionInfo mSessionInfo; 1281 1282 @GuardedBy("mControllerLock") 1283 private int mState; 1284 RoutingController(@onNull RoutingSessionInfo sessionInfo)1285 RoutingController(@NonNull RoutingSessionInfo sessionInfo) { 1286 mSessionInfo = sessionInfo; 1287 mState = CONTROLLER_STATE_ACTIVE; 1288 } 1289 RoutingController(@onNull RoutingSessionInfo sessionInfo, int state)1290 RoutingController(@NonNull RoutingSessionInfo sessionInfo, int state) { 1291 mSessionInfo = sessionInfo; 1292 mState = state; 1293 } 1294 1295 /** 1296 * @return the ID of the controller. It is globally unique. 1297 */ 1298 @NonNull getId()1299 public String getId() { 1300 synchronized (mControllerLock) { 1301 return mSessionInfo.getId(); 1302 } 1303 } 1304 1305 /** 1306 * Gets the original session ID set by 1307 * {@link RoutingSessionInfo.Builder#Builder(String, String)}. 1308 * 1309 * @hide 1310 */ 1311 @NonNull 1312 @TestApi getOriginalId()1313 public String getOriginalId() { 1314 synchronized (mControllerLock) { 1315 return mSessionInfo.getOriginalId(); 1316 } 1317 } 1318 1319 /** 1320 * Gets the control hints used to control routing session if available. 1321 * It is set by the media route provider. 1322 */ 1323 @Nullable getControlHints()1324 public Bundle getControlHints() { 1325 synchronized (mControllerLock) { 1326 return mSessionInfo.getControlHints(); 1327 } 1328 } 1329 1330 /** 1331 * @return the unmodifiable list of currently selected routes 1332 */ 1333 @NonNull getSelectedRoutes()1334 public List<MediaRoute2Info> getSelectedRoutes() { 1335 List<String> selectedRouteIds; 1336 synchronized (mControllerLock) { 1337 selectedRouteIds = mSessionInfo.getSelectedRoutes(); 1338 } 1339 return getRoutesWithIds(selectedRouteIds); 1340 } 1341 1342 /** 1343 * @return the unmodifiable list of selectable routes for the session. 1344 */ 1345 @NonNull getSelectableRoutes()1346 public List<MediaRoute2Info> getSelectableRoutes() { 1347 List<String> selectableRouteIds; 1348 synchronized (mControllerLock) { 1349 selectableRouteIds = mSessionInfo.getSelectableRoutes(); 1350 } 1351 return getRoutesWithIds(selectableRouteIds); 1352 } 1353 1354 /** 1355 * @return the unmodifiable list of deselectable routes for the session. 1356 */ 1357 @NonNull getDeselectableRoutes()1358 public List<MediaRoute2Info> getDeselectableRoutes() { 1359 List<String> deselectableRouteIds; 1360 synchronized (mControllerLock) { 1361 deselectableRouteIds = mSessionInfo.getDeselectableRoutes(); 1362 } 1363 return getRoutesWithIds(deselectableRouteIds); 1364 } 1365 1366 /** 1367 * Gets the information about how volume is handled on the session. 1368 * <p>Please note that you may not control the volume of the session even when 1369 * you can control the volume of each selected route in the session. 1370 * 1371 * @return {@link MediaRoute2Info#PLAYBACK_VOLUME_FIXED} or 1372 * {@link MediaRoute2Info#PLAYBACK_VOLUME_VARIABLE} 1373 */ 1374 @MediaRoute2Info.PlaybackVolume getVolumeHandling()1375 public int getVolumeHandling() { 1376 synchronized (mControllerLock) { 1377 return mSessionInfo.getVolumeHandling(); 1378 } 1379 } 1380 1381 /** 1382 * Gets the maximum volume of the session. 1383 */ getVolumeMax()1384 public int getVolumeMax() { 1385 synchronized (mControllerLock) { 1386 return mSessionInfo.getVolumeMax(); 1387 } 1388 } 1389 1390 /** 1391 * Gets the current volume of the session. 1392 * <p> 1393 * When it's available, it represents the volume of routing session, which is a group 1394 * of selected routes. Use {@link MediaRoute2Info#getVolume()} 1395 * to get the volume of a route, 1396 * </p> 1397 * @see MediaRoute2Info#getVolume() 1398 */ getVolume()1399 public int getVolume() { 1400 synchronized (mControllerLock) { 1401 return mSessionInfo.getVolume(); 1402 } 1403 } 1404 1405 /** 1406 * Returns true if this controller is released, false otherwise. 1407 * If it is released, then all other getters from this instance may return invalid values. 1408 * Also, any operations to this instance will be ignored once released. 1409 * 1410 * @see #release 1411 */ isReleased()1412 public boolean isReleased() { 1413 synchronized (mControllerLock) { 1414 return mState == CONTROLLER_STATE_RELEASED; 1415 } 1416 } 1417 1418 /** 1419 * Selects a route for the remote session. After a route is selected, the media is expected 1420 * to be played to the all the selected routes. This is different from {@link 1421 * MediaRouter2#transferTo(MediaRoute2Info)} transferring to a route}, 1422 * where the media is expected to 'move' from one route to another. 1423 * <p> 1424 * The given route must satisfy all of the following conditions: 1425 * <ul> 1426 * <li>It should not be included in {@link #getSelectedRoutes()}</li> 1427 * <li>It should be included in {@link #getSelectableRoutes()}</li> 1428 * </ul> 1429 * If the route doesn't meet any of above conditions, it will be ignored. 1430 * 1431 * @see #deselectRoute(MediaRoute2Info) 1432 * @see #getSelectedRoutes() 1433 * @see #getSelectableRoutes() 1434 * @see ControllerCallback#onControllerUpdated 1435 */ selectRoute(@onNull MediaRoute2Info route)1436 public void selectRoute(@NonNull MediaRoute2Info route) { 1437 Objects.requireNonNull(route, "route must not be null"); 1438 if (isReleased()) { 1439 Log.w(TAG, "selectRoute: Called on released controller. Ignoring."); 1440 return; 1441 } 1442 1443 List<MediaRoute2Info> selectedRoutes = getSelectedRoutes(); 1444 if (checkRouteListContainsRouteId(selectedRoutes, route.getId())) { 1445 Log.w(TAG, "Ignoring selecting a route that is already selected. route=" + route); 1446 return; 1447 } 1448 1449 List<MediaRoute2Info> selectableRoutes = getSelectableRoutes(); 1450 if (!checkRouteListContainsRouteId(selectableRoutes, route.getId())) { 1451 Log.w(TAG, "Ignoring selecting a non-selectable route=" + route); 1452 return; 1453 } 1454 1455 if (isSystemRouter()) { 1456 sManager.selectRoute(getRoutingSessionInfo(), route); 1457 return; 1458 } 1459 1460 MediaRouter2Stub stub; 1461 synchronized (mLock) { 1462 stub = mStub; 1463 } 1464 if (stub != null) { 1465 try { 1466 mMediaRouterService.selectRouteWithRouter2(stub, getId(), route); 1467 } catch (RemoteException ex) { 1468 Log.e(TAG, "Unable to select route for session.", ex); 1469 } 1470 } 1471 } 1472 1473 /** 1474 * Deselects a route from the remote session. After a route is deselected, the media is 1475 * expected to be stopped on the deselected route. 1476 * <p> 1477 * The given route must satisfy all of the following conditions: 1478 * <ul> 1479 * <li>It should be included in {@link #getSelectedRoutes()}</li> 1480 * <li>It should be included in {@link #getDeselectableRoutes()}</li> 1481 * </ul> 1482 * If the route doesn't meet any of above conditions, it will be ignored. 1483 * 1484 * @see #getSelectedRoutes() 1485 * @see #getDeselectableRoutes() 1486 * @see ControllerCallback#onControllerUpdated 1487 */ deselectRoute(@onNull MediaRoute2Info route)1488 public void deselectRoute(@NonNull MediaRoute2Info route) { 1489 Objects.requireNonNull(route, "route must not be null"); 1490 if (isReleased()) { 1491 Log.w(TAG, "deselectRoute: called on released controller. Ignoring."); 1492 return; 1493 } 1494 1495 List<MediaRoute2Info> selectedRoutes = getSelectedRoutes(); 1496 if (!checkRouteListContainsRouteId(selectedRoutes, route.getId())) { 1497 Log.w(TAG, "Ignoring deselecting a route that is not selected. route=" + route); 1498 return; 1499 } 1500 1501 List<MediaRoute2Info> deselectableRoutes = getDeselectableRoutes(); 1502 if (!checkRouteListContainsRouteId(deselectableRoutes, route.getId())) { 1503 Log.w(TAG, "Ignoring deselecting a non-deselectable route=" + route); 1504 return; 1505 } 1506 1507 if (isSystemRouter()) { 1508 sManager.deselectRoute(getRoutingSessionInfo(), route); 1509 return; 1510 } 1511 1512 MediaRouter2Stub stub; 1513 synchronized (mLock) { 1514 stub = mStub; 1515 } 1516 if (stub != null) { 1517 try { 1518 mMediaRouterService.deselectRouteWithRouter2(stub, getId(), route); 1519 } catch (RemoteException ex) { 1520 Log.e(TAG, "Unable to deselect route from session.", ex); 1521 } 1522 } 1523 } 1524 1525 /** 1526 * Transfers to a given route for the remote session. The given route must be included 1527 * in {@link RoutingSessionInfo#getTransferableRoutes()}. 1528 * 1529 * @see RoutingSessionInfo#getSelectedRoutes() 1530 * @see RoutingSessionInfo#getTransferableRoutes() 1531 * @see ControllerCallback#onControllerUpdated 1532 */ transferToRoute(@onNull MediaRoute2Info route)1533 void transferToRoute(@NonNull MediaRoute2Info route) { 1534 Objects.requireNonNull(route, "route must not be null"); 1535 synchronized (mControllerLock) { 1536 if (isReleased()) { 1537 Log.w(TAG, "transferToRoute: Called on released controller. Ignoring."); 1538 return; 1539 } 1540 1541 if (!mSessionInfo.getTransferableRoutes().contains(route.getId())) { 1542 Log.w(TAG, "Ignoring transferring to a non-transferable route=" + route); 1543 return; 1544 } 1545 } 1546 1547 MediaRouter2Stub stub; 1548 synchronized (mLock) { 1549 stub = mStub; 1550 } 1551 if (stub != null) { 1552 try { 1553 mMediaRouterService.transferToRouteWithRouter2(stub, getId(), route); 1554 } catch (RemoteException ex) { 1555 Log.e(TAG, "Unable to transfer to route for session.", ex); 1556 } 1557 } 1558 } 1559 1560 /** 1561 * Requests a volume change for the remote session asynchronously. 1562 * 1563 * @param volume The new volume value between 0 and {@link RoutingController#getVolumeMax} 1564 * (inclusive). 1565 * @see #getVolume() 1566 */ setVolume(int volume)1567 public void setVolume(int volume) { 1568 if (getVolumeHandling() == MediaRoute2Info.PLAYBACK_VOLUME_FIXED) { 1569 Log.w(TAG, "setVolume: The routing session has fixed volume. Ignoring."); 1570 return; 1571 } 1572 if (volume < 0 || volume > getVolumeMax()) { 1573 Log.w(TAG, "setVolume: The target volume is out of range. Ignoring"); 1574 return; 1575 } 1576 1577 if (isReleased()) { 1578 Log.w(TAG, "setVolume: Called on released controller. Ignoring."); 1579 return; 1580 } 1581 1582 if (isSystemRouter()) { 1583 sManager.setSessionVolume(getRoutingSessionInfo(), volume); 1584 return; 1585 } 1586 1587 MediaRouter2Stub stub; 1588 synchronized (mLock) { 1589 stub = mStub; 1590 } 1591 if (stub != null) { 1592 try { 1593 mMediaRouterService.setSessionVolumeWithRouter2(stub, getId(), volume); 1594 } catch (RemoteException ex) { 1595 Log.e(TAG, "setVolume: Failed to deliver request.", ex); 1596 } 1597 } 1598 } 1599 1600 /** 1601 * Releases this controller and the corresponding session. 1602 * Any operations on this controller after calling this method will be ignored. 1603 * The devices that are playing media will stop playing it. 1604 */ release()1605 public void release() { 1606 releaseInternal(/* shouldReleaseSession= */ true); 1607 } 1608 1609 /** 1610 * Schedules release of the controller. 1611 * @return {@code true} if it's successfully scheduled, {@code false} if it's already 1612 * scheduled to be released or released. 1613 */ scheduleRelease()1614 boolean scheduleRelease() { 1615 synchronized (mControllerLock) { 1616 if (mState != CONTROLLER_STATE_ACTIVE) { 1617 return false; 1618 } 1619 mState = CONTROLLER_STATE_RELEASING; 1620 } 1621 1622 synchronized (mLock) { 1623 // It could happen if the controller is released by the another thread 1624 // in between two locks 1625 if (!mNonSystemRoutingControllers.remove(getId(), this)) { 1626 // In that case, onStop isn't called so we return true to call onTransfer. 1627 // It's also consistent with that the another thread acquires the lock later. 1628 return true; 1629 } 1630 } 1631 1632 mHandler.postDelayed(this::release, TRANSFER_TIMEOUT_MS); 1633 1634 return true; 1635 } 1636 releaseInternal(boolean shouldReleaseSession)1637 void releaseInternal(boolean shouldReleaseSession) { 1638 boolean shouldNotifyStop; 1639 1640 synchronized (mControllerLock) { 1641 if (mState == CONTROLLER_STATE_RELEASED) { 1642 if (DEBUG) { 1643 Log.d(TAG, "releaseInternal: Called on released controller. Ignoring."); 1644 } 1645 return; 1646 } 1647 shouldNotifyStop = (mState == CONTROLLER_STATE_ACTIVE); 1648 mState = CONTROLLER_STATE_RELEASED; 1649 } 1650 1651 if (isSystemRouter()) { 1652 sManager.releaseSession(getRoutingSessionInfo()); 1653 return; 1654 } 1655 1656 synchronized (mLock) { 1657 mNonSystemRoutingControllers.remove(getId(), this); 1658 1659 if (shouldReleaseSession && mStub != null) { 1660 try { 1661 mMediaRouterService.releaseSessionWithRouter2(mStub, getId()); 1662 } catch (RemoteException ex) { 1663 Log.e(TAG, "Unable to release session", ex); 1664 } 1665 } 1666 1667 if (shouldNotifyStop) { 1668 mHandler.sendMessage(obtainMessage(MediaRouter2::notifyStop, MediaRouter2.this, 1669 RoutingController.this)); 1670 } 1671 1672 if (mRouteCallbackRecords.isEmpty() && mNonSystemRoutingControllers.isEmpty() 1673 && mStub != null) { 1674 try { 1675 mMediaRouterService.unregisterRouter2(mStub); 1676 } catch (RemoteException ex) { 1677 Log.e(TAG, "releaseInternal: Unable to unregister media router.", ex); 1678 } 1679 mStub = null; 1680 } 1681 } 1682 } 1683 1684 @Override toString()1685 public String toString() { 1686 // To prevent logging spam, we only print the ID of each route. 1687 List<String> selectedRoutes = getSelectedRoutes().stream() 1688 .map(MediaRoute2Info::getId).collect(Collectors.toList()); 1689 List<String> selectableRoutes = getSelectableRoutes().stream() 1690 .map(MediaRoute2Info::getId).collect(Collectors.toList()); 1691 List<String> deselectableRoutes = getDeselectableRoutes().stream() 1692 .map(MediaRoute2Info::getId).collect(Collectors.toList()); 1693 1694 StringBuilder result = new StringBuilder() 1695 .append("RoutingController{ ") 1696 .append("id=").append(getId()) 1697 .append(", selectedRoutes={") 1698 .append(selectedRoutes) 1699 .append("}") 1700 .append(", selectableRoutes={") 1701 .append(selectableRoutes) 1702 .append("}") 1703 .append(", deselectableRoutes={") 1704 .append(deselectableRoutes) 1705 .append("}") 1706 .append(" }"); 1707 return result.toString(); 1708 } 1709 1710 @NonNull getRoutingSessionInfo()1711 RoutingSessionInfo getRoutingSessionInfo() { 1712 synchronized (mControllerLock) { 1713 return mSessionInfo; 1714 } 1715 } 1716 setRoutingSessionInfo(@onNull RoutingSessionInfo info)1717 void setRoutingSessionInfo(@NonNull RoutingSessionInfo info) { 1718 synchronized (mControllerLock) { 1719 mSessionInfo = info; 1720 } 1721 } 1722 getRoutesWithIds(List<String> routeIds)1723 private List<MediaRoute2Info> getRoutesWithIds(List<String> routeIds) { 1724 if (isSystemRouter()) { 1725 return getRoutes().stream() 1726 .filter(r -> routeIds.contains(r.getId())) 1727 .collect(Collectors.toList()); 1728 } 1729 1730 synchronized (mLock) { 1731 return routeIds.stream().map(mRoutes::get) 1732 .filter(Objects::nonNull) 1733 .collect(Collectors.toList()); 1734 } 1735 } 1736 } 1737 1738 class SystemRoutingController extends RoutingController { SystemRoutingController(@onNull RoutingSessionInfo sessionInfo)1739 SystemRoutingController(@NonNull RoutingSessionInfo sessionInfo) { 1740 super(sessionInfo); 1741 } 1742 1743 @Override isReleased()1744 public boolean isReleased() { 1745 // SystemRoutingController will never be released 1746 return false; 1747 } 1748 1749 @Override scheduleRelease()1750 boolean scheduleRelease() { 1751 // SystemRoutingController can be always transferred 1752 return true; 1753 } 1754 1755 @Override releaseInternal(boolean shouldReleaseSession)1756 void releaseInternal(boolean shouldReleaseSession) { 1757 // Do nothing. SystemRoutingController will never be released 1758 } 1759 } 1760 1761 static final class RouteCallbackRecord { 1762 public final Executor mExecutor; 1763 public final RouteCallback mRouteCallback; 1764 public final RouteDiscoveryPreference mPreference; 1765 RouteCallbackRecord(@ullable Executor executor, @NonNull RouteCallback routeCallback, @Nullable RouteDiscoveryPreference preference)1766 RouteCallbackRecord(@Nullable Executor executor, @NonNull RouteCallback routeCallback, 1767 @Nullable RouteDiscoveryPreference preference) { 1768 mRouteCallback = routeCallback; 1769 mExecutor = executor; 1770 mPreference = preference; 1771 } 1772 1773 @Override equals(Object obj)1774 public boolean equals(Object obj) { 1775 if (this == obj) { 1776 return true; 1777 } 1778 if (!(obj instanceof RouteCallbackRecord)) { 1779 return false; 1780 } 1781 return mRouteCallback == ((RouteCallbackRecord) obj).mRouteCallback; 1782 } 1783 1784 @Override hashCode()1785 public int hashCode() { 1786 return mRouteCallback.hashCode(); 1787 } 1788 } 1789 1790 static final class TransferCallbackRecord { 1791 public final Executor mExecutor; 1792 public final TransferCallback mTransferCallback; 1793 TransferCallbackRecord(@onNull Executor executor, @NonNull TransferCallback transferCallback)1794 TransferCallbackRecord(@NonNull Executor executor, 1795 @NonNull TransferCallback transferCallback) { 1796 mTransferCallback = transferCallback; 1797 mExecutor = executor; 1798 } 1799 1800 @Override equals(Object obj)1801 public boolean equals(Object obj) { 1802 if (this == obj) { 1803 return true; 1804 } 1805 if (!(obj instanceof TransferCallbackRecord)) { 1806 return false; 1807 } 1808 return mTransferCallback == ((TransferCallbackRecord) obj).mTransferCallback; 1809 } 1810 1811 @Override hashCode()1812 public int hashCode() { 1813 return mTransferCallback.hashCode(); 1814 } 1815 } 1816 1817 static final class ControllerCallbackRecord { 1818 public final Executor mExecutor; 1819 public final ControllerCallback mCallback; 1820 ControllerCallbackRecord(@ullable Executor executor, @NonNull ControllerCallback callback)1821 ControllerCallbackRecord(@Nullable Executor executor, 1822 @NonNull ControllerCallback callback) { 1823 mCallback = callback; 1824 mExecutor = executor; 1825 } 1826 1827 @Override equals(Object obj)1828 public boolean equals(Object obj) { 1829 if (this == obj) { 1830 return true; 1831 } 1832 if (!(obj instanceof ControllerCallbackRecord)) { 1833 return false; 1834 } 1835 return mCallback == ((ControllerCallbackRecord) obj).mCallback; 1836 } 1837 1838 @Override hashCode()1839 public int hashCode() { 1840 return mCallback.hashCode(); 1841 } 1842 } 1843 1844 static final class ControllerCreationRequest { 1845 public final int mRequestId; 1846 public final long mManagerRequestId; 1847 public final MediaRoute2Info mRoute; 1848 public final RoutingController mOldController; 1849 ControllerCreationRequest(int requestId, long managerRequestId, @NonNull MediaRoute2Info route, @NonNull RoutingController oldController)1850 ControllerCreationRequest(int requestId, long managerRequestId, 1851 @NonNull MediaRoute2Info route, @NonNull RoutingController oldController) { 1852 mRequestId = requestId; 1853 mManagerRequestId = managerRequestId; 1854 mRoute = Objects.requireNonNull(route, "route must not be null"); 1855 mOldController = Objects.requireNonNull(oldController, 1856 "oldController must not be null"); 1857 } 1858 } 1859 1860 class MediaRouter2Stub extends IMediaRouter2.Stub { 1861 @Override notifyRouterRegistered(List<MediaRoute2Info> currentRoutes, RoutingSessionInfo currentSystemSessionInfo)1862 public void notifyRouterRegistered(List<MediaRoute2Info> currentRoutes, 1863 RoutingSessionInfo currentSystemSessionInfo) { 1864 mHandler.sendMessage(obtainMessage(MediaRouter2::syncRoutesOnHandler, 1865 MediaRouter2.this, currentRoutes, currentSystemSessionInfo)); 1866 } 1867 1868 @Override notifyRoutesAdded(List<MediaRoute2Info> routes)1869 public void notifyRoutesAdded(List<MediaRoute2Info> routes) { 1870 mHandler.sendMessage(obtainMessage(MediaRouter2::addRoutesOnHandler, 1871 MediaRouter2.this, routes)); 1872 } 1873 1874 @Override notifyRoutesRemoved(List<MediaRoute2Info> routes)1875 public void notifyRoutesRemoved(List<MediaRoute2Info> routes) { 1876 mHandler.sendMessage(obtainMessage(MediaRouter2::removeRoutesOnHandler, 1877 MediaRouter2.this, routes)); 1878 } 1879 1880 @Override notifyRoutesChanged(List<MediaRoute2Info> routes)1881 public void notifyRoutesChanged(List<MediaRoute2Info> routes) { 1882 mHandler.sendMessage(obtainMessage(MediaRouter2::changeRoutesOnHandler, 1883 MediaRouter2.this, routes)); 1884 } 1885 1886 @Override notifySessionCreated(int requestId, @Nullable RoutingSessionInfo sessionInfo)1887 public void notifySessionCreated(int requestId, @Nullable RoutingSessionInfo sessionInfo) { 1888 mHandler.sendMessage(obtainMessage(MediaRouter2::createControllerOnHandler, 1889 MediaRouter2.this, requestId, sessionInfo)); 1890 } 1891 1892 @Override notifySessionInfoChanged(@ullable RoutingSessionInfo sessionInfo)1893 public void notifySessionInfoChanged(@Nullable RoutingSessionInfo sessionInfo) { 1894 mHandler.sendMessage(obtainMessage(MediaRouter2::updateControllerOnHandler, 1895 MediaRouter2.this, sessionInfo)); 1896 } 1897 1898 @Override notifySessionReleased(RoutingSessionInfo sessionInfo)1899 public void notifySessionReleased(RoutingSessionInfo sessionInfo) { 1900 mHandler.sendMessage(obtainMessage(MediaRouter2::releaseControllerOnHandler, 1901 MediaRouter2.this, sessionInfo)); 1902 } 1903 1904 @Override requestCreateSessionByManager(long managerRequestId, RoutingSessionInfo oldSession, MediaRoute2Info route)1905 public void requestCreateSessionByManager(long managerRequestId, 1906 RoutingSessionInfo oldSession, MediaRoute2Info route) { 1907 mHandler.sendMessage(obtainMessage( 1908 MediaRouter2::onRequestCreateControllerByManagerOnHandler, 1909 MediaRouter2.this, oldSession, route, managerRequestId)); 1910 } 1911 } 1912 1913 // Note: All methods are run on main thread. 1914 class ManagerCallback implements MediaRouter2Manager.Callback { 1915 1916 @Override onRoutesAdded(@onNull List<MediaRoute2Info> routes)1917 public void onRoutesAdded(@NonNull List<MediaRoute2Info> routes) { 1918 updateAllRoutesFromManager(); 1919 1920 List<MediaRoute2Info> filteredRoutes; 1921 synchronized (mLock) { 1922 filteredRoutes = filterRoutes(routes, mDiscoveryPreference); 1923 } 1924 if (filteredRoutes.isEmpty()) { 1925 return; 1926 } 1927 for (RouteCallbackRecord record: mRouteCallbackRecords) { 1928 record.mExecutor.execute( 1929 () -> record.mRouteCallback.onRoutesAdded(filteredRoutes)); 1930 } 1931 } 1932 1933 @Override onRoutesRemoved(@onNull List<MediaRoute2Info> routes)1934 public void onRoutesRemoved(@NonNull List<MediaRoute2Info> routes) { 1935 updateAllRoutesFromManager(); 1936 1937 List<MediaRoute2Info> filteredRoutes; 1938 synchronized (mLock) { 1939 filteredRoutes = filterRoutes(routes, mDiscoveryPreference); 1940 } 1941 if (filteredRoutes.isEmpty()) { 1942 return; 1943 } 1944 for (RouteCallbackRecord record: mRouteCallbackRecords) { 1945 record.mExecutor.execute( 1946 () -> record.mRouteCallback.onRoutesRemoved(filteredRoutes)); 1947 } 1948 } 1949 1950 @Override onRoutesChanged(@onNull List<MediaRoute2Info> routes)1951 public void onRoutesChanged(@NonNull List<MediaRoute2Info> routes) { 1952 updateAllRoutesFromManager(); 1953 1954 List<MediaRoute2Info> filteredRoutes; 1955 synchronized (mLock) { 1956 filteredRoutes = filterRoutes(routes, mDiscoveryPreference); 1957 } 1958 if (filteredRoutes.isEmpty()) { 1959 return; 1960 } 1961 for (RouteCallbackRecord record: mRouteCallbackRecords) { 1962 record.mExecutor.execute( 1963 () -> record.mRouteCallback.onRoutesChanged(filteredRoutes)); 1964 } 1965 } 1966 1967 @Override onTransferred(@onNull RoutingSessionInfo oldSession, @NonNull RoutingSessionInfo newSession)1968 public void onTransferred(@NonNull RoutingSessionInfo oldSession, 1969 @NonNull RoutingSessionInfo newSession) { 1970 if (!oldSession.isSystemSession() 1971 && !TextUtils.equals(mClientPackageName, oldSession.getClientPackageName())) { 1972 return; 1973 } 1974 1975 if (!newSession.isSystemSession() 1976 && !TextUtils.equals(mClientPackageName, newSession.getClientPackageName())) { 1977 return; 1978 } 1979 1980 // For successful in-session transfer, onControllerUpdated() handles it. 1981 if (TextUtils.equals(oldSession.getId(), newSession.getId())) { 1982 return; 1983 } 1984 1985 1986 RoutingController oldController; 1987 if (oldSession.isSystemSession()) { 1988 mSystemController.setRoutingSessionInfo( 1989 ensureClientPackageNameForSystemSession(oldSession)); 1990 oldController = mSystemController; 1991 } else { 1992 oldController = new RoutingController(oldSession); 1993 } 1994 1995 RoutingController newController; 1996 if (newSession.isSystemSession()) { 1997 mSystemController.setRoutingSessionInfo( 1998 ensureClientPackageNameForSystemSession(newSession)); 1999 newController = mSystemController; 2000 } else { 2001 newController = new RoutingController(newSession); 2002 } 2003 2004 notifyTransfer(oldController, newController); 2005 } 2006 2007 @Override onTransferFailed(@onNull RoutingSessionInfo session, @NonNull MediaRoute2Info route)2008 public void onTransferFailed(@NonNull RoutingSessionInfo session, 2009 @NonNull MediaRoute2Info route) { 2010 if (!session.isSystemSession() 2011 && !TextUtils.equals(mClientPackageName, session.getClientPackageName())) { 2012 return; 2013 } 2014 notifyTransferFailure(route); 2015 } 2016 2017 @Override onSessionUpdated(@onNull RoutingSessionInfo session)2018 public void onSessionUpdated(@NonNull RoutingSessionInfo session) { 2019 if (!session.isSystemSession() 2020 && !TextUtils.equals(mClientPackageName, session.getClientPackageName())) { 2021 return; 2022 } 2023 2024 RoutingController controller; 2025 if (session.isSystemSession()) { 2026 mSystemController.setRoutingSessionInfo( 2027 ensureClientPackageNameForSystemSession(session)); 2028 controller = mSystemController; 2029 } else { 2030 controller = new RoutingController(session); 2031 } 2032 notifyControllerUpdated(controller); 2033 } 2034 2035 @Override onSessionReleased(@onNull RoutingSessionInfo session)2036 public void onSessionReleased(@NonNull RoutingSessionInfo session) { 2037 if (session.isSystemSession()) { 2038 Log.e(TAG, "onSessionReleased: Called on system session. Ignoring."); 2039 return; 2040 } 2041 2042 if (!TextUtils.equals(mClientPackageName, session.getClientPackageName())) { 2043 return; 2044 } 2045 2046 notifyStop(new RoutingController(session, RoutingController.CONTROLLER_STATE_RELEASED)); 2047 } 2048 2049 @Override onPreferredFeaturesChanged(@onNull String packageName, @NonNull List<String> preferredFeatures)2050 public void onPreferredFeaturesChanged(@NonNull String packageName, 2051 @NonNull List<String> preferredFeatures) { 2052 if (!TextUtils.equals(mClientPackageName, packageName)) { 2053 return; 2054 } 2055 2056 synchronized (mLock) { 2057 mDiscoveryPreference = new RouteDiscoveryPreference.Builder( 2058 preferredFeatures, true).build(); 2059 } 2060 2061 updateAllRoutesFromManager(); 2062 notifyPreferredFeaturesChanged(preferredFeatures); 2063 } 2064 2065 @Override onRequestFailed(int reason)2066 public void onRequestFailed(int reason) { 2067 // Does nothing. 2068 } 2069 } 2070 } 2071