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