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 android.media.MediaRouter2.SCANNING_STATE_NOT_SCANNING; 20 import static android.media.MediaRouter2.SCANNING_STATE_WHILE_INTERACTIVE; 21 22 import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; 23 24 import android.Manifest; 25 import android.annotation.CallbackExecutor; 26 import android.annotation.NonNull; 27 import android.annotation.Nullable; 28 import android.annotation.RequiresPermission; 29 import android.content.Context; 30 import android.media.session.MediaController; 31 import android.media.session.MediaSessionManager; 32 import android.os.Handler; 33 import android.os.Message; 34 import android.os.RemoteException; 35 import android.os.ServiceManager; 36 import android.os.UserHandle; 37 import android.text.TextUtils; 38 import android.util.ArrayMap; 39 import android.util.ArraySet; 40 import android.util.Log; 41 42 import com.android.internal.annotations.GuardedBy; 43 import com.android.internal.annotations.VisibleForTesting; 44 import com.android.internal.util.Preconditions; 45 46 import java.util.ArrayList; 47 import java.util.Collections; 48 import java.util.Comparator; 49 import java.util.HashMap; 50 import java.util.List; 51 import java.util.Map; 52 import java.util.Objects; 53 import java.util.Set; 54 import java.util.concurrent.ConcurrentHashMap; 55 import java.util.concurrent.ConcurrentMap; 56 import java.util.concurrent.CopyOnWriteArrayList; 57 import java.util.concurrent.Executor; 58 import java.util.concurrent.atomic.AtomicInteger; 59 import java.util.function.Predicate; 60 import java.util.stream.Collectors; 61 62 /** 63 * A class that monitors and controls media routing of other apps. 64 * {@link android.Manifest.permission#MEDIA_CONTENT_CONTROL} is required to use this class, 65 * or {@link SecurityException} will be thrown. 66 * @hide 67 */ 68 public final class MediaRouter2Manager { 69 private static final String TAG = "MR2Manager"; 70 private static final Object sLock = new Object(); 71 /** 72 * The request ID for requests not asked by this instance. 73 * Shouldn't be used for a valid request. 74 * @hide 75 */ 76 public static final int REQUEST_ID_NONE = 0; 77 /** @hide */ 78 @VisibleForTesting 79 public static final int TRANSFER_TIMEOUT_MS = 30_000; 80 81 @GuardedBy("sLock") 82 private static MediaRouter2Manager sInstance; 83 84 private final Context mContext; 85 private final MediaSessionManager mMediaSessionManager; 86 private final Client mClient; 87 private final IMediaRouterService mMediaRouterService; 88 private final AtomicInteger mScanRequestCount = new AtomicInteger(/* initialValue= */ 0); 89 final Handler mHandler; 90 final CopyOnWriteArrayList<CallbackRecord> mCallbackRecords = new CopyOnWriteArrayList<>(); 91 92 private final Object mRoutesLock = new Object(); 93 @GuardedBy("mRoutesLock") 94 private final Map<String, MediaRoute2Info> mRoutes = new HashMap<>(); 95 @NonNull 96 final ConcurrentMap<String, RouteDiscoveryPreference> mDiscoveryPreferenceMap = 97 new ConcurrentHashMap<>(); 98 // TODO(b/241888071): Merge mDiscoveryPreferenceMap and mPackageToRouteListingPreferenceMap into 99 // a single record object maintained by a single package-to-record map. 100 @NonNull 101 private final ConcurrentMap<String, RouteListingPreference> 102 mPackageToRouteListingPreferenceMap = new ConcurrentHashMap<>(); 103 104 private final AtomicInteger mNextRequestId = new AtomicInteger(1); 105 private final CopyOnWriteArrayList<TransferRequest> mTransferRequests = 106 new CopyOnWriteArrayList<>(); 107 108 /** 109 * Gets an instance of media router manager that controls media route of other applications. 110 * 111 * @return The media router manager instance for the context. 112 */ getInstance(@onNull Context context)113 public static MediaRouter2Manager getInstance(@NonNull Context context) { 114 Objects.requireNonNull(context, "context must not be null"); 115 synchronized (sLock) { 116 if (sInstance == null) { 117 sInstance = new MediaRouter2Manager(context); 118 } 119 return sInstance; 120 } 121 } 122 MediaRouter2Manager(Context context)123 private MediaRouter2Manager(Context context) { 124 mContext = context.getApplicationContext(); 125 mMediaRouterService = IMediaRouterService.Stub.asInterface( 126 ServiceManager.getService(Context.MEDIA_ROUTER_SERVICE)); 127 mMediaSessionManager = (MediaSessionManager) context 128 .getSystemService(Context.MEDIA_SESSION_SERVICE); 129 mHandler = new Handler(context.getMainLooper()); 130 mClient = new Client(); 131 try { 132 mMediaRouterService.registerManager(mClient, context.getPackageName()); 133 } catch (RemoteException ex) { 134 throw ex.rethrowFromSystemServer(); 135 } 136 } 137 138 /** 139 * Registers a callback to listen route info. 140 * 141 * @param executor the executor that runs the callback 142 * @param callback the callback to add 143 */ registerCallback(@onNull @allbackExecutor Executor executor, @NonNull Callback callback)144 public void registerCallback(@NonNull @CallbackExecutor Executor executor, 145 @NonNull Callback callback) { 146 Objects.requireNonNull(executor, "executor must not be null"); 147 Objects.requireNonNull(callback, "callback must not be null"); 148 149 CallbackRecord callbackRecord = new CallbackRecord(executor, callback); 150 if (!mCallbackRecords.addIfAbsent(callbackRecord)) { 151 Log.w(TAG, "Ignoring to register the same callback twice."); 152 return; 153 } 154 } 155 156 /** 157 * Unregisters the specified callback. 158 * 159 * @param callback the callback to unregister 160 */ unregisterCallback(@onNull Callback callback)161 public void unregisterCallback(@NonNull Callback callback) { 162 Objects.requireNonNull(callback, "callback must not be null"); 163 164 if (!mCallbackRecords.remove(new CallbackRecord(null, callback))) { 165 Log.w(TAG, "unregisterCallback: Ignore unknown callback. " + callback); 166 return; 167 } 168 } 169 170 /** 171 * Registers a request to scan for remote routes. 172 * 173 * <p>Increases the count of active scanning requests. When the count transitions from zero to 174 * one, sends a request to the system server to start scanning. 175 * 176 * <p>Clients must {@link #unregisterScanRequest() unregister their scan requests} when scanning 177 * is no longer needed, to avoid unnecessary resource usage. 178 */ registerScanRequest()179 public void registerScanRequest() { 180 if (mScanRequestCount.getAndIncrement() == 0) { 181 try { 182 mMediaRouterService.updateScanningState(mClient, SCANNING_STATE_WHILE_INTERACTIVE); 183 } catch (RemoteException ex) { 184 throw ex.rethrowFromSystemServer(); 185 } 186 } 187 } 188 189 /** 190 * Unregisters a scan request made by {@link #registerScanRequest()}. 191 * 192 * <p>Decreases the count of active scanning requests. When the count transitions from one to 193 * zero, sends a request to the system server to stop scanning. 194 * 195 * @throws IllegalStateException If called while there are no active scan requests. 196 */ unregisterScanRequest()197 public void unregisterScanRequest() { 198 if (mScanRequestCount.updateAndGet( 199 count -> { 200 if (count == 0) { 201 throw new IllegalStateException( 202 "No active scan requests to unregister."); 203 } else { 204 return --count; 205 } 206 }) 207 == 0) { 208 try { 209 mMediaRouterService.updateScanningState(mClient, SCANNING_STATE_NOT_SCANNING); 210 } catch (RemoteException ex) { 211 throw ex.rethrowFromSystemServer(); 212 } 213 } 214 } 215 216 /** 217 * Gets a {@link android.media.session.MediaController} associated with the 218 * given routing session. 219 * If there is no matching media session, {@code null} is returned. 220 */ 221 @Nullable getMediaControllerForRoutingSession( @onNull RoutingSessionInfo sessionInfo)222 public MediaController getMediaControllerForRoutingSession( 223 @NonNull RoutingSessionInfo sessionInfo) { 224 for (MediaController controller : mMediaSessionManager.getActiveSessions(null)) { 225 if (areSessionsMatched(controller, sessionInfo)) { 226 return controller; 227 } 228 } 229 return null; 230 } 231 232 /** 233 * Gets available routes for an application. 234 * 235 * @param packageName the package name of the application 236 */ 237 @NonNull getAvailableRoutes(@onNull String packageName)238 public List<MediaRoute2Info> getAvailableRoutes(@NonNull String packageName) { 239 Objects.requireNonNull(packageName, "packageName must not be null"); 240 241 List<RoutingSessionInfo> sessions = getRoutingSessions(packageName); 242 return getAvailableRoutes(sessions.get(sessions.size() - 1)); 243 } 244 245 /** 246 * Gets routes that can be transferable seamlessly for an application. 247 * 248 * @param packageName the package name of the application 249 */ 250 @NonNull getTransferableRoutes(@onNull String packageName)251 public List<MediaRoute2Info> getTransferableRoutes(@NonNull String packageName) { 252 Objects.requireNonNull(packageName, "packageName must not be null"); 253 254 List<RoutingSessionInfo> sessions = getRoutingSessions(packageName); 255 return getTransferableRoutes(sessions.get(sessions.size() - 1)); 256 } 257 258 /** 259 * Gets available routes for the given routing session. 260 * The returned routes can be passed to 261 * {@link #transfer(RoutingSessionInfo, MediaRoute2Info)} for transferring the routing session. 262 * 263 * @param sessionInfo the routing session that would be transferred 264 */ 265 @NonNull getAvailableRoutes(@onNull RoutingSessionInfo sessionInfo)266 public List<MediaRoute2Info> getAvailableRoutes(@NonNull RoutingSessionInfo sessionInfo) { 267 return getFilteredRoutes(sessionInfo, /*includeSelectedRoutes=*/true, 268 /*additionalFilter=*/null); 269 } 270 271 /** 272 * Gets routes that can be transferable seamlessly for the given routing session. 273 * The returned routes can be passed to 274 * {@link #transfer(RoutingSessionInfo, MediaRoute2Info)} for transferring the routing session. 275 * <p> 276 * This includes routes that are {@link RoutingSessionInfo#getTransferableRoutes() transferable} 277 * by provider itself and routes that are different playback type (e.g. local/remote) 278 * from the given routing session. 279 * 280 * @param sessionInfo the routing session that would be transferred 281 */ 282 @NonNull getTransferableRoutes(@onNull RoutingSessionInfo sessionInfo)283 public List<MediaRoute2Info> getTransferableRoutes(@NonNull RoutingSessionInfo sessionInfo) { 284 return getFilteredRoutes(sessionInfo, /*includeSelectedRoutes=*/false, 285 (route) -> sessionInfo.isSystemSession() ^ route.isSystemRoute()); 286 } 287 getSortedRoutes(RouteDiscoveryPreference preference)288 private List<MediaRoute2Info> getSortedRoutes(RouteDiscoveryPreference preference) { 289 if (!preference.shouldRemoveDuplicates()) { 290 synchronized (mRoutesLock) { 291 return List.copyOf(mRoutes.values()); 292 } 293 } 294 Map<String, Integer> packagePriority = new ArrayMap<>(); 295 int count = preference.getDeduplicationPackageOrder().size(); 296 for (int i = 0; i < count; i++) { 297 // the last package will have 1 as the priority 298 packagePriority.put(preference.getDeduplicationPackageOrder().get(i), count - i); 299 } 300 ArrayList<MediaRoute2Info> routes; 301 synchronized (mRoutesLock) { 302 routes = new ArrayList<>(mRoutes.values()); 303 } 304 // take the negative for descending order 305 routes.sort( 306 Comparator.comparingInt( 307 r -> -packagePriority.getOrDefault(r.getProviderPackageName(), 0))); 308 return routes; 309 } 310 getFilteredRoutes( @onNull RoutingSessionInfo sessionInfo, boolean includeSelectedRoutes, @Nullable Predicate<MediaRoute2Info> additionalFilter)311 private List<MediaRoute2Info> getFilteredRoutes( 312 @NonNull RoutingSessionInfo sessionInfo, 313 boolean includeSelectedRoutes, 314 @Nullable Predicate<MediaRoute2Info> additionalFilter) { 315 Objects.requireNonNull(sessionInfo, "sessionInfo must not be null"); 316 317 List<MediaRoute2Info> routes = new ArrayList<>(); 318 319 Set<String> deduplicationIdSet = new ArraySet<>(); 320 String packageName = sessionInfo.getClientPackageName(); 321 RouteDiscoveryPreference discoveryPreference = 322 mDiscoveryPreferenceMap.getOrDefault(packageName, RouteDiscoveryPreference.EMPTY); 323 324 for (MediaRoute2Info route : getSortedRoutes(discoveryPreference)) { 325 if (!route.isVisibleTo(packageName)) { 326 continue; 327 } 328 boolean transferableRoutesContainRoute = 329 sessionInfo.getTransferableRoutes().contains(route.getId()); 330 boolean selectedRoutesContainRoute = 331 sessionInfo.getSelectedRoutes().contains(route.getId()); 332 if (transferableRoutesContainRoute 333 || (includeSelectedRoutes && selectedRoutesContainRoute)) { 334 routes.add(route); 335 continue; 336 } 337 if (!route.hasAnyFeatures(discoveryPreference.getPreferredFeatures())) { 338 continue; 339 } 340 if (!discoveryPreference.getAllowedPackages().isEmpty() 341 && (route.getProviderPackageName() == null 342 || !discoveryPreference 343 .getAllowedPackages() 344 .contains(route.getProviderPackageName()))) { 345 continue; 346 } 347 if (additionalFilter != null && !additionalFilter.test(route)) { 348 continue; 349 } 350 if (discoveryPreference.shouldRemoveDuplicates()) { 351 if (!Collections.disjoint(deduplicationIdSet, route.getDeduplicationIds())) { 352 continue; 353 } 354 deduplicationIdSet.addAll(route.getDeduplicationIds()); 355 } 356 routes.add(route); 357 } 358 return routes; 359 } 360 361 /** 362 * Returns the preferred features of the specified package name. 363 */ 364 @NonNull getDiscoveryPreference(@onNull String packageName)365 public RouteDiscoveryPreference getDiscoveryPreference(@NonNull String packageName) { 366 Objects.requireNonNull(packageName, "packageName must not be null"); 367 368 return mDiscoveryPreferenceMap.getOrDefault(packageName, RouteDiscoveryPreference.EMPTY); 369 } 370 371 /** 372 * Returns the {@link RouteListingPreference} of the app with the given {@code packageName}, or 373 * null if the app has not set any. 374 */ 375 @Nullable getRouteListingPreference(@onNull String packageName)376 public RouteListingPreference getRouteListingPreference(@NonNull String packageName) { 377 Preconditions.checkArgument(!TextUtils.isEmpty(packageName)); 378 return mPackageToRouteListingPreferenceMap.get(packageName); 379 } 380 381 /** 382 * Gets the system routing session for the given {@code targetPackageName}. Apps can select a 383 * route that is not the global route. (e.g. an app can select the device route while BT route 384 * is available.) 385 * 386 * @param targetPackageName the package name of the application. 387 */ 388 @Nullable getSystemRoutingSession(@ullable String targetPackageName)389 public RoutingSessionInfo getSystemRoutingSession(@Nullable String targetPackageName) { 390 try { 391 return mMediaRouterService.getSystemSessionInfoForPackage( 392 mContext.getPackageName(), targetPackageName); 393 } catch (RemoteException ex) { 394 throw ex.rethrowFromSystemServer(); 395 } 396 } 397 398 /** 399 * Gets the routing session of a media session. 400 * If the session is using {#link PlaybackInfo#PLAYBACK_TYPE_LOCAL local playback}, 401 * the system routing session is returned. 402 * If the session is using {#link PlaybackInfo#PLAYBACK_TYPE_REMOTE remote playback}, 403 * it returns the corresponding routing session or {@code null} if it's unavailable. 404 */ 405 @Nullable getRoutingSessionForMediaController(MediaController mediaController)406 public RoutingSessionInfo getRoutingSessionForMediaController(MediaController mediaController) { 407 MediaController.PlaybackInfo playbackInfo = mediaController.getPlaybackInfo(); 408 if (playbackInfo.getPlaybackType() == MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL) { 409 return getSystemRoutingSession(mediaController.getPackageName()); 410 } 411 for (RoutingSessionInfo sessionInfo : getRemoteSessions()) { 412 if (areSessionsMatched(mediaController, sessionInfo)) { 413 return sessionInfo; 414 } 415 } 416 return null; 417 } 418 419 /** 420 * Gets routing sessions of an application with the given package name. 421 * The first element of the returned list is the system routing session. 422 * 423 * @param packageName the package name of the application that is routing. 424 * @see #getSystemRoutingSession(String) 425 */ 426 @NonNull getRoutingSessions(@onNull String packageName)427 public List<RoutingSessionInfo> getRoutingSessions(@NonNull String packageName) { 428 Objects.requireNonNull(packageName, "packageName must not be null"); 429 430 List<RoutingSessionInfo> sessions = new ArrayList<>(); 431 sessions.add(getSystemRoutingSession(packageName)); 432 433 for (RoutingSessionInfo sessionInfo : getRemoteSessions()) { 434 if (TextUtils.equals(sessionInfo.getClientPackageName(), packageName)) { 435 sessions.add(sessionInfo); 436 } 437 } 438 return sessions; 439 } 440 441 /** 442 * Gets the list of all routing sessions except the system routing session. 443 * <p> 444 * If you want to transfer media of an application, use {@link #getRoutingSessions(String)}. 445 * If you want to get only the system routing session, use 446 * {@link #getSystemRoutingSession(String)}. 447 * 448 * @see #getRoutingSessions(String) 449 * @see #getSystemRoutingSession(String) 450 */ 451 @NonNull getRemoteSessions()452 public List<RoutingSessionInfo> getRemoteSessions() { 453 try { 454 return mMediaRouterService.getRemoteSessions(mClient); 455 } catch (RemoteException ex) { 456 throw ex.rethrowFromSystemServer(); 457 } 458 } 459 460 /** 461 * Gets the list of all discovered routes. 462 */ 463 @NonNull getAllRoutes()464 public List<MediaRoute2Info> getAllRoutes() { 465 List<MediaRoute2Info> routes = new ArrayList<>(); 466 synchronized (mRoutesLock) { 467 routes.addAll(mRoutes.values()); 468 } 469 return routes; 470 } 471 472 /** 473 * Transfers a {@link RoutingSessionInfo routing session} belonging to a specified package name 474 * to a {@link MediaRoute2Info media route}. 475 * 476 * <p>Same as {@link #transfer(RoutingSessionInfo, MediaRoute2Info)}, but resolves the routing 477 * session based on the provided package name. 478 */ 479 @RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL) transfer( @onNull String packageName, @NonNull MediaRoute2Info route, @NonNull UserHandle userHandle)480 public void transfer( 481 @NonNull String packageName, 482 @NonNull MediaRoute2Info route, 483 @NonNull UserHandle userHandle) { 484 Objects.requireNonNull(packageName, "packageName must not be null"); 485 Objects.requireNonNull(route, "route must not be null"); 486 487 List<RoutingSessionInfo> sessionInfos = getRoutingSessions(packageName); 488 RoutingSessionInfo targetSession = sessionInfos.get(sessionInfos.size() - 1); 489 transfer(targetSession, route, userHandle, packageName); 490 } 491 492 /** 493 * Transfers a routing session to a media route. 494 * 495 * <p>{@link Callback#onTransferred} or {@link Callback#onTransferFailed} will be called 496 * depending on the result. 497 * 498 * @param sessionInfo the routing session info to transfer 499 * @param route the route transfer to 500 * @param transferInitiatorUserHandle the user handle of an app initiated the transfer 501 * @param transferInitiatorPackageName the package name of an app initiated the transfer 502 * @see Callback#onTransferred(RoutingSessionInfo, RoutingSessionInfo) 503 * @see Callback#onTransferFailed(RoutingSessionInfo, MediaRoute2Info) 504 */ 505 @RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL) transfer( @onNull RoutingSessionInfo sessionInfo, @NonNull MediaRoute2Info route, @NonNull UserHandle transferInitiatorUserHandle, @NonNull String transferInitiatorPackageName)506 public void transfer( 507 @NonNull RoutingSessionInfo sessionInfo, 508 @NonNull MediaRoute2Info route, 509 @NonNull UserHandle transferInitiatorUserHandle, 510 @NonNull String transferInitiatorPackageName) { 511 Objects.requireNonNull(sessionInfo, "sessionInfo must not be null"); 512 Objects.requireNonNull(route, "route must not be null"); 513 Objects.requireNonNull(transferInitiatorUserHandle); 514 Objects.requireNonNull(transferInitiatorPackageName); 515 516 Log.v(TAG, "Transferring routing session. session= " + sessionInfo + ", route=" + route); 517 518 synchronized (mRoutesLock) { 519 if (!mRoutes.containsKey(route.getId())) { 520 Log.w(TAG, "transfer: Ignoring an unknown route id=" + route.getId()); 521 notifyTransferFailed(sessionInfo, route); 522 return; 523 } 524 } 525 526 if (sessionInfo.getTransferableRoutes().contains(route.getId())) { 527 transferToRoute( 528 sessionInfo, route, transferInitiatorUserHandle, transferInitiatorPackageName); 529 } else { 530 requestCreateSession(sessionInfo, route); 531 } 532 } 533 534 /** 535 * Requests a volume change for a route asynchronously. 536 * <p> 537 * It may have no effect if the route is currently not selected. 538 * </p> 539 * 540 * @param volume The new volume value between 0 and {@link MediaRoute2Info#getVolumeMax} 541 * (inclusive). 542 */ setRouteVolume(@onNull MediaRoute2Info route, int volume)543 public void setRouteVolume(@NonNull MediaRoute2Info route, int volume) { 544 Objects.requireNonNull(route, "route must not be null"); 545 546 if (route.getVolumeHandling() == MediaRoute2Info.PLAYBACK_VOLUME_FIXED) { 547 Log.w(TAG, "setRouteVolume: the route has fixed volume. Ignoring."); 548 return; 549 } 550 if (volume < 0 || volume > route.getVolumeMax()) { 551 Log.w(TAG, "setRouteVolume: the target volume is out of range. Ignoring"); 552 return; 553 } 554 555 try { 556 int requestId = mNextRequestId.getAndIncrement(); 557 mMediaRouterService.setRouteVolumeWithManager(mClient, requestId, route, volume); 558 } catch (RemoteException ex) { 559 throw ex.rethrowFromSystemServer(); 560 } 561 } 562 563 /** 564 * Requests a volume change for a routing session asynchronously. 565 * 566 * @param volume The new volume value between 0 and {@link RoutingSessionInfo#getVolumeMax} 567 * (inclusive). 568 */ setSessionVolume(@onNull RoutingSessionInfo sessionInfo, int volume)569 public void setSessionVolume(@NonNull RoutingSessionInfo sessionInfo, int volume) { 570 Objects.requireNonNull(sessionInfo, "sessionInfo must not be null"); 571 572 if (sessionInfo.getVolumeHandling() == MediaRoute2Info.PLAYBACK_VOLUME_FIXED) { 573 Log.w(TAG, "setSessionVolume: the route has fixed volume. Ignoring."); 574 return; 575 } 576 if (volume < 0 || volume > sessionInfo.getVolumeMax()) { 577 Log.w(TAG, "setSessionVolume: the target volume is out of range. Ignoring"); 578 return; 579 } 580 581 try { 582 int requestId = mNextRequestId.getAndIncrement(); 583 mMediaRouterService.setSessionVolumeWithManager( 584 mClient, requestId, sessionInfo.getId(), volume); 585 } catch (RemoteException ex) { 586 throw ex.rethrowFromSystemServer(); 587 } 588 } 589 updateRoutesOnHandler(@onNull List<MediaRoute2Info> routes)590 void updateRoutesOnHandler(@NonNull List<MediaRoute2Info> routes) { 591 synchronized (mRoutesLock) { 592 mRoutes.clear(); 593 for (MediaRoute2Info route : routes) { 594 mRoutes.put(route.getId(), route); 595 } 596 } 597 598 notifyRoutesUpdated(); 599 } 600 createSessionOnHandler(int requestId, RoutingSessionInfo sessionInfo)601 void createSessionOnHandler(int requestId, RoutingSessionInfo sessionInfo) { 602 TransferRequest matchingRequest = null; 603 for (TransferRequest request : mTransferRequests) { 604 if (request.mRequestId == requestId) { 605 matchingRequest = request; 606 break; 607 } 608 } 609 610 if (matchingRequest == null) { 611 return; 612 } 613 614 mTransferRequests.remove(matchingRequest); 615 616 MediaRoute2Info requestedRoute = matchingRequest.mTargetRoute; 617 618 if (sessionInfo == null) { 619 notifyTransferFailed(matchingRequest.mOldSessionInfo, requestedRoute); 620 return; 621 } else if (!sessionInfo.getSelectedRoutes().contains(requestedRoute.getId())) { 622 Log.w(TAG, "The session does not contain the requested route. " 623 + "(requestedRouteId=" + requestedRoute.getId() 624 + ", actualRoutes=" + sessionInfo.getSelectedRoutes() 625 + ")"); 626 notifyTransferFailed(matchingRequest.mOldSessionInfo, requestedRoute); 627 return; 628 } else if (!TextUtils.equals(requestedRoute.getProviderId(), 629 sessionInfo.getProviderId())) { 630 Log.w(TAG, "The session's provider ID does not match the requested route's. " 631 + "(requested route's providerId=" + requestedRoute.getProviderId() 632 + ", actual providerId=" + sessionInfo.getProviderId() 633 + ")"); 634 notifyTransferFailed(matchingRequest.mOldSessionInfo, requestedRoute); 635 return; 636 } 637 notifyTransferred(matchingRequest.mOldSessionInfo, sessionInfo); 638 } 639 handleFailureOnHandler(int requestId, int reason)640 void handleFailureOnHandler(int requestId, int reason) { 641 TransferRequest matchingRequest = null; 642 for (TransferRequest request : mTransferRequests) { 643 if (request.mRequestId == requestId) { 644 matchingRequest = request; 645 break; 646 } 647 } 648 649 if (matchingRequest != null) { 650 mTransferRequests.remove(matchingRequest); 651 notifyTransferFailed(matchingRequest.mOldSessionInfo, matchingRequest.mTargetRoute); 652 return; 653 } 654 notifyRequestFailed(reason); 655 } 656 handleSessionsUpdatedOnHandler(RoutingSessionInfo sessionInfo)657 void handleSessionsUpdatedOnHandler(RoutingSessionInfo sessionInfo) { 658 for (TransferRequest request : mTransferRequests) { 659 String sessionId = request.mOldSessionInfo.getId(); 660 if (!TextUtils.equals(sessionId, sessionInfo.getId())) { 661 continue; 662 } 663 if (sessionInfo.getSelectedRoutes().contains(request.mTargetRoute.getId())) { 664 mTransferRequests.remove(request); 665 notifyTransferred(request.mOldSessionInfo, sessionInfo); 666 break; 667 } 668 } 669 notifySessionUpdated(sessionInfo); 670 } 671 notifyRoutesUpdated()672 private void notifyRoutesUpdated() { 673 for (CallbackRecord record: mCallbackRecords) { 674 record.mExecutor.execute(() -> record.mCallback.onRoutesUpdated()); 675 } 676 } 677 notifySessionUpdated(RoutingSessionInfo sessionInfo)678 void notifySessionUpdated(RoutingSessionInfo sessionInfo) { 679 for (CallbackRecord record : mCallbackRecords) { 680 record.mExecutor.execute(() -> record.mCallback.onSessionUpdated(sessionInfo)); 681 } 682 } 683 notifySessionReleased(RoutingSessionInfo session)684 void notifySessionReleased(RoutingSessionInfo session) { 685 for (CallbackRecord record : mCallbackRecords) { 686 record.mExecutor.execute(() -> record.mCallback.onSessionReleased(session)); 687 } 688 } 689 notifyRequestFailed(int reason)690 void notifyRequestFailed(int reason) { 691 for (CallbackRecord record : mCallbackRecords) { 692 record.mExecutor.execute(() -> record.mCallback.onRequestFailed(reason)); 693 } 694 } 695 notifyTransferred(RoutingSessionInfo oldSession, RoutingSessionInfo newSession)696 void notifyTransferred(RoutingSessionInfo oldSession, RoutingSessionInfo newSession) { 697 for (CallbackRecord record : mCallbackRecords) { 698 record.mExecutor.execute(() -> record.mCallback.onTransferred(oldSession, newSession)); 699 } 700 } 701 notifyTransferFailed(RoutingSessionInfo sessionInfo, MediaRoute2Info route)702 void notifyTransferFailed(RoutingSessionInfo sessionInfo, MediaRoute2Info route) { 703 for (CallbackRecord record : mCallbackRecords) { 704 record.mExecutor.execute(() -> record.mCallback.onTransferFailed(sessionInfo, route)); 705 } 706 } 707 updateDiscoveryPreference(String packageName, RouteDiscoveryPreference preference)708 void updateDiscoveryPreference(String packageName, RouteDiscoveryPreference preference) { 709 if (preference == null) { 710 mDiscoveryPreferenceMap.remove(packageName); 711 return; 712 } 713 RouteDiscoveryPreference prevPreference = 714 mDiscoveryPreferenceMap.put(packageName, preference); 715 if (Objects.equals(preference, prevPreference)) { 716 return; 717 } 718 for (CallbackRecord record : mCallbackRecords) { 719 record.mExecutor.execute(() -> record.mCallback 720 .onDiscoveryPreferenceChanged(packageName, preference)); 721 } 722 } 723 updateRouteListingPreference( @onNull String packageName, @Nullable RouteListingPreference routeListingPreference)724 private void updateRouteListingPreference( 725 @NonNull String packageName, @Nullable RouteListingPreference routeListingPreference) { 726 RouteListingPreference oldRouteListingPreference = 727 routeListingPreference == null 728 ? mPackageToRouteListingPreferenceMap.remove(packageName) 729 : mPackageToRouteListingPreferenceMap.put( 730 packageName, routeListingPreference); 731 if (Objects.equals(oldRouteListingPreference, routeListingPreference)) { 732 return; 733 } 734 for (CallbackRecord record : mCallbackRecords) { 735 record.mExecutor.execute( 736 () -> 737 record.mCallback.onRouteListingPreferenceUpdated( 738 packageName, routeListingPreference)); 739 } 740 } 741 742 /** 743 * Gets the unmodifiable list of selected routes for the session. 744 */ 745 @NonNull getSelectedRoutes(@onNull RoutingSessionInfo sessionInfo)746 public List<MediaRoute2Info> getSelectedRoutes(@NonNull RoutingSessionInfo sessionInfo) { 747 Objects.requireNonNull(sessionInfo, "sessionInfo must not be null"); 748 749 synchronized (mRoutesLock) { 750 return sessionInfo.getSelectedRoutes().stream().map(mRoutes::get) 751 .filter(Objects::nonNull) 752 .collect(Collectors.toList()); 753 } 754 } 755 756 /** 757 * Gets the unmodifiable list of selectable routes for the session. 758 */ 759 @NonNull getSelectableRoutes(@onNull RoutingSessionInfo sessionInfo)760 public List<MediaRoute2Info> getSelectableRoutes(@NonNull RoutingSessionInfo sessionInfo) { 761 Objects.requireNonNull(sessionInfo, "sessionInfo must not be null"); 762 763 List<String> selectedRouteIds = sessionInfo.getSelectedRoutes(); 764 765 synchronized (mRoutesLock) { 766 return sessionInfo.getSelectableRoutes().stream() 767 .filter(routeId -> !selectedRouteIds.contains(routeId)) 768 .map(mRoutes::get) 769 .filter(Objects::nonNull) 770 .collect(Collectors.toList()); 771 } 772 } 773 774 /** 775 * Gets the unmodifiable list of deselectable routes for the session. 776 */ 777 @NonNull getDeselectableRoutes(@onNull RoutingSessionInfo sessionInfo)778 public List<MediaRoute2Info> getDeselectableRoutes(@NonNull RoutingSessionInfo sessionInfo) { 779 Objects.requireNonNull(sessionInfo, "sessionInfo must not be null"); 780 781 List<String> selectedRouteIds = sessionInfo.getSelectedRoutes(); 782 783 synchronized (mRoutesLock) { 784 return sessionInfo.getDeselectableRoutes().stream() 785 .filter(routeId -> selectedRouteIds.contains(routeId)) 786 .map(mRoutes::get) 787 .filter(Objects::nonNull) 788 .collect(Collectors.toList()); 789 } 790 } 791 792 /** 793 * Selects a route for the remote session. After a route is selected, the media is expected 794 * to be played to the all the selected routes. This is different from {@link 795 * #transfer(RoutingSessionInfo, MediaRoute2Info)} transferring to a route}, 796 * where the media is expected to 'move' from one route to another. 797 * <p> 798 * The given route must satisfy all of the following conditions: 799 * <ul> 800 * <li>it should not be included in {@link #getSelectedRoutes(RoutingSessionInfo)}</li> 801 * <li>it should be included in {@link #getSelectableRoutes(RoutingSessionInfo)}</li> 802 * </ul> 803 * If the route doesn't meet any of above conditions, it will be ignored. 804 * 805 * @see #getSelectedRoutes(RoutingSessionInfo) 806 * @see #getSelectableRoutes(RoutingSessionInfo) 807 * @see Callback#onSessionUpdated(RoutingSessionInfo) 808 */ selectRoute(@onNull RoutingSessionInfo sessionInfo, @NonNull MediaRoute2Info route)809 public void selectRoute(@NonNull RoutingSessionInfo sessionInfo, 810 @NonNull MediaRoute2Info route) { 811 Objects.requireNonNull(sessionInfo, "sessionInfo must not be null"); 812 Objects.requireNonNull(route, "route must not be null"); 813 814 if (sessionInfo.getSelectedRoutes().contains(route.getId())) { 815 Log.w(TAG, "Ignoring selecting a route that is already selected. route=" + route); 816 return; 817 } 818 819 if (!sessionInfo.getSelectableRoutes().contains(route.getId())) { 820 Log.w(TAG, "Ignoring selecting a non-selectable route=" + route); 821 return; 822 } 823 824 try { 825 int requestId = mNextRequestId.getAndIncrement(); 826 mMediaRouterService.selectRouteWithManager( 827 mClient, requestId, sessionInfo.getId(), route); 828 } catch (RemoteException ex) { 829 throw ex.rethrowFromSystemServer(); 830 } 831 } 832 833 /** 834 * Deselects a route from the remote session. After a route is deselected, the media is 835 * expected to be stopped on the deselected routes. 836 * <p> 837 * The given route must satisfy all of the following conditions: 838 * <ul> 839 * <li>it should be included in {@link #getSelectedRoutes(RoutingSessionInfo)}</li> 840 * <li>it should be included in {@link #getDeselectableRoutes(RoutingSessionInfo)}</li> 841 * </ul> 842 * If the route doesn't meet any of above conditions, it will be ignored. 843 * 844 * @see #getSelectedRoutes(RoutingSessionInfo) 845 * @see #getDeselectableRoutes(RoutingSessionInfo) 846 * @see Callback#onSessionUpdated(RoutingSessionInfo) 847 */ deselectRoute(@onNull RoutingSessionInfo sessionInfo, @NonNull MediaRoute2Info route)848 public void deselectRoute(@NonNull RoutingSessionInfo sessionInfo, 849 @NonNull MediaRoute2Info route) { 850 Objects.requireNonNull(sessionInfo, "sessionInfo must not be null"); 851 Objects.requireNonNull(route, "route must not be null"); 852 853 if (!sessionInfo.getSelectedRoutes().contains(route.getId())) { 854 Log.w(TAG, "Ignoring deselecting a route that is not selected. route=" + route); 855 return; 856 } 857 858 if (!sessionInfo.getDeselectableRoutes().contains(route.getId())) { 859 Log.w(TAG, "Ignoring deselecting a non-deselectable route=" + route); 860 return; 861 } 862 863 try { 864 int requestId = mNextRequestId.getAndIncrement(); 865 mMediaRouterService.deselectRouteWithManager( 866 mClient, requestId, sessionInfo.getId(), route); 867 } catch (RemoteException ex) { 868 throw ex.rethrowFromSystemServer(); 869 } 870 } 871 872 /** 873 * Requests releasing a session. 874 * <p> 875 * If a session is released, any operation on the session will be ignored. 876 * {@link Callback#onSessionReleased(RoutingSessionInfo)} will be called 877 * when the session is released. 878 * </p> 879 * 880 * @see Callback#onTransferred(RoutingSessionInfo, RoutingSessionInfo) 881 */ releaseSession(@onNull RoutingSessionInfo sessionInfo)882 public void releaseSession(@NonNull RoutingSessionInfo sessionInfo) { 883 Objects.requireNonNull(sessionInfo, "sessionInfo must not be null"); 884 885 try { 886 int requestId = mNextRequestId.getAndIncrement(); 887 mMediaRouterService.releaseSessionWithManager(mClient, requestId, sessionInfo.getId()); 888 } catch (RemoteException ex) { 889 throw ex.rethrowFromSystemServer(); 890 } 891 } 892 893 /** 894 * Transfers the remote session to the given route. 895 * 896 * @hide 897 */ 898 @RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL) transferToRoute( @onNull RoutingSessionInfo session, @NonNull MediaRoute2Info route, @NonNull UserHandle transferInitiatorUserHandle, @NonNull String transferInitiatorPackageName)899 private void transferToRoute( 900 @NonNull RoutingSessionInfo session, 901 @NonNull MediaRoute2Info route, 902 @NonNull UserHandle transferInitiatorUserHandle, 903 @NonNull String transferInitiatorPackageName) { 904 int requestId = createTransferRequest(session, route); 905 906 try { 907 mMediaRouterService.transferToRouteWithManager( 908 mClient, 909 requestId, 910 session.getId(), 911 route, 912 transferInitiatorUserHandle, 913 transferInitiatorPackageName); 914 } catch (RemoteException ex) { 915 throw ex.rethrowFromSystemServer(); 916 } 917 } 918 requestCreateSession(RoutingSessionInfo oldSession, MediaRoute2Info route)919 private void requestCreateSession(RoutingSessionInfo oldSession, MediaRoute2Info route) { 920 if (TextUtils.isEmpty(oldSession.getClientPackageName())) { 921 Log.w(TAG, "requestCreateSession: Can't create a session without package name."); 922 notifyTransferFailed(oldSession, route); 923 return; 924 } 925 926 int requestId = createTransferRequest(oldSession, route); 927 928 try { 929 mMediaRouterService.requestCreateSessionWithManager( 930 mClient, requestId, oldSession, route); 931 } catch (RemoteException ex) { 932 throw ex.rethrowFromSystemServer(); 933 } 934 } 935 createTransferRequest(RoutingSessionInfo session, MediaRoute2Info route)936 private int createTransferRequest(RoutingSessionInfo session, MediaRoute2Info route) { 937 int requestId = mNextRequestId.getAndIncrement(); 938 TransferRequest transferRequest = new TransferRequest(requestId, session, route); 939 mTransferRequests.add(transferRequest); 940 941 Message timeoutMessage = 942 obtainMessage(MediaRouter2Manager::handleTransferTimeout, this, transferRequest); 943 mHandler.sendMessageDelayed(timeoutMessage, TRANSFER_TIMEOUT_MS); 944 return requestId; 945 } 946 handleTransferTimeout(TransferRequest request)947 private void handleTransferTimeout(TransferRequest request) { 948 boolean removed = mTransferRequests.remove(request); 949 if (removed) { 950 notifyTransferFailed(request.mOldSessionInfo, request.mTargetRoute); 951 } 952 } 953 954 areSessionsMatched(MediaController mediaController, RoutingSessionInfo sessionInfo)955 private boolean areSessionsMatched(MediaController mediaController, 956 RoutingSessionInfo sessionInfo) { 957 MediaController.PlaybackInfo playbackInfo = mediaController.getPlaybackInfo(); 958 String volumeControlId = playbackInfo.getVolumeControlId(); 959 if (volumeControlId == null) { 960 return false; 961 } 962 963 if (TextUtils.equals(volumeControlId, sessionInfo.getId())) { 964 return true; 965 } 966 // Workaround for provider not being able to know the unique session ID. 967 return TextUtils.equals(volumeControlId, sessionInfo.getOriginalId()) 968 && TextUtils.equals(mediaController.getPackageName(), 969 sessionInfo.getOwnerPackageName()); 970 } 971 972 /** 973 * Interface for receiving events about media routing changes. 974 */ 975 public interface Callback { 976 977 /** 978 * Called when the routes list changes. This includes adding, modifying, or removing 979 * individual routes. 980 */ onRoutesUpdated()981 default void onRoutesUpdated() {} 982 983 /** 984 * Called when a session is changed. 985 * @param session the updated session 986 */ onSessionUpdated(@onNull RoutingSessionInfo session)987 default void onSessionUpdated(@NonNull RoutingSessionInfo session) {} 988 989 /** 990 * Called when a session is released. 991 * @param session the released session. 992 * @see #releaseSession(RoutingSessionInfo) 993 */ onSessionReleased(@onNull RoutingSessionInfo session)994 default void onSessionReleased(@NonNull RoutingSessionInfo session) {} 995 996 /** 997 * Called when media is transferred. 998 * 999 * @param oldSession the previous session 1000 * @param newSession the new session 1001 */ onTransferred(@onNull RoutingSessionInfo oldSession, @NonNull RoutingSessionInfo newSession)1002 default void onTransferred(@NonNull RoutingSessionInfo oldSession, 1003 @NonNull RoutingSessionInfo newSession) { } 1004 1005 /** 1006 * Called when {@link #transfer(RoutingSessionInfo, MediaRoute2Info)} fails. 1007 */ onTransferFailed(@onNull RoutingSessionInfo session, @NonNull MediaRoute2Info route)1008 default void onTransferFailed(@NonNull RoutingSessionInfo session, 1009 @NonNull MediaRoute2Info route) { } 1010 1011 /** 1012 * Called when the preferred route features of an app is changed. 1013 * 1014 * @param packageName the package name of the application 1015 * @param preferredFeatures the list of preferred route features set by an application. 1016 */ onPreferredFeaturesChanged(@onNull String packageName, @NonNull List<String> preferredFeatures)1017 default void onPreferredFeaturesChanged(@NonNull String packageName, 1018 @NonNull List<String> preferredFeatures) {} 1019 1020 /** 1021 * Called when the preferred route features of an app is changed. 1022 * 1023 * @param packageName the package name of the application 1024 * @param discoveryPreference the new discovery preference set by the application. 1025 */ onDiscoveryPreferenceChanged(@onNull String packageName, @NonNull RouteDiscoveryPreference discoveryPreference)1026 default void onDiscoveryPreferenceChanged(@NonNull String packageName, 1027 @NonNull RouteDiscoveryPreference discoveryPreference) { 1028 onPreferredFeaturesChanged(packageName, discoveryPreference.getPreferredFeatures()); 1029 } 1030 1031 /** 1032 * Called when the app with the given {@code packageName} updates its {@link 1033 * MediaRouter2#setRouteListingPreference route listing preference}. 1034 * 1035 * @param packageName The package name of the app that changed its listing preference. 1036 * @param routeListingPreference The new {@link RouteListingPreference} set by the app with 1037 * the given {@code packageName}. Maybe null if an app has unset its preference (by 1038 * passing null to {@link MediaRouter2#setRouteListingPreference}). 1039 */ onRouteListingPreferenceUpdated( @onNull String packageName, @Nullable RouteListingPreference routeListingPreference)1040 default void onRouteListingPreferenceUpdated( 1041 @NonNull String packageName, 1042 @Nullable RouteListingPreference routeListingPreference) {} 1043 1044 /** 1045 * Called when a previous request has failed. 1046 * 1047 * @param reason the reason that the request has failed. Can be one of followings: 1048 * {@link MediaRoute2ProviderService#REASON_UNKNOWN_ERROR}, 1049 * {@link MediaRoute2ProviderService#REASON_REJECTED}, 1050 * {@link MediaRoute2ProviderService#REASON_NETWORK_ERROR}, 1051 * {@link MediaRoute2ProviderService#REASON_ROUTE_NOT_AVAILABLE}, 1052 * {@link MediaRoute2ProviderService#REASON_INVALID_COMMAND}, 1053 */ onRequestFailed(int reason)1054 default void onRequestFailed(int reason) {} 1055 } 1056 1057 final class CallbackRecord { 1058 public final Executor mExecutor; 1059 public final Callback mCallback; 1060 CallbackRecord(Executor executor, Callback callback)1061 CallbackRecord(Executor executor, Callback callback) { 1062 mExecutor = executor; 1063 mCallback = callback; 1064 } 1065 1066 @Override equals(Object obj)1067 public boolean equals(Object obj) { 1068 if (this == obj) { 1069 return true; 1070 } 1071 if (!(obj instanceof CallbackRecord)) { 1072 return false; 1073 } 1074 return mCallback == ((CallbackRecord) obj).mCallback; 1075 } 1076 1077 @Override hashCode()1078 public int hashCode() { 1079 return mCallback.hashCode(); 1080 } 1081 } 1082 1083 static final class TransferRequest { 1084 public final int mRequestId; 1085 public final RoutingSessionInfo mOldSessionInfo; 1086 public final MediaRoute2Info mTargetRoute; 1087 TransferRequest(int requestId, @NonNull RoutingSessionInfo oldSessionInfo, @NonNull MediaRoute2Info targetRoute)1088 TransferRequest(int requestId, @NonNull RoutingSessionInfo oldSessionInfo, 1089 @NonNull MediaRoute2Info targetRoute) { 1090 mRequestId = requestId; 1091 mOldSessionInfo = oldSessionInfo; 1092 mTargetRoute = targetRoute; 1093 } 1094 } 1095 1096 class Client extends IMediaRouter2Manager.Stub { 1097 @Override notifySessionCreated(int requestId, RoutingSessionInfo session)1098 public void notifySessionCreated(int requestId, RoutingSessionInfo session) { 1099 mHandler.sendMessage(obtainMessage(MediaRouter2Manager::createSessionOnHandler, 1100 MediaRouter2Manager.this, requestId, session)); 1101 } 1102 1103 @Override notifySessionUpdated(RoutingSessionInfo session)1104 public void notifySessionUpdated(RoutingSessionInfo session) { 1105 mHandler.sendMessage(obtainMessage(MediaRouter2Manager::handleSessionsUpdatedOnHandler, 1106 MediaRouter2Manager.this, session)); 1107 } 1108 1109 @Override notifySessionReleased(RoutingSessionInfo session)1110 public void notifySessionReleased(RoutingSessionInfo session) { 1111 mHandler.sendMessage(obtainMessage(MediaRouter2Manager::notifySessionReleased, 1112 MediaRouter2Manager.this, session)); 1113 } 1114 1115 @Override notifyRequestFailed(int requestId, int reason)1116 public void notifyRequestFailed(int requestId, int reason) { 1117 // Note: requestId is not used. 1118 mHandler.sendMessage(obtainMessage(MediaRouter2Manager::handleFailureOnHandler, 1119 MediaRouter2Manager.this, requestId, reason)); 1120 } 1121 1122 @Override notifyDiscoveryPreferenceChanged(String packageName, RouteDiscoveryPreference discoveryPreference)1123 public void notifyDiscoveryPreferenceChanged(String packageName, 1124 RouteDiscoveryPreference discoveryPreference) { 1125 mHandler.sendMessage(obtainMessage(MediaRouter2Manager::updateDiscoveryPreference, 1126 MediaRouter2Manager.this, packageName, discoveryPreference)); 1127 } 1128 1129 @Override notifyRouteListingPreferenceChange( String packageName, @Nullable RouteListingPreference routeListingPreference)1130 public void notifyRouteListingPreferenceChange( 1131 String packageName, @Nullable RouteListingPreference routeListingPreference) { 1132 mHandler.sendMessage( 1133 obtainMessage( 1134 MediaRouter2Manager::updateRouteListingPreference, 1135 MediaRouter2Manager.this, 1136 packageName, 1137 routeListingPreference)); 1138 } 1139 1140 @Override notifyDeviceSuggestionsUpdated( String packageName, String suggestingPackageName, @Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo)1141 public void notifyDeviceSuggestionsUpdated( 1142 String packageName, 1143 String suggestingPackageName, 1144 @Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) { 1145 // MediaRouter2Manager doesn't support device suggestions 1146 } 1147 1148 @Override notifyRoutesUpdated(List<MediaRoute2Info> routes)1149 public void notifyRoutesUpdated(List<MediaRoute2Info> routes) { 1150 mHandler.sendMessage( 1151 obtainMessage( 1152 MediaRouter2Manager::updateRoutesOnHandler, 1153 MediaRouter2Manager.this, 1154 routes)); 1155 } 1156 1157 @Override invalidateInstance()1158 public void invalidateInstance() { 1159 // Should never happen since MediaRouter2Manager should only be used with 1160 // MEDIA_CONTENT_CONTROL, which cannot be revoked. 1161 } 1162 } 1163 } 1164