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