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