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