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