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(Comparator.comparingInt( 306 r -> -packagePriority.getOrDefault(r.getPackageName(), 0))); 307 return routes; 308 } 309 getFilteredRoutes(@onNull RoutingSessionInfo sessionInfo, boolean includeSelectedRoutes, @Nullable Predicate<MediaRoute2Info> additionalFilter)310 private List<MediaRoute2Info> getFilteredRoutes(@NonNull RoutingSessionInfo sessionInfo, 311 boolean includeSelectedRoutes, 312 @Nullable Predicate<MediaRoute2Info> additionalFilter) { 313 Objects.requireNonNull(sessionInfo, "sessionInfo must not be null"); 314 315 List<MediaRoute2Info> routes = new ArrayList<>(); 316 317 Set<String> deduplicationIdSet = new ArraySet<>(); 318 String packageName = sessionInfo.getClientPackageName(); 319 RouteDiscoveryPreference discoveryPreference = 320 mDiscoveryPreferenceMap.getOrDefault(packageName, RouteDiscoveryPreference.EMPTY); 321 322 for (MediaRoute2Info route : getSortedRoutes(discoveryPreference)) { 323 if (!route.isVisibleTo(packageName)) { 324 continue; 325 } 326 boolean transferableRoutesContainRoute = 327 sessionInfo.getTransferableRoutes().contains(route.getId()); 328 boolean selectedRoutesContainRoute = 329 sessionInfo.getSelectedRoutes().contains(route.getId()); 330 if (transferableRoutesContainRoute 331 || (includeSelectedRoutes && selectedRoutesContainRoute)) { 332 routes.add(route); 333 continue; 334 } 335 if (!route.hasAnyFeatures(discoveryPreference.getPreferredFeatures())) { 336 continue; 337 } 338 if (!discoveryPreference.getAllowedPackages().isEmpty() 339 && (route.getPackageName() == null 340 || !discoveryPreference.getAllowedPackages() 341 .contains(route.getPackageName()))) { 342 continue; 343 } 344 if (additionalFilter != null && !additionalFilter.test(route)) { 345 continue; 346 } 347 if (discoveryPreference.shouldRemoveDuplicates()) { 348 if (!Collections.disjoint(deduplicationIdSet, route.getDeduplicationIds())) { 349 continue; 350 } 351 deduplicationIdSet.addAll(route.getDeduplicationIds()); 352 } 353 routes.add(route); 354 } 355 return routes; 356 } 357 358 /** 359 * Returns the preferred features of the specified package name. 360 */ 361 @NonNull getDiscoveryPreference(@onNull String packageName)362 public RouteDiscoveryPreference getDiscoveryPreference(@NonNull String packageName) { 363 Objects.requireNonNull(packageName, "packageName must not be null"); 364 365 return mDiscoveryPreferenceMap.getOrDefault(packageName, RouteDiscoveryPreference.EMPTY); 366 } 367 368 /** 369 * Returns the {@link RouteListingPreference} of the app with the given {@code packageName}, or 370 * null if the app has not set any. 371 */ 372 @Nullable getRouteListingPreference(@onNull String packageName)373 public RouteListingPreference getRouteListingPreference(@NonNull String packageName) { 374 Preconditions.checkArgument(!TextUtils.isEmpty(packageName)); 375 return mPackageToRouteListingPreferenceMap.get(packageName); 376 } 377 378 /** 379 * Gets the system routing session for the given {@code targetPackageName}. Apps can select a 380 * route that is not the global route. (e.g. an app can select the device route while BT route 381 * is available.) 382 * 383 * @param targetPackageName the package name of the application. 384 */ 385 @Nullable getSystemRoutingSession(@ullable String targetPackageName)386 public RoutingSessionInfo getSystemRoutingSession(@Nullable String targetPackageName) { 387 try { 388 return mMediaRouterService.getSystemSessionInfoForPackage( 389 mContext.getPackageName(), targetPackageName); 390 } catch (RemoteException ex) { 391 throw ex.rethrowFromSystemServer(); 392 } 393 } 394 395 /** 396 * Gets the routing session of a media session. 397 * If the session is using {#link PlaybackInfo#PLAYBACK_TYPE_LOCAL local playback}, 398 * the system routing session is returned. 399 * If the session is using {#link PlaybackInfo#PLAYBACK_TYPE_REMOTE remote playback}, 400 * it returns the corresponding routing session or {@code null} if it's unavailable. 401 */ 402 @Nullable getRoutingSessionForMediaController(MediaController mediaController)403 public RoutingSessionInfo getRoutingSessionForMediaController(MediaController mediaController) { 404 MediaController.PlaybackInfo playbackInfo = mediaController.getPlaybackInfo(); 405 if (playbackInfo.getPlaybackType() == MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL) { 406 return getSystemRoutingSession(mediaController.getPackageName()); 407 } 408 for (RoutingSessionInfo sessionInfo : getRemoteSessions()) { 409 if (areSessionsMatched(mediaController, sessionInfo)) { 410 return sessionInfo; 411 } 412 } 413 return null; 414 } 415 416 /** 417 * Gets routing sessions of an application with the given package name. 418 * The first element of the returned list is the system routing session. 419 * 420 * @param packageName the package name of the application that is routing. 421 * @see #getSystemRoutingSession(String) 422 */ 423 @NonNull getRoutingSessions(@onNull String packageName)424 public List<RoutingSessionInfo> getRoutingSessions(@NonNull String packageName) { 425 Objects.requireNonNull(packageName, "packageName must not be null"); 426 427 List<RoutingSessionInfo> sessions = new ArrayList<>(); 428 sessions.add(getSystemRoutingSession(packageName)); 429 430 for (RoutingSessionInfo sessionInfo : getRemoteSessions()) { 431 if (TextUtils.equals(sessionInfo.getClientPackageName(), packageName)) { 432 sessions.add(sessionInfo); 433 } 434 } 435 return sessions; 436 } 437 438 /** 439 * Gets the list of all routing sessions except the system routing session. 440 * <p> 441 * If you want to transfer media of an application, use {@link #getRoutingSessions(String)}. 442 * If you want to get only the system routing session, use 443 * {@link #getSystemRoutingSession(String)}. 444 * 445 * @see #getRoutingSessions(String) 446 * @see #getSystemRoutingSession(String) 447 */ 448 @NonNull getRemoteSessions()449 public List<RoutingSessionInfo> getRemoteSessions() { 450 try { 451 return mMediaRouterService.getRemoteSessions(mClient); 452 } catch (RemoteException ex) { 453 throw ex.rethrowFromSystemServer(); 454 } 455 } 456 457 /** 458 * Gets the list of all discovered routes. 459 */ 460 @NonNull getAllRoutes()461 public List<MediaRoute2Info> getAllRoutes() { 462 List<MediaRoute2Info> routes = new ArrayList<>(); 463 synchronized (mRoutesLock) { 464 routes.addAll(mRoutes.values()); 465 } 466 return routes; 467 } 468 469 /** 470 * Transfers a {@link RoutingSessionInfo routing session} belonging to a specified package name 471 * to a {@link MediaRoute2Info media route}. 472 * 473 * <p>Same as {@link #transfer(RoutingSessionInfo, MediaRoute2Info)}, but resolves the routing 474 * session based on the provided package name. 475 */ 476 @RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL) transfer( @onNull String packageName, @NonNull MediaRoute2Info route, @NonNull UserHandle userHandle)477 public void transfer( 478 @NonNull String packageName, 479 @NonNull MediaRoute2Info route, 480 @NonNull UserHandle userHandle) { 481 Objects.requireNonNull(packageName, "packageName must not be null"); 482 Objects.requireNonNull(route, "route must not be null"); 483 484 List<RoutingSessionInfo> sessionInfos = getRoutingSessions(packageName); 485 RoutingSessionInfo targetSession = sessionInfos.get(sessionInfos.size() - 1); 486 transfer(targetSession, route, userHandle, packageName); 487 } 488 489 /** 490 * Transfers a routing session to a media route. 491 * 492 * <p>{@link Callback#onTransferred} or {@link Callback#onTransferFailed} will be called 493 * depending on the result. 494 * 495 * @param sessionInfo the routing session info to transfer 496 * @param route the route transfer to 497 * @param transferInitiatorUserHandle the user handle of an app initiated the transfer 498 * @param transferInitiatorPackageName the package name of an app initiated the transfer 499 * @see Callback#onTransferred(RoutingSessionInfo, RoutingSessionInfo) 500 * @see Callback#onTransferFailed(RoutingSessionInfo, MediaRoute2Info) 501 */ 502 @RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL) transfer( @onNull RoutingSessionInfo sessionInfo, @NonNull MediaRoute2Info route, @NonNull UserHandle transferInitiatorUserHandle, @NonNull String transferInitiatorPackageName)503 public void transfer( 504 @NonNull RoutingSessionInfo sessionInfo, 505 @NonNull MediaRoute2Info route, 506 @NonNull UserHandle transferInitiatorUserHandle, 507 @NonNull String transferInitiatorPackageName) { 508 Objects.requireNonNull(sessionInfo, "sessionInfo must not be null"); 509 Objects.requireNonNull(route, "route must not be null"); 510 Objects.requireNonNull(transferInitiatorUserHandle); 511 Objects.requireNonNull(transferInitiatorPackageName); 512 513 Log.v(TAG, "Transferring routing session. session= " + sessionInfo + ", route=" + route); 514 515 synchronized (mRoutesLock) { 516 if (!mRoutes.containsKey(route.getId())) { 517 Log.w(TAG, "transfer: Ignoring an unknown route id=" + route.getId()); 518 notifyTransferFailed(sessionInfo, route); 519 return; 520 } 521 } 522 523 if (sessionInfo.getTransferableRoutes().contains(route.getId())) { 524 transferToRoute( 525 sessionInfo, route, transferInitiatorUserHandle, transferInitiatorPackageName); 526 } else { 527 requestCreateSession(sessionInfo, route, transferInitiatorUserHandle, 528 transferInitiatorPackageName); 529 } 530 } 531 532 /** 533 * Requests a volume change for a route asynchronously. 534 * <p> 535 * It may have no effect if the route is currently not selected. 536 * </p> 537 * 538 * @param volume The new volume value between 0 and {@link MediaRoute2Info#getVolumeMax} 539 * (inclusive). 540 */ setRouteVolume(@onNull MediaRoute2Info route, int volume)541 public void setRouteVolume(@NonNull MediaRoute2Info route, int volume) { 542 Objects.requireNonNull(route, "route must not be null"); 543 544 if (route.getVolumeHandling() == MediaRoute2Info.PLAYBACK_VOLUME_FIXED) { 545 Log.w(TAG, "setRouteVolume: the route has fixed volume. Ignoring."); 546 return; 547 } 548 if (volume < 0 || volume > route.getVolumeMax()) { 549 Log.w(TAG, "setRouteVolume: the target volume is out of range. Ignoring"); 550 return; 551 } 552 553 try { 554 int requestId = mNextRequestId.getAndIncrement(); 555 mMediaRouterService.setRouteVolumeWithManager(mClient, requestId, route, volume); 556 } catch (RemoteException ex) { 557 throw ex.rethrowFromSystemServer(); 558 } 559 } 560 561 /** 562 * Requests a volume change for a routing session asynchronously. 563 * 564 * @param volume The new volume value between 0 and {@link RoutingSessionInfo#getVolumeMax} 565 * (inclusive). 566 */ setSessionVolume(@onNull RoutingSessionInfo sessionInfo, int volume)567 public void setSessionVolume(@NonNull RoutingSessionInfo sessionInfo, int volume) { 568 Objects.requireNonNull(sessionInfo, "sessionInfo must not be null"); 569 570 if (sessionInfo.getVolumeHandling() == MediaRoute2Info.PLAYBACK_VOLUME_FIXED) { 571 Log.w(TAG, "setSessionVolume: the route has fixed volume. Ignoring."); 572 return; 573 } 574 if (volume < 0 || volume > sessionInfo.getVolumeMax()) { 575 Log.w(TAG, "setSessionVolume: the target volume is out of range. Ignoring"); 576 return; 577 } 578 579 try { 580 int requestId = mNextRequestId.getAndIncrement(); 581 mMediaRouterService.setSessionVolumeWithManager( 582 mClient, requestId, sessionInfo.getId(), volume); 583 } catch (RemoteException ex) { 584 throw ex.rethrowFromSystemServer(); 585 } 586 } 587 updateRoutesOnHandler(@onNull List<MediaRoute2Info> routes)588 void updateRoutesOnHandler(@NonNull List<MediaRoute2Info> routes) { 589 synchronized (mRoutesLock) { 590 mRoutes.clear(); 591 for (MediaRoute2Info route : routes) { 592 mRoutes.put(route.getId(), route); 593 } 594 } 595 596 notifyRoutesUpdated(); 597 } 598 createSessionOnHandler(int requestId, RoutingSessionInfo sessionInfo)599 void createSessionOnHandler(int requestId, RoutingSessionInfo sessionInfo) { 600 TransferRequest matchingRequest = null; 601 for (TransferRequest request : mTransferRequests) { 602 if (request.mRequestId == requestId) { 603 matchingRequest = request; 604 break; 605 } 606 } 607 608 if (matchingRequest == null) { 609 return; 610 } 611 612 mTransferRequests.remove(matchingRequest); 613 614 MediaRoute2Info requestedRoute = matchingRequest.mTargetRoute; 615 616 if (sessionInfo == null) { 617 notifyTransferFailed(matchingRequest.mOldSessionInfo, requestedRoute); 618 return; 619 } else if (!sessionInfo.getSelectedRoutes().contains(requestedRoute.getId())) { 620 Log.w(TAG, "The session does not contain the requested route. " 621 + "(requestedRouteId=" + requestedRoute.getId() 622 + ", actualRoutes=" + sessionInfo.getSelectedRoutes() 623 + ")"); 624 notifyTransferFailed(matchingRequest.mOldSessionInfo, requestedRoute); 625 return; 626 } else if (!TextUtils.equals(requestedRoute.getProviderId(), 627 sessionInfo.getProviderId())) { 628 Log.w(TAG, "The session's provider ID does not match the requested route's. " 629 + "(requested route's providerId=" + requestedRoute.getProviderId() 630 + ", actual providerId=" + sessionInfo.getProviderId() 631 + ")"); 632 notifyTransferFailed(matchingRequest.mOldSessionInfo, requestedRoute); 633 return; 634 } 635 notifyTransferred(matchingRequest.mOldSessionInfo, sessionInfo); 636 } 637 handleFailureOnHandler(int requestId, int reason)638 void handleFailureOnHandler(int requestId, int reason) { 639 TransferRequest matchingRequest = null; 640 for (TransferRequest request : mTransferRequests) { 641 if (request.mRequestId == requestId) { 642 matchingRequest = request; 643 break; 644 } 645 } 646 647 if (matchingRequest != null) { 648 mTransferRequests.remove(matchingRequest); 649 notifyTransferFailed(matchingRequest.mOldSessionInfo, matchingRequest.mTargetRoute); 650 return; 651 } 652 notifyRequestFailed(reason); 653 } 654 handleSessionsUpdatedOnHandler(RoutingSessionInfo sessionInfo)655 void handleSessionsUpdatedOnHandler(RoutingSessionInfo sessionInfo) { 656 for (TransferRequest request : mTransferRequests) { 657 String sessionId = request.mOldSessionInfo.getId(); 658 if (!TextUtils.equals(sessionId, sessionInfo.getId())) { 659 continue; 660 } 661 if (sessionInfo.getSelectedRoutes().contains(request.mTargetRoute.getId())) { 662 mTransferRequests.remove(request); 663 notifyTransferred(request.mOldSessionInfo, sessionInfo); 664 break; 665 } 666 } 667 notifySessionUpdated(sessionInfo); 668 } 669 notifyRoutesUpdated()670 private void notifyRoutesUpdated() { 671 for (CallbackRecord record: mCallbackRecords) { 672 record.mExecutor.execute(() -> record.mCallback.onRoutesUpdated()); 673 } 674 } 675 notifySessionUpdated(RoutingSessionInfo sessionInfo)676 void notifySessionUpdated(RoutingSessionInfo sessionInfo) { 677 for (CallbackRecord record : mCallbackRecords) { 678 record.mExecutor.execute(() -> record.mCallback.onSessionUpdated(sessionInfo)); 679 } 680 } 681 notifySessionReleased(RoutingSessionInfo session)682 void notifySessionReleased(RoutingSessionInfo session) { 683 for (CallbackRecord record : mCallbackRecords) { 684 record.mExecutor.execute(() -> record.mCallback.onSessionReleased(session)); 685 } 686 } 687 notifyRequestFailed(int reason)688 void notifyRequestFailed(int reason) { 689 for (CallbackRecord record : mCallbackRecords) { 690 record.mExecutor.execute(() -> record.mCallback.onRequestFailed(reason)); 691 } 692 } 693 notifyTransferred(RoutingSessionInfo oldSession, RoutingSessionInfo newSession)694 void notifyTransferred(RoutingSessionInfo oldSession, RoutingSessionInfo newSession) { 695 for (CallbackRecord record : mCallbackRecords) { 696 record.mExecutor.execute(() -> record.mCallback.onTransferred(oldSession, newSession)); 697 } 698 } 699 notifyTransferFailed(RoutingSessionInfo sessionInfo, MediaRoute2Info route)700 void notifyTransferFailed(RoutingSessionInfo sessionInfo, MediaRoute2Info route) { 701 for (CallbackRecord record : mCallbackRecords) { 702 record.mExecutor.execute(() -> record.mCallback.onTransferFailed(sessionInfo, route)); 703 } 704 } 705 updateDiscoveryPreference(String packageName, RouteDiscoveryPreference preference)706 void updateDiscoveryPreference(String packageName, RouteDiscoveryPreference preference) { 707 if (preference == null) { 708 mDiscoveryPreferenceMap.remove(packageName); 709 return; 710 } 711 RouteDiscoveryPreference prevPreference = 712 mDiscoveryPreferenceMap.put(packageName, preference); 713 if (Objects.equals(preference, prevPreference)) { 714 return; 715 } 716 for (CallbackRecord record : mCallbackRecords) { 717 record.mExecutor.execute(() -> record.mCallback 718 .onDiscoveryPreferenceChanged(packageName, preference)); 719 } 720 } 721 updateRouteListingPreference( @onNull String packageName, @Nullable RouteListingPreference routeListingPreference)722 private void updateRouteListingPreference( 723 @NonNull String packageName, @Nullable RouteListingPreference routeListingPreference) { 724 RouteListingPreference oldRouteListingPreference = 725 routeListingPreference == null 726 ? mPackageToRouteListingPreferenceMap.remove(packageName) 727 : mPackageToRouteListingPreferenceMap.put( 728 packageName, routeListingPreference); 729 if (Objects.equals(oldRouteListingPreference, routeListingPreference)) { 730 return; 731 } 732 for (CallbackRecord record : mCallbackRecords) { 733 record.mExecutor.execute( 734 () -> 735 record.mCallback.onRouteListingPreferenceUpdated( 736 packageName, routeListingPreference)); 737 } 738 } 739 740 /** 741 * Gets the unmodifiable list of selected routes for the session. 742 */ 743 @NonNull getSelectedRoutes(@onNull RoutingSessionInfo sessionInfo)744 public List<MediaRoute2Info> getSelectedRoutes(@NonNull RoutingSessionInfo sessionInfo) { 745 Objects.requireNonNull(sessionInfo, "sessionInfo must not be null"); 746 747 synchronized (mRoutesLock) { 748 return sessionInfo.getSelectedRoutes().stream().map(mRoutes::get) 749 .filter(Objects::nonNull) 750 .collect(Collectors.toList()); 751 } 752 } 753 754 /** 755 * Gets the unmodifiable list of selectable routes for the session. 756 */ 757 @NonNull getSelectableRoutes(@onNull RoutingSessionInfo sessionInfo)758 public List<MediaRoute2Info> getSelectableRoutes(@NonNull RoutingSessionInfo sessionInfo) { 759 Objects.requireNonNull(sessionInfo, "sessionInfo must not be null"); 760 761 List<String> selectedRouteIds = sessionInfo.getSelectedRoutes(); 762 763 synchronized (mRoutesLock) { 764 return sessionInfo.getSelectableRoutes().stream() 765 .filter(routeId -> !selectedRouteIds.contains(routeId)) 766 .map(mRoutes::get) 767 .filter(Objects::nonNull) 768 .collect(Collectors.toList()); 769 } 770 } 771 772 /** 773 * Gets the unmodifiable list of deselectable routes for the session. 774 */ 775 @NonNull getDeselectableRoutes(@onNull RoutingSessionInfo sessionInfo)776 public List<MediaRoute2Info> getDeselectableRoutes(@NonNull RoutingSessionInfo sessionInfo) { 777 Objects.requireNonNull(sessionInfo, "sessionInfo must not be null"); 778 779 List<String> selectedRouteIds = sessionInfo.getSelectedRoutes(); 780 781 synchronized (mRoutesLock) { 782 return sessionInfo.getDeselectableRoutes().stream() 783 .filter(routeId -> selectedRouteIds.contains(routeId)) 784 .map(mRoutes::get) 785 .filter(Objects::nonNull) 786 .collect(Collectors.toList()); 787 } 788 } 789 790 /** 791 * Selects a route for the remote session. After a route is selected, the media is expected 792 * to be played to the all the selected routes. This is different from {@link 793 * #transfer(RoutingSessionInfo, MediaRoute2Info)} transferring to a route}, 794 * where the media is expected to 'move' from one route to another. 795 * <p> 796 * The given route must satisfy all of the following conditions: 797 * <ul> 798 * <li>it should not be included in {@link #getSelectedRoutes(RoutingSessionInfo)}</li> 799 * <li>it should be included in {@link #getSelectableRoutes(RoutingSessionInfo)}</li> 800 * </ul> 801 * If the route doesn't meet any of above conditions, it will be ignored. 802 * 803 * @see #getSelectedRoutes(RoutingSessionInfo) 804 * @see #getSelectableRoutes(RoutingSessionInfo) 805 * @see Callback#onSessionUpdated(RoutingSessionInfo) 806 */ selectRoute(@onNull RoutingSessionInfo sessionInfo, @NonNull MediaRoute2Info route)807 public void selectRoute(@NonNull RoutingSessionInfo sessionInfo, 808 @NonNull MediaRoute2Info route) { 809 Objects.requireNonNull(sessionInfo, "sessionInfo must not be null"); 810 Objects.requireNonNull(route, "route must not be null"); 811 812 if (sessionInfo.getSelectedRoutes().contains(route.getId())) { 813 Log.w(TAG, "Ignoring selecting a route that is already selected. route=" + route); 814 return; 815 } 816 817 if (!sessionInfo.getSelectableRoutes().contains(route.getId())) { 818 Log.w(TAG, "Ignoring selecting a non-selectable route=" + route); 819 return; 820 } 821 822 try { 823 int requestId = mNextRequestId.getAndIncrement(); 824 mMediaRouterService.selectRouteWithManager( 825 mClient, requestId, sessionInfo.getId(), route); 826 } catch (RemoteException ex) { 827 throw ex.rethrowFromSystemServer(); 828 } 829 } 830 831 /** 832 * Deselects a route from the remote session. After a route is deselected, the media is 833 * expected to be stopped on the deselected routes. 834 * <p> 835 * The given route must satisfy all of the following conditions: 836 * <ul> 837 * <li>it should be included in {@link #getSelectedRoutes(RoutingSessionInfo)}</li> 838 * <li>it should be included in {@link #getDeselectableRoutes(RoutingSessionInfo)}</li> 839 * </ul> 840 * If the route doesn't meet any of above conditions, it will be ignored. 841 * 842 * @see #getSelectedRoutes(RoutingSessionInfo) 843 * @see #getDeselectableRoutes(RoutingSessionInfo) 844 * @see Callback#onSessionUpdated(RoutingSessionInfo) 845 */ deselectRoute(@onNull RoutingSessionInfo sessionInfo, @NonNull MediaRoute2Info route)846 public void deselectRoute(@NonNull RoutingSessionInfo sessionInfo, 847 @NonNull MediaRoute2Info route) { 848 Objects.requireNonNull(sessionInfo, "sessionInfo must not be null"); 849 Objects.requireNonNull(route, "route must not be null"); 850 851 if (!sessionInfo.getSelectedRoutes().contains(route.getId())) { 852 Log.w(TAG, "Ignoring deselecting a route that is not selected. route=" + route); 853 return; 854 } 855 856 if (!sessionInfo.getDeselectableRoutes().contains(route.getId())) { 857 Log.w(TAG, "Ignoring deselecting a non-deselectable route=" + route); 858 return; 859 } 860 861 try { 862 int requestId = mNextRequestId.getAndIncrement(); 863 mMediaRouterService.deselectRouteWithManager( 864 mClient, requestId, sessionInfo.getId(), route); 865 } catch (RemoteException ex) { 866 throw ex.rethrowFromSystemServer(); 867 } 868 } 869 870 /** 871 * Requests releasing a session. 872 * <p> 873 * If a session is released, any operation on the session will be ignored. 874 * {@link Callback#onSessionReleased(RoutingSessionInfo)} will be called 875 * when the session is released. 876 * </p> 877 * 878 * @see Callback#onTransferred(RoutingSessionInfo, RoutingSessionInfo) 879 */ releaseSession(@onNull RoutingSessionInfo sessionInfo)880 public void releaseSession(@NonNull RoutingSessionInfo sessionInfo) { 881 Objects.requireNonNull(sessionInfo, "sessionInfo must not be null"); 882 883 try { 884 int requestId = mNextRequestId.getAndIncrement(); 885 mMediaRouterService.releaseSessionWithManager(mClient, requestId, sessionInfo.getId()); 886 } catch (RemoteException ex) { 887 throw ex.rethrowFromSystemServer(); 888 } 889 } 890 891 /** 892 * Transfers the remote session to the given route. 893 * 894 * @hide 895 */ 896 @RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL) transferToRoute( @onNull RoutingSessionInfo session, @NonNull MediaRoute2Info route, @NonNull UserHandle transferInitiatorUserHandle, @NonNull String transferInitiatorPackageName)897 private void transferToRoute( 898 @NonNull RoutingSessionInfo session, 899 @NonNull MediaRoute2Info route, 900 @NonNull UserHandle transferInitiatorUserHandle, 901 @NonNull String transferInitiatorPackageName) { 902 int requestId = createTransferRequest(session, route); 903 904 try { 905 mMediaRouterService.transferToRouteWithManager( 906 mClient, 907 requestId, 908 session.getId(), 909 route, 910 transferInitiatorUserHandle, 911 transferInitiatorPackageName); 912 } catch (RemoteException ex) { 913 throw ex.rethrowFromSystemServer(); 914 } 915 } 916 requestCreateSession(RoutingSessionInfo oldSession, MediaRoute2Info route, @NonNull UserHandle transferInitiatorUserHandle, @NonNull String transferInitiationPackageName)917 private void requestCreateSession(RoutingSessionInfo oldSession, MediaRoute2Info route, 918 @NonNull UserHandle transferInitiatorUserHandle, 919 @NonNull String transferInitiationPackageName) { 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, transferInitiatorUserHandle, 931 transferInitiationPackageName); 932 } catch (RemoteException ex) { 933 throw ex.rethrowFromSystemServer(); 934 } 935 } 936 createTransferRequest(RoutingSessionInfo session, MediaRoute2Info route)937 private int createTransferRequest(RoutingSessionInfo session, MediaRoute2Info route) { 938 int requestId = mNextRequestId.getAndIncrement(); 939 TransferRequest transferRequest = new TransferRequest(requestId, session, route); 940 mTransferRequests.add(transferRequest); 941 942 Message timeoutMessage = 943 obtainMessage(MediaRouter2Manager::handleTransferTimeout, this, transferRequest); 944 mHandler.sendMessageDelayed(timeoutMessage, TRANSFER_TIMEOUT_MS); 945 return requestId; 946 } 947 handleTransferTimeout(TransferRequest request)948 private void handleTransferTimeout(TransferRequest request) { 949 boolean removed = mTransferRequests.remove(request); 950 if (removed) { 951 notifyTransferFailed(request.mOldSessionInfo, request.mTargetRoute); 952 } 953 } 954 955 areSessionsMatched(MediaController mediaController, RoutingSessionInfo sessionInfo)956 private boolean areSessionsMatched(MediaController mediaController, 957 RoutingSessionInfo sessionInfo) { 958 MediaController.PlaybackInfo playbackInfo = mediaController.getPlaybackInfo(); 959 String volumeControlId = playbackInfo.getVolumeControlId(); 960 if (volumeControlId == null) { 961 return false; 962 } 963 964 if (TextUtils.equals(volumeControlId, sessionInfo.getId())) { 965 return true; 966 } 967 // Workaround for provider not being able to know the unique session ID. 968 return TextUtils.equals(volumeControlId, sessionInfo.getOriginalId()) 969 && TextUtils.equals(mediaController.getPackageName(), 970 sessionInfo.getOwnerPackageName()); 971 } 972 973 /** 974 * Interface for receiving events about media routing changes. 975 */ 976 public interface Callback { 977 978 /** 979 * Called when the routes list changes. This includes adding, modifying, or removing 980 * individual routes. 981 */ onRoutesUpdated()982 default void onRoutesUpdated() {} 983 984 /** 985 * Called when a session is changed. 986 * @param session the updated session 987 */ onSessionUpdated(@onNull RoutingSessionInfo session)988 default void onSessionUpdated(@NonNull RoutingSessionInfo session) {} 989 990 /** 991 * Called when a session is released. 992 * @param session the released session. 993 * @see #releaseSession(RoutingSessionInfo) 994 */ onSessionReleased(@onNull RoutingSessionInfo session)995 default void onSessionReleased(@NonNull RoutingSessionInfo session) {} 996 997 /** 998 * Called when media is transferred. 999 * 1000 * @param oldSession the previous session 1001 * @param newSession the new session 1002 */ onTransferred(@onNull RoutingSessionInfo oldSession, @NonNull RoutingSessionInfo newSession)1003 default void onTransferred(@NonNull RoutingSessionInfo oldSession, 1004 @NonNull RoutingSessionInfo newSession) { } 1005 1006 /** 1007 * Called when {@link #transfer(RoutingSessionInfo, MediaRoute2Info)} fails. 1008 */ onTransferFailed(@onNull RoutingSessionInfo session, @NonNull MediaRoute2Info route)1009 default void onTransferFailed(@NonNull RoutingSessionInfo session, 1010 @NonNull MediaRoute2Info route) { } 1011 1012 /** 1013 * Called when the preferred route features of an app is changed. 1014 * 1015 * @param packageName the package name of the application 1016 * @param preferredFeatures the list of preferred route features set by an application. 1017 */ onPreferredFeaturesChanged(@onNull String packageName, @NonNull List<String> preferredFeatures)1018 default void onPreferredFeaturesChanged(@NonNull String packageName, 1019 @NonNull List<String> preferredFeatures) {} 1020 1021 /** 1022 * Called when the preferred route features of an app is changed. 1023 * 1024 * @param packageName the package name of the application 1025 * @param discoveryPreference the new discovery preference set by the application. 1026 */ onDiscoveryPreferenceChanged(@onNull String packageName, @NonNull RouteDiscoveryPreference discoveryPreference)1027 default void onDiscoveryPreferenceChanged(@NonNull String packageName, 1028 @NonNull RouteDiscoveryPreference discoveryPreference) { 1029 onPreferredFeaturesChanged(packageName, discoveryPreference.getPreferredFeatures()); 1030 } 1031 1032 /** 1033 * Called when the app with the given {@code packageName} updates its {@link 1034 * MediaRouter2#setRouteListingPreference route listing preference}. 1035 * 1036 * @param packageName The package name of the app that changed its listing preference. 1037 * @param routeListingPreference The new {@link RouteListingPreference} set by the app with 1038 * the given {@code packageName}. Maybe null if an app has unset its preference (by 1039 * passing null to {@link MediaRouter2#setRouteListingPreference}). 1040 */ onRouteListingPreferenceUpdated( @onNull String packageName, @Nullable RouteListingPreference routeListingPreference)1041 default void onRouteListingPreferenceUpdated( 1042 @NonNull String packageName, 1043 @Nullable RouteListingPreference routeListingPreference) {} 1044 1045 /** 1046 * Called when a previous request has failed. 1047 * 1048 * @param reason the reason that the request has failed. Can be one of followings: 1049 * {@link MediaRoute2ProviderService#REASON_UNKNOWN_ERROR}, 1050 * {@link MediaRoute2ProviderService#REASON_REJECTED}, 1051 * {@link MediaRoute2ProviderService#REASON_NETWORK_ERROR}, 1052 * {@link MediaRoute2ProviderService#REASON_ROUTE_NOT_AVAILABLE}, 1053 * {@link MediaRoute2ProviderService#REASON_INVALID_COMMAND}, 1054 */ onRequestFailed(int reason)1055 default void onRequestFailed(int reason) {} 1056 } 1057 1058 final class CallbackRecord { 1059 public final Executor mExecutor; 1060 public final Callback mCallback; 1061 CallbackRecord(Executor executor, Callback callback)1062 CallbackRecord(Executor executor, Callback callback) { 1063 mExecutor = executor; 1064 mCallback = callback; 1065 } 1066 1067 @Override equals(Object obj)1068 public boolean equals(Object obj) { 1069 if (this == obj) { 1070 return true; 1071 } 1072 if (!(obj instanceof CallbackRecord)) { 1073 return false; 1074 } 1075 return mCallback == ((CallbackRecord) obj).mCallback; 1076 } 1077 1078 @Override hashCode()1079 public int hashCode() { 1080 return mCallback.hashCode(); 1081 } 1082 } 1083 1084 static final class TransferRequest { 1085 public final int mRequestId; 1086 public final RoutingSessionInfo mOldSessionInfo; 1087 public final MediaRoute2Info mTargetRoute; 1088 TransferRequest(int requestId, @NonNull RoutingSessionInfo oldSessionInfo, @NonNull MediaRoute2Info targetRoute)1089 TransferRequest(int requestId, @NonNull RoutingSessionInfo oldSessionInfo, 1090 @NonNull MediaRoute2Info targetRoute) { 1091 mRequestId = requestId; 1092 mOldSessionInfo = oldSessionInfo; 1093 mTargetRoute = targetRoute; 1094 } 1095 } 1096 1097 class Client extends IMediaRouter2Manager.Stub { 1098 @Override notifySessionCreated(int requestId, RoutingSessionInfo session)1099 public void notifySessionCreated(int requestId, RoutingSessionInfo session) { 1100 mHandler.sendMessage(obtainMessage(MediaRouter2Manager::createSessionOnHandler, 1101 MediaRouter2Manager.this, requestId, session)); 1102 } 1103 1104 @Override notifySessionUpdated(RoutingSessionInfo session)1105 public void notifySessionUpdated(RoutingSessionInfo session) { 1106 mHandler.sendMessage(obtainMessage(MediaRouter2Manager::handleSessionsUpdatedOnHandler, 1107 MediaRouter2Manager.this, session)); 1108 } 1109 1110 @Override notifySessionReleased(RoutingSessionInfo session)1111 public void notifySessionReleased(RoutingSessionInfo session) { 1112 mHandler.sendMessage(obtainMessage(MediaRouter2Manager::notifySessionReleased, 1113 MediaRouter2Manager.this, session)); 1114 } 1115 1116 @Override notifyRequestFailed(int requestId, int reason)1117 public void notifyRequestFailed(int requestId, int reason) { 1118 // Note: requestId is not used. 1119 mHandler.sendMessage(obtainMessage(MediaRouter2Manager::handleFailureOnHandler, 1120 MediaRouter2Manager.this, requestId, reason)); 1121 } 1122 1123 @Override notifyDiscoveryPreferenceChanged(String packageName, RouteDiscoveryPreference discoveryPreference)1124 public void notifyDiscoveryPreferenceChanged(String packageName, 1125 RouteDiscoveryPreference discoveryPreference) { 1126 mHandler.sendMessage(obtainMessage(MediaRouter2Manager::updateDiscoveryPreference, 1127 MediaRouter2Manager.this, packageName, discoveryPreference)); 1128 } 1129 1130 @Override notifyRouteListingPreferenceChange( String packageName, @Nullable RouteListingPreference routeListingPreference)1131 public void notifyRouteListingPreferenceChange( 1132 String packageName, @Nullable RouteListingPreference routeListingPreference) { 1133 mHandler.sendMessage( 1134 obtainMessage( 1135 MediaRouter2Manager::updateRouteListingPreference, 1136 MediaRouter2Manager.this, 1137 packageName, 1138 routeListingPreference)); 1139 } 1140 1141 @Override notifyRoutesUpdated(List<MediaRoute2Info> routes)1142 public void notifyRoutesUpdated(List<MediaRoute2Info> routes) { 1143 mHandler.sendMessage( 1144 obtainMessage( 1145 MediaRouter2Manager::updateRoutesOnHandler, 1146 MediaRouter2Manager.this, 1147 routes)); 1148 } 1149 1150 @Override invalidateInstance()1151 public void invalidateInstance() { 1152 // Should never happen since MediaRouter2Manager should only be used with 1153 // MEDIA_CONTENT_CONTROL, which cannot be revoked. 1154 } 1155 } 1156 } 1157