1/* 2 * Copyright (C) 2013 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 17package androidx.mediarouter.media; 18 19import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP; 20 21import android.app.ActivityManager; 22import android.content.ComponentName; 23import android.content.ContentResolver; 24import android.content.Context; 25import android.content.Intent; 26import android.content.IntentFilter; 27import android.content.IntentSender; 28import android.content.pm.PackageManager.NameNotFoundException; 29import android.content.res.Resources; 30import android.net.Uri; 31import android.os.Bundle; 32import android.os.Handler; 33import android.os.Looper; 34import android.os.Message; 35import android.support.v4.media.session.MediaSessionCompat; 36import android.text.TextUtils; 37import android.util.Log; 38import android.view.Display; 39 40import androidx.annotation.IntDef; 41import androidx.annotation.NonNull; 42import androidx.annotation.Nullable; 43import androidx.annotation.RestrictTo; 44import androidx.core.app.ActivityManagerCompat; 45import androidx.core.hardware.display.DisplayManagerCompat; 46import androidx.core.util.ObjectsCompat; 47import androidx.core.util.Pair; 48import androidx.media.VolumeProviderCompat; 49import androidx.mediarouter.app.MediaRouteDiscoveryFragment; 50import androidx.mediarouter.media.MediaRouteProvider.ProviderMetadata; 51import androidx.mediarouter.media.MediaRouteProvider.RouteController; 52 53import java.lang.annotation.Retention; 54import java.lang.annotation.RetentionPolicy; 55import java.lang.ref.WeakReference; 56import java.util.ArrayList; 57import java.util.Collections; 58import java.util.HashMap; 59import java.util.HashSet; 60import java.util.Iterator; 61import java.util.List; 62import java.util.Locale; 63import java.util.Map; 64import java.util.Set; 65 66/** 67 * MediaRouter allows applications to control the routing of media channels 68 * and streams from the current device to external speakers and destination devices. 69 * <p> 70 * A MediaRouter instance is retrieved through {@link #getInstance}. Applications 71 * can query the media router about the currently selected route and its capabilities 72 * to determine how to send content to the route's destination. Applications can 73 * also {@link RouteInfo#sendControlRequest send control requests} to the route 74 * to ask the route's destination to perform certain remote control functions 75 * such as playing media. 76 * </p><p> 77 * See also {@link MediaRouteProvider} for information on how an application 78 * can publish new media routes to the media router. 79 * </p><p> 80 * The media router API is not thread-safe; all interactions with it must be 81 * done from the main thread of the process. 82 * </p> 83 */ 84public final class MediaRouter { 85 static final String TAG = "MediaRouter"; 86 static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 87 88 /** 89 * Passed to {@link androidx.mediarouter.media.MediaRouteProvider.RouteController#onUnselect(int)} 90 * and {@link Callback#onRouteUnselected(MediaRouter, RouteInfo, int)} when the reason the route 91 * was unselected is unknown. 92 */ 93 public static final int UNSELECT_REASON_UNKNOWN = 0; 94 /** 95 * Passed to {@link androidx.mediarouter.media.MediaRouteProvider.RouteController#onUnselect(int)} 96 * and {@link Callback#onRouteUnselected(MediaRouter, RouteInfo, int)} when the user pressed 97 * the disconnect button to disconnect and keep playing. 98 * <p> 99 * 100 * @see MediaRouteDescriptor#canDisconnectAndKeepPlaying() 101 */ 102 public static final int UNSELECT_REASON_DISCONNECTED = 1; 103 /** 104 * Passed to {@link androidx.mediarouter.media.MediaRouteProvider.RouteController#onUnselect(int)} 105 * and {@link Callback#onRouteUnselected(MediaRouter, RouteInfo, int)} when the user pressed 106 * the stop casting button. 107 */ 108 public static final int UNSELECT_REASON_STOPPED = 2; 109 /** 110 * Passed to {@link androidx.mediarouter.media.MediaRouteProvider.RouteController#onUnselect(int)} 111 * and {@link Callback#onRouteUnselected(MediaRouter, RouteInfo, int)} when the user selected 112 * a different route. 113 */ 114 public static final int UNSELECT_REASON_ROUTE_CHANGED = 3; 115 116 // Maintains global media router state for the process. 117 // This field is initialized in MediaRouter.getInstance() before any 118 // MediaRouter objects are instantiated so it is guaranteed to be 119 // valid whenever any instance method is invoked. 120 static GlobalMediaRouter sGlobal; 121 122 // Context-bound state of the media router. 123 final Context mContext; 124 final ArrayList<CallbackRecord> mCallbackRecords = new ArrayList<CallbackRecord>(); 125 126 @IntDef(flag = true, 127 value = { 128 CALLBACK_FLAG_PERFORM_ACTIVE_SCAN, 129 CALLBACK_FLAG_REQUEST_DISCOVERY, 130 CALLBACK_FLAG_UNFILTERED_EVENTS 131 } 132 ) 133 @Retention(RetentionPolicy.SOURCE) 134 private @interface CallbackFlags {} 135 136 /** 137 * Flag for {@link #addCallback}: Actively scan for routes while this callback 138 * is registered. 139 * <p> 140 * When this flag is specified, the media router will actively scan for new 141 * routes. Certain routes, such as wifi display routes, may not be discoverable 142 * except when actively scanning. This flag is typically used when the route picker 143 * dialog has been opened by the user to ensure that the route information is 144 * up to date. 145 * </p><p> 146 * Active scanning may consume a significant amount of power and may have intrusive 147 * effects on wireless connectivity. Therefore it is important that active scanning 148 * only be requested when it is actually needed to satisfy a user request to 149 * discover and select a new route. 150 * </p><p> 151 * This flag implies {@link #CALLBACK_FLAG_REQUEST_DISCOVERY} but performing 152 * active scans is much more expensive than a normal discovery request. 153 * </p> 154 * 155 * @see #CALLBACK_FLAG_REQUEST_DISCOVERY 156 */ 157 public static final int CALLBACK_FLAG_PERFORM_ACTIVE_SCAN = 1 << 0; 158 159 /** 160 * Flag for {@link #addCallback}: Do not filter route events. 161 * <p> 162 * When this flag is specified, the callback will be invoked for events that affect any 163 * route even if they do not match the callback's filter. 164 * </p> 165 */ 166 public static final int CALLBACK_FLAG_UNFILTERED_EVENTS = 1 << 1; 167 168 /** 169 * Flag for {@link #addCallback}: Request passive route discovery while this 170 * callback is registered, except on {@link ActivityManager#isLowRamDevice low-RAM devices}. 171 * <p> 172 * When this flag is specified, the media router will try to discover routes. 173 * Although route discovery is intended to be efficient, checking for new routes may 174 * result in some network activity and could slowly drain the battery. Therefore 175 * applications should only specify {@link #CALLBACK_FLAG_REQUEST_DISCOVERY} when 176 * they are running in the foreground and would like to provide the user with the 177 * option of connecting to new routes. 178 * </p><p> 179 * Applications should typically add a callback using this flag in the 180 * {@link android.app.Activity activity's} {@link android.app.Activity#onStart onStart} 181 * method and remove it in the {@link android.app.Activity#onStop onStop} method. 182 * The {@link MediaRouteDiscoveryFragment} fragment may 183 * also be used for this purpose. 184 * </p><p class="note"> 185 * On {@link ActivityManager#isLowRamDevice low-RAM devices} this flag 186 * will be ignored. Refer to 187 * {@link #addCallback(MediaRouteSelector, Callback, int) addCallback} for details. 188 * </p> 189 * 190 * @see MediaRouteDiscoveryFragment 191 */ 192 public static final int CALLBACK_FLAG_REQUEST_DISCOVERY = 1 << 2; 193 194 /** 195 * Flag for {@link #addCallback}: Request passive route discovery while this 196 * callback is registered, even on {@link ActivityManager#isLowRamDevice low-RAM devices}. 197 * <p class="note"> 198 * This flag has a significant performance impact on low-RAM devices 199 * since it may cause many media route providers to be started simultaneously. 200 * It is much better to use {@link #CALLBACK_FLAG_REQUEST_DISCOVERY} instead to avoid 201 * performing passive discovery on these devices altogether. Refer to 202 * {@link #addCallback(MediaRouteSelector, Callback, int) addCallback} for details. 203 * </p> 204 * 205 * @see MediaRouteDiscoveryFragment 206 */ 207 public static final int CALLBACK_FLAG_FORCE_DISCOVERY = 1 << 3; 208 209 /** 210 * Flag for {@link #isRouteAvailable}: Ignore the default route. 211 * <p> 212 * This flag is used to determine whether a matching non-default route is available. 213 * This constraint may be used to decide whether to offer the route chooser dialog 214 * to the user. There is no point offering the chooser if there are no 215 * non-default choices. 216 * </p> 217 */ 218 public static final int AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE = 1 << 0; 219 220 /** 221 * Flag for {@link #isRouteAvailable}: Require an actual route to be matched. 222 * <p> 223 * If this flag is not set, then {@link #isRouteAvailable} will return true 224 * if it is possible to discover a matching route even if discovery is not in 225 * progress or if no matching route has yet been found. This feature is used to 226 * save resources by removing the need to perform passive route discovery on 227 * {@link ActivityManager#isLowRamDevice low-RAM devices}. 228 * </p><p> 229 * If this flag is set, then {@link #isRouteAvailable} will only return true if 230 * a matching route has actually been discovered. 231 * </p> 232 */ 233 public static final int AVAILABILITY_FLAG_REQUIRE_MATCH = 1 << 1; 234 235 private MediaRouter(Context context) { 236 mContext = context; 237 } 238 239 /** 240 * Gets an instance of the media router service associated with the context. 241 * <p> 242 * The application is responsible for holding a strong reference to the returned 243 * {@link MediaRouter} instance, such as by storing the instance in a field of 244 * the {@link android.app.Activity}, to ensure that the media router remains alive 245 * as long as the application is using its features. 246 * </p><p> 247 * In other words, the support library only holds a {@link WeakReference weak reference} 248 * to each media router instance. When there are no remaining strong references to the 249 * media router instance, all of its callbacks will be removed and route discovery 250 * will no longer be performed on its behalf. 251 * </p> 252 * 253 * @return The media router instance for the context. The application must hold 254 * a strong reference to this object as long as it is in use. 255 */ 256 public static MediaRouter getInstance(@NonNull Context context) { 257 if (context == null) { 258 throw new IllegalArgumentException("context must not be null"); 259 } 260 checkCallingThread(); 261 262 if (sGlobal == null) { 263 sGlobal = new GlobalMediaRouter(context.getApplicationContext()); 264 sGlobal.start(); 265 } 266 return sGlobal.getRouter(context); 267 } 268 269 /** 270 * Gets information about the {@link MediaRouter.RouteInfo routes} currently known to 271 * this media router. 272 */ 273 public List<RouteInfo> getRoutes() { 274 checkCallingThread(); 275 return sGlobal.getRoutes(); 276 } 277 278 /** 279 * Gets information about the {@link MediaRouter.ProviderInfo route providers} 280 * currently known to this media router. 281 */ 282 public List<ProviderInfo> getProviders() { 283 checkCallingThread(); 284 return sGlobal.getProviders(); 285 } 286 287 /** 288 * Gets the default route for playing media content on the system. 289 * <p> 290 * The system always provides a default route. 291 * </p> 292 * 293 * @return The default route, which is guaranteed to never be null. 294 */ 295 @NonNull 296 public RouteInfo getDefaultRoute() { 297 checkCallingThread(); 298 return sGlobal.getDefaultRoute(); 299 } 300 301 /** 302 * Gets a bluetooth route for playing media content on the system. 303 * 304 * @return A bluetooth route, if exist, otherwise null. 305 */ 306 public RouteInfo getBluetoothRoute() { 307 checkCallingThread(); 308 return sGlobal.getBluetoothRoute(); 309 } 310 311 /** 312 * Gets the currently selected route. 313 * <p> 314 * The application should examine the route's 315 * {@link RouteInfo#getControlFilters media control intent filters} to assess the 316 * capabilities of the route before attempting to use it. 317 * </p> 318 * 319 * <h3>Example</h3> 320 * <pre> 321 * public boolean playMovie() { 322 * MediaRouter mediaRouter = MediaRouter.getInstance(context); 323 * MediaRouter.RouteInfo route = mediaRouter.getSelectedRoute(); 324 * 325 * // First try using the remote playback interface, if supported. 326 * if (route.supportsControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)) { 327 * // The route supports remote playback. 328 * // Try to send it the Uri of the movie to play. 329 * Intent intent = new Intent(MediaControlIntent.ACTION_PLAY); 330 * intent.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK); 331 * intent.setDataAndType("http://example.com/videos/movie.mp4", "video/mp4"); 332 * if (route.supportsControlRequest(intent)) { 333 * route.sendControlRequest(intent, null); 334 * return true; // sent the request to play the movie 335 * } 336 * } 337 * 338 * // If remote playback was not possible, then play locally. 339 * if (route.supportsControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO)) { 340 * // The route supports live video streaming. 341 * // Prepare to play content locally in a window or in a presentation. 342 * return playMovieInWindow(); 343 * } 344 * 345 * // Neither interface is supported, so we can't play the movie to this route. 346 * return false; 347 * } 348 * </pre> 349 * 350 * @return The selected route, which is guaranteed to never be null. 351 * 352 * @see RouteInfo#getControlFilters 353 * @see RouteInfo#supportsControlCategory 354 * @see RouteInfo#supportsControlRequest 355 */ 356 @NonNull 357 public RouteInfo getSelectedRoute() { 358 checkCallingThread(); 359 return sGlobal.getSelectedRoute(); 360 } 361 362 /** 363 * Returns the selected route if it matches the specified selector, otherwise 364 * selects the default route and returns it. If there is one live audio route 365 * (usually Bluetooth A2DP), it will be selected instead of default route. 366 * 367 * @param selector The selector to match. 368 * @return The previously selected route if it matched the selector, otherwise the 369 * newly selected default route which is guaranteed to never be null. 370 * 371 * @see MediaRouteSelector 372 * @see RouteInfo#matchesSelector 373 */ 374 @NonNull 375 public RouteInfo updateSelectedRoute(@NonNull MediaRouteSelector selector) { 376 if (selector == null) { 377 throw new IllegalArgumentException("selector must not be null"); 378 } 379 checkCallingThread(); 380 381 if (DEBUG) { 382 Log.d(TAG, "updateSelectedRoute: " + selector); 383 } 384 RouteInfo route = sGlobal.getSelectedRoute(); 385 if (!route.isDefaultOrBluetooth() && !route.matchesSelector(selector)) { 386 route = sGlobal.chooseFallbackRoute(); 387 sGlobal.selectRoute(route); 388 } 389 return route; 390 } 391 392 /** 393 * Selects the specified route. 394 * 395 * @param route The route to select. 396 */ 397 public void selectRoute(@NonNull RouteInfo route) { 398 if (route == null) { 399 throw new IllegalArgumentException("route must not be null"); 400 } 401 checkCallingThread(); 402 403 if (DEBUG) { 404 Log.d(TAG, "selectRoute: " + route); 405 } 406 sGlobal.selectRoute(route); 407 } 408 409 /** 410 * Unselects the current round and selects the default route instead. 411 * <p> 412 * The reason given must be one of: 413 * <ul> 414 * <li>{@link MediaRouter#UNSELECT_REASON_UNKNOWN}</li> 415 * <li>{@link MediaRouter#UNSELECT_REASON_DISCONNECTED}</li> 416 * <li>{@link MediaRouter#UNSELECT_REASON_STOPPED}</li> 417 * <li>{@link MediaRouter#UNSELECT_REASON_ROUTE_CHANGED}</li> 418 * </ul> 419 * 420 * @param reason The reason for disconnecting the current route. 421 */ 422 public void unselect(int reason) { 423 if (reason < MediaRouter.UNSELECT_REASON_UNKNOWN || 424 reason > MediaRouter.UNSELECT_REASON_ROUTE_CHANGED) { 425 throw new IllegalArgumentException("Unsupported reason to unselect route"); 426 } 427 checkCallingThread(); 428 429 // Choose the fallback route if it's not already selected. 430 // Otherwise, select the default route. 431 RouteInfo fallbackRoute = sGlobal.chooseFallbackRoute(); 432 if (sGlobal.getSelectedRoute() != fallbackRoute) { 433 sGlobal.selectRoute(fallbackRoute, reason); 434 } else { 435 sGlobal.selectRoute(sGlobal.getDefaultRoute(), reason); 436 } 437 } 438 439 /** 440 * Returns true if there is a route that matches the specified selector. 441 * <p> 442 * This method returns true if there are any available routes that match the 443 * selector regardless of whether they are enabled or disabled. If the 444 * {@link #AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE} flag is specified, then 445 * the method will only consider non-default routes. 446 * </p> 447 * <p class="note"> 448 * On {@link ActivityManager#isLowRamDevice low-RAM devices} this method 449 * will return true if it is possible to discover a matching route even if 450 * discovery is not in progress or if no matching route has yet been found. 451 * Use {@link #AVAILABILITY_FLAG_REQUIRE_MATCH} to require an actual match. 452 * </p> 453 * 454 * @param selector The selector to match. 455 * @param flags Flags to control the determination of whether a route may be 456 * available. May be zero or some combination of 457 * {@link #AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE} and 458 * {@link #AVAILABILITY_FLAG_REQUIRE_MATCH}. 459 * @return True if a matching route may be available. 460 */ 461 public boolean isRouteAvailable(@NonNull MediaRouteSelector selector, int flags) { 462 if (selector == null) { 463 throw new IllegalArgumentException("selector must not be null"); 464 } 465 checkCallingThread(); 466 467 return sGlobal.isRouteAvailable(selector, flags); 468 } 469 470 /** 471 * Registers a callback to discover routes that match the selector and to receive 472 * events when they change. 473 * <p> 474 * This is a convenience method that has the same effect as calling 475 * {@link #addCallback(MediaRouteSelector, Callback, int)} without flags. 476 * </p> 477 * 478 * @param selector A route selector that indicates the kinds of routes that the 479 * callback would like to discover. 480 * @param callback The callback to add. 481 * @see #removeCallback 482 */ 483 public void addCallback(MediaRouteSelector selector, Callback callback) { 484 addCallback(selector, callback, 0); 485 } 486 487 /** 488 * Registers a callback to discover routes that match the selector and to receive 489 * events when they change. 490 * <p> 491 * The selector describes the kinds of routes that the application wants to 492 * discover. For example, if the application wants to use 493 * live audio routes then it should include the 494 * {@link MediaControlIntent#CATEGORY_LIVE_AUDIO live audio media control intent category} 495 * in its selector when it adds a callback to the media router. 496 * The selector may include any number of categories. 497 * </p><p> 498 * If the callback has already been registered, then the selector is added to 499 * the set of selectors being monitored by the callback. 500 * </p><p> 501 * By default, the callback will only be invoked for events that affect routes 502 * that match the specified selector. Event filtering may be disabled by specifying 503 * the {@link #CALLBACK_FLAG_UNFILTERED_EVENTS} flag when the callback is registered. 504 * </p><p> 505 * Applications should use the {@link #isRouteAvailable} method to determine 506 * whether is it possible to discover a route with the desired capabilities 507 * and therefore whether the media route button should be shown to the user. 508 * </p><p> 509 * The {@link #CALLBACK_FLAG_REQUEST_DISCOVERY} flag should be used while the application 510 * is in the foreground to request that passive discovery be performed if there are 511 * sufficient resources to allow continuous passive discovery. 512 * On {@link ActivityManager#isLowRamDevice low-RAM devices} this flag will be 513 * ignored to conserve resources. 514 * </p><p> 515 * The {@link #CALLBACK_FLAG_FORCE_DISCOVERY} flag should be used when 516 * passive discovery absolutely must be performed, even on low-RAM devices. 517 * This flag has a significant performance impact on low-RAM devices 518 * since it may cause many media route providers to be started simultaneously. 519 * It is much better to use {@link #CALLBACK_FLAG_REQUEST_DISCOVERY} instead to avoid 520 * performing passive discovery on these devices altogether. 521 * </p><p> 522 * The {@link #CALLBACK_FLAG_PERFORM_ACTIVE_SCAN} flag should be used when the 523 * media route chooser dialog is showing to confirm the presence of available 524 * routes that the user may connect to. This flag may use substantially more 525 * power. 526 * </p> 527 * 528 * <h3>Example</h3> 529 * <pre> 530 * public class MyActivity extends Activity { 531 * private MediaRouter mRouter; 532 * private MediaRouter.Callback mCallback; 533 * private MediaRouteSelector mSelector; 534 * 535 * protected void onCreate(Bundle savedInstanceState) { 536 * super.onCreate(savedInstanceState); 537 * 538 * mRouter = Mediarouter.getInstance(this); 539 * mCallback = new MyCallback(); 540 * mSelector = new MediaRouteSelector.Builder() 541 * .addControlCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO) 542 * .addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK) 543 * .build(); 544 * } 545 * 546 * // Add the callback on start to tell the media router what kinds of routes 547 * // the application is interested in so that it can try to discover suitable ones. 548 * public void onStart() { 549 * super.onStart(); 550 * 551 * mediaRouter.addCallback(mSelector, mCallback, 552 * MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY); 553 * 554 * MediaRouter.RouteInfo route = mediaRouter.updateSelectedRoute(mSelector); 555 * // do something with the route... 556 * } 557 * 558 * // Remove the selector on stop to tell the media router that it no longer 559 * // needs to invest effort trying to discover routes of these kinds for now. 560 * public void onStop() { 561 * super.onStop(); 562 * 563 * mediaRouter.removeCallback(mCallback); 564 * } 565 * 566 * private final class MyCallback extends MediaRouter.Callback { 567 * // Implement callback methods as needed. 568 * } 569 * } 570 * </pre> 571 * 572 * @param selector A route selector that indicates the kinds of routes that the 573 * callback would like to discover. 574 * @param callback The callback to add. 575 * @param flags Flags to control the behavior of the callback. 576 * May be zero or a combination of {@link #CALLBACK_FLAG_PERFORM_ACTIVE_SCAN} and 577 * {@link #CALLBACK_FLAG_UNFILTERED_EVENTS}. 578 * @see #removeCallback 579 */ 580 public void addCallback(@NonNull MediaRouteSelector selector, @NonNull Callback callback, 581 @CallbackFlags int flags) { 582 if (selector == null) { 583 throw new IllegalArgumentException("selector must not be null"); 584 } 585 if (callback == null) { 586 throw new IllegalArgumentException("callback must not be null"); 587 } 588 checkCallingThread(); 589 590 if (DEBUG) { 591 Log.d(TAG, "addCallback: selector=" + selector 592 + ", callback=" + callback + ", flags=" + Integer.toHexString(flags)); 593 } 594 595 CallbackRecord record; 596 int index = findCallbackRecord(callback); 597 if (index < 0) { 598 record = new CallbackRecord(this, callback); 599 mCallbackRecords.add(record); 600 } else { 601 record = mCallbackRecords.get(index); 602 } 603 boolean updateNeeded = false; 604 if ((flags & ~record.mFlags) != 0) { 605 record.mFlags |= flags; 606 updateNeeded = true; 607 } 608 if (!record.mSelector.contains(selector)) { 609 record.mSelector = new MediaRouteSelector.Builder(record.mSelector) 610 .addSelector(selector) 611 .build(); 612 updateNeeded = true; 613 } 614 if (updateNeeded) { 615 sGlobal.updateDiscoveryRequest(); 616 } 617 } 618 619 /** 620 * Removes the specified callback. It will no longer receive events about 621 * changes to media routes. 622 * 623 * @param callback The callback to remove. 624 * @see #addCallback 625 */ 626 public void removeCallback(@NonNull Callback callback) { 627 if (callback == null) { 628 throw new IllegalArgumentException("callback must not be null"); 629 } 630 checkCallingThread(); 631 632 if (DEBUG) { 633 Log.d(TAG, "removeCallback: callback=" + callback); 634 } 635 636 int index = findCallbackRecord(callback); 637 if (index >= 0) { 638 mCallbackRecords.remove(index); 639 sGlobal.updateDiscoveryRequest(); 640 } 641 } 642 643 private int findCallbackRecord(Callback callback) { 644 final int count = mCallbackRecords.size(); 645 for (int i = 0; i < count; i++) { 646 if (mCallbackRecords.get(i).mCallback == callback) { 647 return i; 648 } 649 } 650 return -1; 651 } 652 653 /** 654 * Registers a media route provider within this application process. 655 * <p> 656 * The provider will be added to the list of providers that all {@link MediaRouter} 657 * instances within this process can use to discover routes. 658 * </p> 659 * 660 * @param providerInstance The media route provider instance to add. 661 * 662 * @see MediaRouteProvider 663 * @see #removeCallback 664 */ 665 public void addProvider(@NonNull MediaRouteProvider providerInstance) { 666 if (providerInstance == null) { 667 throw new IllegalArgumentException("providerInstance must not be null"); 668 } 669 checkCallingThread(); 670 671 if (DEBUG) { 672 Log.d(TAG, "addProvider: " + providerInstance); 673 } 674 sGlobal.addProvider(providerInstance); 675 } 676 677 /** 678 * Unregisters a media route provider within this application process. 679 * <p> 680 * The provider will be removed from the list of providers that all {@link MediaRouter} 681 * instances within this process can use to discover routes. 682 * </p> 683 * 684 * @param providerInstance The media route provider instance to remove. 685 * 686 * @see MediaRouteProvider 687 * @see #addCallback 688 */ 689 public void removeProvider(@NonNull MediaRouteProvider providerInstance) { 690 if (providerInstance == null) { 691 throw new IllegalArgumentException("providerInstance must not be null"); 692 } 693 checkCallingThread(); 694 695 if (DEBUG) { 696 Log.d(TAG, "removeProvider: " + providerInstance); 697 } 698 sGlobal.removeProvider(providerInstance); 699 } 700 701 /** 702 * Adds a remote control client to enable remote control of the volume 703 * of the selected route. 704 * <p> 705 * The remote control client must have previously been registered with 706 * the audio manager using the {@link android.media.AudioManager#registerRemoteControlClient 707 * AudioManager.registerRemoteControlClient} method. 708 * </p> 709 * 710 * @param remoteControlClient The {@link android.media.RemoteControlClient} to register. 711 */ 712 public void addRemoteControlClient(@NonNull Object remoteControlClient) { 713 if (remoteControlClient == null) { 714 throw new IllegalArgumentException("remoteControlClient must not be null"); 715 } 716 checkCallingThread(); 717 718 if (DEBUG) { 719 Log.d(TAG, "addRemoteControlClient: " + remoteControlClient); 720 } 721 sGlobal.addRemoteControlClient(remoteControlClient); 722 } 723 724 /** 725 * Removes a remote control client. 726 * 727 * @param remoteControlClient The {@link android.media.RemoteControlClient} 728 * to unregister. 729 */ 730 public void removeRemoteControlClient(@NonNull Object remoteControlClient) { 731 if (remoteControlClient == null) { 732 throw new IllegalArgumentException("remoteControlClient must not be null"); 733 } 734 735 if (DEBUG) { 736 Log.d(TAG, "removeRemoteControlClient: " + remoteControlClient); 737 } 738 sGlobal.removeRemoteControlClient(remoteControlClient); 739 } 740 741 /** 742 * Sets the media session to enable remote control of the volume of the 743 * selected route. This should be used instead of 744 * {@link #addRemoteControlClient} when using media sessions. Set the 745 * session to null to clear it. 746 * 747 * @param mediaSession The {@link android.media.session.MediaSession} to 748 * use. 749 */ 750 public void setMediaSession(Object mediaSession) { 751 if (DEBUG) { 752 Log.d(TAG, "addMediaSession: " + mediaSession); 753 } 754 sGlobal.setMediaSession(mediaSession); 755 } 756 757 /** 758 * Sets a compat media session to enable remote control of the volume of the 759 * selected route. This should be used instead of 760 * {@link #addRemoteControlClient} when using {@link MediaSessionCompat}. 761 * Set the session to null to clear it. 762 * 763 * @param mediaSession 764 */ 765 public void setMediaSessionCompat(MediaSessionCompat mediaSession) { 766 if (DEBUG) { 767 Log.d(TAG, "addMediaSessionCompat: " + mediaSession); 768 } 769 sGlobal.setMediaSessionCompat(mediaSession); 770 } 771 772 public MediaSessionCompat.Token getMediaSessionToken() { 773 return sGlobal.getMediaSessionToken(); 774 } 775 776 /** 777 * Ensures that calls into the media router are on the correct thread. 778 * It pays to be a little paranoid when global state invariants are at risk. 779 */ 780 static void checkCallingThread() { 781 if (Looper.myLooper() != Looper.getMainLooper()) { 782 throw new IllegalStateException("The media router service must only be " 783 + "accessed on the application's main thread."); 784 } 785 } 786 787 /** 788 * Provides information about a media route. 789 * <p> 790 * Each media route has a list of {@link MediaControlIntent media control} 791 * {@link #getControlFilters intent filters} that describe the capabilities of the 792 * route and the manner in which it is used and controlled. 793 * </p> 794 */ 795 public static class RouteInfo { 796 private final ProviderInfo mProvider; 797 private final String mDescriptorId; 798 private final String mUniqueId; 799 private String mName; 800 private String mDescription; 801 private Uri mIconUri; 802 private boolean mEnabled; 803 private boolean mConnecting; 804 private int mConnectionState; 805 private boolean mCanDisconnect; 806 private final ArrayList<IntentFilter> mControlFilters = new ArrayList<>(); 807 private int mPlaybackType; 808 private int mPlaybackStream; 809 private int mDeviceType; 810 private int mVolumeHandling; 811 private int mVolume; 812 private int mVolumeMax; 813 private Display mPresentationDisplay; 814 private int mPresentationDisplayId = PRESENTATION_DISPLAY_ID_NONE; 815 private Bundle mExtras; 816 private IntentSender mSettingsIntent; 817 MediaRouteDescriptor mDescriptor; 818 819 @IntDef({CONNECTION_STATE_DISCONNECTED, CONNECTION_STATE_CONNECTING, 820 CONNECTION_STATE_CONNECTED}) 821 @Retention(RetentionPolicy.SOURCE) 822 private @interface ConnectionState {} 823 824 /** 825 * The default connection state indicating the route is disconnected. 826 * 827 * @see #getConnectionState 828 */ 829 public static final int CONNECTION_STATE_DISCONNECTED = 0; 830 831 /** 832 * A connection state indicating the route is in the process of connecting and is not yet 833 * ready for use. 834 * 835 * @see #getConnectionState 836 */ 837 public static final int CONNECTION_STATE_CONNECTING = 1; 838 839 /** 840 * A connection state indicating the route is connected. 841 * 842 * @see #getConnectionState 843 */ 844 public static final int CONNECTION_STATE_CONNECTED = 2; 845 846 @IntDef({PLAYBACK_TYPE_LOCAL,PLAYBACK_TYPE_REMOTE}) 847 @Retention(RetentionPolicy.SOURCE) 848 private @interface PlaybackType {} 849 850 /** 851 * The default playback type, "local", indicating the presentation of the media 852 * is happening on the same device (e.g. a phone, a tablet) as where it is 853 * controlled from. 854 * 855 * @see #getPlaybackType 856 */ 857 public static final int PLAYBACK_TYPE_LOCAL = 0; 858 859 /** 860 * A playback type indicating the presentation of the media is happening on 861 * a different device (i.e. the remote device) than where it is controlled from. 862 * 863 * @see #getPlaybackType 864 */ 865 public static final int PLAYBACK_TYPE_REMOTE = 1; 866 867 @IntDef({DEVICE_TYPE_UNKNOWN, DEVICE_TYPE_TV, DEVICE_TYPE_SPEAKER, DEVICE_TYPE_BLUETOOTH}) 868 @Retention(RetentionPolicy.SOURCE) 869 private @interface DeviceType {} 870 871 /** 872 * The default receiver device type of the route indicating the type is unknown. 873 * 874 * @see #getDeviceType 875 * @hide 876 */ 877 @RestrictTo(LIBRARY_GROUP) 878 public static final int DEVICE_TYPE_UNKNOWN = 0; 879 880 /** 881 * A receiver device type of the route indicating the presentation of the media is happening 882 * on a TV. 883 * 884 * @see #getDeviceType 885 */ 886 public static final int DEVICE_TYPE_TV = 1; 887 888 /** 889 * A receiver device type of the route indicating the presentation of the media is happening 890 * on a speaker. 891 * 892 * @see #getDeviceType 893 */ 894 public static final int DEVICE_TYPE_SPEAKER = 2; 895 896 /** 897 * A receiver device type of the route indicating the presentation of the media is happening 898 * on a bluetooth device such as a bluetooth speaker. 899 * 900 * @see #getDeviceType 901 * @hide 902 */ 903 @RestrictTo(LIBRARY_GROUP) 904 public static final int DEVICE_TYPE_BLUETOOTH = 3; 905 906 @IntDef({PLAYBACK_VOLUME_FIXED,PLAYBACK_VOLUME_VARIABLE}) 907 @Retention(RetentionPolicy.SOURCE) 908 private @interface PlaybackVolume {} 909 910 /** 911 * Playback information indicating the playback volume is fixed, i.e. it cannot be 912 * controlled from this object. An example of fixed playback volume is a remote player, 913 * playing over HDMI where the user prefers to control the volume on the HDMI sink, rather 914 * than attenuate at the source. 915 * 916 * @see #getVolumeHandling 917 */ 918 public static final int PLAYBACK_VOLUME_FIXED = 0; 919 920 /** 921 * Playback information indicating the playback volume is variable and can be controlled 922 * from this object. 923 * 924 * @see #getVolumeHandling 925 */ 926 public static final int PLAYBACK_VOLUME_VARIABLE = 1; 927 928 /** 929 * The default presentation display id indicating no presentation display is associated 930 * with the route. 931 * @hide 932 */ 933 @RestrictTo(LIBRARY_GROUP) 934 public static final int PRESENTATION_DISPLAY_ID_NONE = -1; 935 936 static final int CHANGE_GENERAL = 1 << 0; 937 static final int CHANGE_VOLUME = 1 << 1; 938 static final int CHANGE_PRESENTATION_DISPLAY = 1 << 2; 939 940 // Should match to SystemMediaRouteProvider.PACKAGE_NAME. 941 static final String SYSTEM_MEDIA_ROUTE_PROVIDER_PACKAGE_NAME = "android"; 942 943 RouteInfo(ProviderInfo provider, String descriptorId, String uniqueId) { 944 mProvider = provider; 945 mDescriptorId = descriptorId; 946 mUniqueId = uniqueId; 947 } 948 949 /** 950 * Gets information about the provider of this media route. 951 */ 952 public ProviderInfo getProvider() { 953 return mProvider; 954 } 955 956 /** 957 * Gets the unique id of the route. 958 * <p> 959 * The route unique id functions as a stable identifier by which the route is known. 960 * For example, an application can use this id as a token to remember the 961 * selected route across restarts or to communicate its identity to a service. 962 * </p> 963 * 964 * @return The unique id of the route, never null. 965 */ 966 @NonNull 967 public String getId() { 968 return mUniqueId; 969 } 970 971 /** 972 * Gets the user-visible name of the route. 973 * <p> 974 * The route name identifies the destination represented by the route. 975 * It may be a user-supplied name, an alias, or device serial number. 976 * </p> 977 * 978 * @return The user-visible name of a media route. This is the string presented 979 * to users who may select this as the active route. 980 */ 981 public String getName() { 982 return mName; 983 } 984 985 /** 986 * Gets the user-visible description of the route. 987 * <p> 988 * The route description describes the kind of destination represented by the route. 989 * It may be a user-supplied string, a model number or brand of device. 990 * </p> 991 * 992 * @return The description of the route, or null if none. 993 */ 994 @Nullable 995 public String getDescription() { 996 return mDescription; 997 } 998 999 /** 1000 * Gets the URI of the icon representing this route. 1001 * <p> 1002 * This icon will be used in picker UIs if available. 1003 * </p> 1004 * 1005 * @return The URI of the icon representing this route, or null if none. 1006 */ 1007 public Uri getIconUri() { 1008 return mIconUri; 1009 } 1010 1011 /** 1012 * Returns true if this route is enabled and may be selected. 1013 * 1014 * @return True if this route is enabled. 1015 */ 1016 public boolean isEnabled() { 1017 return mEnabled; 1018 } 1019 1020 /** 1021 * Returns true if the route is in the process of connecting and is not 1022 * yet ready for use. 1023 * 1024 * @return True if this route is in the process of connecting. 1025 */ 1026 public boolean isConnecting() { 1027 return mConnecting; 1028 } 1029 1030 /** 1031 * Gets the connection state of the route. 1032 * 1033 * @return The connection state of this route: {@link #CONNECTION_STATE_DISCONNECTED}, 1034 * {@link #CONNECTION_STATE_CONNECTING}, or {@link #CONNECTION_STATE_CONNECTED}. 1035 */ 1036 @ConnectionState 1037 public int getConnectionState() { 1038 return mConnectionState; 1039 } 1040 1041 /** 1042 * Returns true if this route is currently selected. 1043 * 1044 * @return True if this route is currently selected. 1045 * 1046 * @see MediaRouter#getSelectedRoute 1047 */ 1048 public boolean isSelected() { 1049 checkCallingThread(); 1050 return sGlobal.getSelectedRoute() == this; 1051 } 1052 1053 /** 1054 * Returns true if this route is the default route. 1055 * 1056 * @return True if this route is the default route. 1057 * 1058 * @see MediaRouter#getDefaultRoute 1059 */ 1060 public boolean isDefault() { 1061 checkCallingThread(); 1062 return sGlobal.getDefaultRoute() == this; 1063 } 1064 1065 /** 1066 * Returns true if this route is a bluetooth route. 1067 * 1068 * @return True if this route is a bluetooth route. 1069 * 1070 * @see MediaRouter#getBluetoothRoute 1071 */ 1072 public boolean isBluetooth() { 1073 checkCallingThread(); 1074 return sGlobal.getBluetoothRoute() == this; 1075 } 1076 1077 /** 1078 * Returns true if this route is the default route and the device speaker. 1079 * 1080 * @return True if this route is the default route and the device speaker. 1081 */ 1082 public boolean isDeviceSpeaker() { 1083 int defaultAudioRouteNameResourceId = Resources.getSystem().getIdentifier( 1084 "default_audio_route_name", "string", "android"); 1085 return isDefault() 1086 && Resources.getSystem().getText(defaultAudioRouteNameResourceId).equals(mName); 1087 } 1088 1089 /** 1090 * Gets a list of {@link MediaControlIntent media control intent} filters that 1091 * describe the capabilities of this route and the media control actions that 1092 * it supports. 1093 * 1094 * @return A list of intent filters that specifies the media control intents that 1095 * this route supports. 1096 * 1097 * @see MediaControlIntent 1098 * @see #supportsControlCategory 1099 * @see #supportsControlRequest 1100 */ 1101 public List<IntentFilter> getControlFilters() { 1102 return mControlFilters; 1103 } 1104 1105 /** 1106 * Returns true if the route supports at least one of the capabilities 1107 * described by a media route selector. 1108 * 1109 * @param selector The selector that specifies the capabilities to check. 1110 * @return True if the route supports at least one of the capabilities 1111 * described in the media route selector. 1112 */ 1113 public boolean matchesSelector(@NonNull MediaRouteSelector selector) { 1114 if (selector == null) { 1115 throw new IllegalArgumentException("selector must not be null"); 1116 } 1117 checkCallingThread(); 1118 return selector.matchesControlFilters(mControlFilters); 1119 } 1120 1121 /** 1122 * Returns true if the route supports the specified 1123 * {@link MediaControlIntent media control} category. 1124 * <p> 1125 * Media control categories describe the capabilities of this route 1126 * such as whether it supports live audio streaming or remote playback. 1127 * </p> 1128 * 1129 * @param category A {@link MediaControlIntent media control} category 1130 * such as {@link MediaControlIntent#CATEGORY_LIVE_AUDIO}, 1131 * {@link MediaControlIntent#CATEGORY_LIVE_VIDEO}, 1132 * {@link MediaControlIntent#CATEGORY_REMOTE_PLAYBACK}, or a provider-defined 1133 * media control category. 1134 * @return True if the route supports the specified intent category. 1135 * 1136 * @see MediaControlIntent 1137 * @see #getControlFilters 1138 */ 1139 public boolean supportsControlCategory(@NonNull String category) { 1140 if (category == null) { 1141 throw new IllegalArgumentException("category must not be null"); 1142 } 1143 checkCallingThread(); 1144 1145 int count = mControlFilters.size(); 1146 for (int i = 0; i < count; i++) { 1147 if (mControlFilters.get(i).hasCategory(category)) { 1148 return true; 1149 } 1150 } 1151 return false; 1152 } 1153 1154 /** 1155 * Returns true if the route supports the specified 1156 * {@link MediaControlIntent media control} category and action. 1157 * <p> 1158 * Media control actions describe specific requests that an application 1159 * can ask a route to perform. 1160 * </p> 1161 * 1162 * @param category A {@link MediaControlIntent media control} category 1163 * such as {@link MediaControlIntent#CATEGORY_LIVE_AUDIO}, 1164 * {@link MediaControlIntent#CATEGORY_LIVE_VIDEO}, 1165 * {@link MediaControlIntent#CATEGORY_REMOTE_PLAYBACK}, or a provider-defined 1166 * media control category. 1167 * @param action A {@link MediaControlIntent media control} action 1168 * such as {@link MediaControlIntent#ACTION_PLAY}. 1169 * @return True if the route supports the specified intent action. 1170 * 1171 * @see MediaControlIntent 1172 * @see #getControlFilters 1173 */ 1174 public boolean supportsControlAction(@NonNull String category, @NonNull String action) { 1175 if (category == null) { 1176 throw new IllegalArgumentException("category must not be null"); 1177 } 1178 if (action == null) { 1179 throw new IllegalArgumentException("action must not be null"); 1180 } 1181 checkCallingThread(); 1182 1183 int count = mControlFilters.size(); 1184 for (int i = 0; i < count; i++) { 1185 IntentFilter filter = mControlFilters.get(i); 1186 if (filter.hasCategory(category) && filter.hasAction(action)) { 1187 return true; 1188 } 1189 } 1190 return false; 1191 } 1192 1193 /** 1194 * Returns true if the route supports the specified 1195 * {@link MediaControlIntent media control} request. 1196 * <p> 1197 * Media control requests are used to request the route to perform 1198 * actions such as starting remote playback of a media item. 1199 * </p> 1200 * 1201 * @param intent A {@link MediaControlIntent media control intent}. 1202 * @return True if the route can handle the specified intent. 1203 * 1204 * @see MediaControlIntent 1205 * @see #getControlFilters 1206 */ 1207 public boolean supportsControlRequest(@NonNull Intent intent) { 1208 if (intent == null) { 1209 throw new IllegalArgumentException("intent must not be null"); 1210 } 1211 checkCallingThread(); 1212 1213 ContentResolver contentResolver = sGlobal.getContentResolver(); 1214 int count = mControlFilters.size(); 1215 for (int i = 0; i < count; i++) { 1216 if (mControlFilters.get(i).match(contentResolver, intent, true, TAG) >= 0) { 1217 return true; 1218 } 1219 } 1220 return false; 1221 } 1222 1223 /** 1224 * Sends a {@link MediaControlIntent media control} request to be performed 1225 * asynchronously by the route's destination. 1226 * <p> 1227 * Media control requests are used to request the route to perform 1228 * actions such as starting remote playback of a media item. 1229 * </p><p> 1230 * This function may only be called on a selected route. Control requests 1231 * sent to unselected routes will fail. 1232 * </p> 1233 * 1234 * @param intent A {@link MediaControlIntent media control intent}. 1235 * @param callback A {@link ControlRequestCallback} to invoke with the result 1236 * of the request, or null if no result is required. 1237 * 1238 * @see MediaControlIntent 1239 */ 1240 public void sendControlRequest(@NonNull Intent intent, 1241 @Nullable ControlRequestCallback callback) { 1242 if (intent == null) { 1243 throw new IllegalArgumentException("intent must not be null"); 1244 } 1245 checkCallingThread(); 1246 1247 sGlobal.sendControlRequest(this, intent, callback); 1248 } 1249 1250 /** 1251 * Gets the type of playback associated with this route. 1252 * 1253 * @return The type of playback associated with this route: {@link #PLAYBACK_TYPE_LOCAL} 1254 * or {@link #PLAYBACK_TYPE_REMOTE}. 1255 */ 1256 @PlaybackType 1257 public int getPlaybackType() { 1258 return mPlaybackType; 1259 } 1260 1261 /** 1262 * Gets the audio stream over which the playback associated with this route is performed. 1263 * 1264 * @return The stream over which the playback associated with this route is performed. 1265 */ 1266 public int getPlaybackStream() { 1267 return mPlaybackStream; 1268 } 1269 1270 /** 1271 * Gets the type of the receiver device associated with this route. 1272 * 1273 * @return The type of the receiver device associated with this route: 1274 * {@link #DEVICE_TYPE_TV} or {@link #DEVICE_TYPE_SPEAKER}. 1275 */ 1276 public int getDeviceType() { 1277 return mDeviceType; 1278 } 1279 1280 1281 /** 1282 * @hide 1283 */ 1284 @RestrictTo(LIBRARY_GROUP) 1285 public boolean isDefaultOrBluetooth() { 1286 if (isDefault() || mDeviceType == DEVICE_TYPE_BLUETOOTH) { 1287 return true; 1288 } 1289 // This is a workaround for platform version 23 or below where the system route 1290 // provider doesn't specify device type for bluetooth media routes. 1291 return isSystemMediaRouteProvider(this) 1292 && supportsControlCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO) 1293 && !supportsControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO); 1294 } 1295 1296 /** 1297 * Returns {@code true} if the route is selectable. 1298 */ 1299 boolean isSelectable() { 1300 // This tests whether the route is still valid and enabled. 1301 // The route descriptor field is set to null when the route is removed. 1302 return mDescriptor != null && mEnabled; 1303 } 1304 1305 private static boolean isSystemMediaRouteProvider(MediaRouter.RouteInfo route) { 1306 return TextUtils.equals(route.getProviderInstance().getMetadata().getPackageName(), 1307 SYSTEM_MEDIA_ROUTE_PROVIDER_PACKAGE_NAME); 1308 } 1309 1310 /** 1311 * Gets information about how volume is handled on the route. 1312 * 1313 * @return How volume is handled on the route: {@link #PLAYBACK_VOLUME_FIXED} 1314 * or {@link #PLAYBACK_VOLUME_VARIABLE}. 1315 */ 1316 @PlaybackVolume 1317 public int getVolumeHandling() { 1318 return mVolumeHandling; 1319 } 1320 1321 /** 1322 * Gets the current volume for this route. Depending on the route, this may only 1323 * be valid if the route is currently selected. 1324 * 1325 * @return The volume at which the playback associated with this route is performed. 1326 */ 1327 public int getVolume() { 1328 return mVolume; 1329 } 1330 1331 /** 1332 * Gets the maximum volume at which the playback associated with this route is performed. 1333 * 1334 * @return The maximum volume at which the playback associated with 1335 * this route is performed. 1336 */ 1337 public int getVolumeMax() { 1338 return mVolumeMax; 1339 } 1340 1341 /** 1342 * Gets whether this route supports disconnecting without interrupting 1343 * playback. 1344 * 1345 * @return True if this route can disconnect without stopping playback, 1346 * false otherwise. 1347 */ 1348 public boolean canDisconnect() { 1349 return mCanDisconnect; 1350 } 1351 1352 /** 1353 * Requests a volume change for this route asynchronously. 1354 * <p> 1355 * This function may only be called on a selected route. It will have 1356 * no effect if the route is currently unselected. 1357 * </p> 1358 * 1359 * @param volume The new volume value between 0 and {@link #getVolumeMax}. 1360 */ 1361 public void requestSetVolume(int volume) { 1362 checkCallingThread(); 1363 sGlobal.requestSetVolume(this, Math.min(mVolumeMax, Math.max(0, volume))); 1364 } 1365 1366 /** 1367 * Requests an incremental volume update for this route asynchronously. 1368 * <p> 1369 * This function may only be called on a selected route. It will have 1370 * no effect if the route is currently unselected. 1371 * </p> 1372 * 1373 * @param delta The delta to add to the current volume. 1374 */ 1375 public void requestUpdateVolume(int delta) { 1376 checkCallingThread(); 1377 if (delta != 0) { 1378 sGlobal.requestUpdateVolume(this, delta); 1379 } 1380 } 1381 1382 /** 1383 * Gets the {@link Display} that should be used by the application to show 1384 * a {@link android.app.Presentation} on an external display when this route is selected. 1385 * Depending on the route, this may only be valid if the route is currently 1386 * selected. 1387 * <p> 1388 * The preferred presentation display may change independently of the route 1389 * being selected or unselected. For example, the presentation display 1390 * of the default system route may change when an external HDMI display is connected 1391 * or disconnected even though the route itself has not changed. 1392 * </p><p> 1393 * This method may return null if there is no external display associated with 1394 * the route or if the display is not ready to show UI yet. 1395 * </p><p> 1396 * The application should listen for changes to the presentation display 1397 * using the {@link Callback#onRoutePresentationDisplayChanged} callback and 1398 * show or dismiss its {@link android.app.Presentation} accordingly when the display 1399 * becomes available or is removed. 1400 * </p><p> 1401 * This method only makes sense for 1402 * {@link MediaControlIntent#CATEGORY_LIVE_VIDEO live video} routes. 1403 * </p> 1404 * 1405 * @return The preferred presentation display to use when this route is 1406 * selected or null if none. 1407 * 1408 * @see MediaControlIntent#CATEGORY_LIVE_VIDEO 1409 * @see android.app.Presentation 1410 */ 1411 @Nullable 1412 public Display getPresentationDisplay() { 1413 checkCallingThread(); 1414 if (mPresentationDisplayId >= 0 && mPresentationDisplay == null) { 1415 mPresentationDisplay = sGlobal.getDisplay(mPresentationDisplayId); 1416 } 1417 return mPresentationDisplay; 1418 } 1419 1420 /** 1421 * Gets the route's presentation display id, or -1 if none. 1422 * @hide 1423 */ 1424 @RestrictTo(LIBRARY_GROUP) 1425 public int getPresentationDisplayId() { 1426 return mPresentationDisplayId; 1427 } 1428 1429 /** 1430 * Gets a collection of extra properties about this route that were supplied 1431 * by its media route provider, or null if none. 1432 */ 1433 @Nullable 1434 public Bundle getExtras() { 1435 return mExtras; 1436 } 1437 1438 /** 1439 * Gets an intent sender for launching a settings activity for this 1440 * route. 1441 */ 1442 @Nullable 1443 public IntentSender getSettingsIntent() { 1444 return mSettingsIntent; 1445 } 1446 1447 /** 1448 * Selects this media route. 1449 */ 1450 public void select() { 1451 checkCallingThread(); 1452 sGlobal.selectRoute(this); 1453 } 1454 1455 @Override 1456 public String toString() { 1457 return "MediaRouter.RouteInfo{ uniqueId=" + mUniqueId 1458 + ", name=" + mName 1459 + ", description=" + mDescription 1460 + ", iconUri=" + mIconUri 1461 + ", enabled=" + mEnabled 1462 + ", connecting=" + mConnecting 1463 + ", connectionState=" + mConnectionState 1464 + ", canDisconnect=" + mCanDisconnect 1465 + ", playbackType=" + mPlaybackType 1466 + ", playbackStream=" + mPlaybackStream 1467 + ", deviceType=" + mDeviceType 1468 + ", volumeHandling=" + mVolumeHandling 1469 + ", volume=" + mVolume 1470 + ", volumeMax=" + mVolumeMax 1471 + ", presentationDisplayId=" + mPresentationDisplayId 1472 + ", extras=" + mExtras 1473 + ", settingsIntent=" + mSettingsIntent 1474 + ", providerPackageName=" + mProvider.getPackageName() 1475 + " }"; 1476 } 1477 1478 int maybeUpdateDescriptor(MediaRouteDescriptor descriptor) { 1479 int changes = 0; 1480 if (mDescriptor != descriptor) { 1481 changes = updateDescriptor(descriptor); 1482 } 1483 return changes; 1484 } 1485 1486 int updateDescriptor(MediaRouteDescriptor descriptor) { 1487 int changes = 0; 1488 mDescriptor = descriptor; 1489 if (descriptor != null) { 1490 if (!ObjectsCompat.equals(mName, descriptor.getName())) { 1491 mName = descriptor.getName(); 1492 changes |= CHANGE_GENERAL; 1493 } 1494 if (!ObjectsCompat.equals(mDescription, descriptor.getDescription())) { 1495 mDescription = descriptor.getDescription(); 1496 changes |= CHANGE_GENERAL; 1497 } 1498 if (!ObjectsCompat.equals(mIconUri, descriptor.getIconUri())) { 1499 mIconUri = descriptor.getIconUri(); 1500 changes |= CHANGE_GENERAL; 1501 } 1502 if (mEnabled != descriptor.isEnabled()) { 1503 mEnabled = descriptor.isEnabled(); 1504 changes |= CHANGE_GENERAL; 1505 } 1506 if (mConnecting != descriptor.isConnecting()) { 1507 mConnecting = descriptor.isConnecting(); 1508 changes |= CHANGE_GENERAL; 1509 } 1510 if (mConnectionState != descriptor.getConnectionState()) { 1511 mConnectionState = descriptor.getConnectionState(); 1512 changes |= CHANGE_GENERAL; 1513 } 1514 if (!mControlFilters.equals(descriptor.getControlFilters())) { 1515 mControlFilters.clear(); 1516 mControlFilters.addAll(descriptor.getControlFilters()); 1517 changes |= CHANGE_GENERAL; 1518 } 1519 if (mPlaybackType != descriptor.getPlaybackType()) { 1520 mPlaybackType = descriptor.getPlaybackType(); 1521 changes |= CHANGE_GENERAL; 1522 } 1523 if (mPlaybackStream != descriptor.getPlaybackStream()) { 1524 mPlaybackStream = descriptor.getPlaybackStream(); 1525 changes |= CHANGE_GENERAL; 1526 } 1527 if (mDeviceType != descriptor.getDeviceType()) { 1528 mDeviceType = descriptor.getDeviceType(); 1529 changes |= CHANGE_GENERAL; 1530 } 1531 if (mVolumeHandling != descriptor.getVolumeHandling()) { 1532 mVolumeHandling = descriptor.getVolumeHandling(); 1533 changes |= CHANGE_GENERAL | CHANGE_VOLUME; 1534 } 1535 if (mVolume != descriptor.getVolume()) { 1536 mVolume = descriptor.getVolume(); 1537 changes |= CHANGE_GENERAL | CHANGE_VOLUME; 1538 } 1539 if (mVolumeMax != descriptor.getVolumeMax()) { 1540 mVolumeMax = descriptor.getVolumeMax(); 1541 changes |= CHANGE_GENERAL | CHANGE_VOLUME; 1542 } 1543 if (mPresentationDisplayId != descriptor.getPresentationDisplayId()) { 1544 mPresentationDisplayId = descriptor.getPresentationDisplayId(); 1545 mPresentationDisplay = null; 1546 changes |= CHANGE_GENERAL | CHANGE_PRESENTATION_DISPLAY; 1547 } 1548 if (!ObjectsCompat.equals(mExtras, descriptor.getExtras())) { 1549 mExtras = descriptor.getExtras(); 1550 changes |= CHANGE_GENERAL; 1551 } 1552 if (!ObjectsCompat.equals(mSettingsIntent, descriptor.getSettingsActivity())) { 1553 mSettingsIntent = descriptor.getSettingsActivity(); 1554 changes |= CHANGE_GENERAL; 1555 } 1556 if (mCanDisconnect != descriptor.canDisconnectAndKeepPlaying()) { 1557 mCanDisconnect = descriptor.canDisconnectAndKeepPlaying(); 1558 changes |= CHANGE_GENERAL | CHANGE_PRESENTATION_DISPLAY; 1559 } 1560 } 1561 return changes; 1562 } 1563 1564 String getDescriptorId() { 1565 return mDescriptorId; 1566 } 1567 1568 /** @hide */ 1569 @RestrictTo(LIBRARY_GROUP) 1570 public MediaRouteProvider getProviderInstance() { 1571 return mProvider.getProviderInstance(); 1572 } 1573 } 1574 1575 /** 1576 * Information about a route that consists of multiple other routes in a group. 1577 * @hide 1578 */ 1579 @RestrictTo(LIBRARY_GROUP) 1580 public static class RouteGroup extends RouteInfo { 1581 private List<RouteInfo> mRoutes = new ArrayList<>(); 1582 1583 RouteGroup(ProviderInfo provider, String descriptorId, String uniqueId) { 1584 super(provider, descriptorId, uniqueId); 1585 } 1586 1587 /** 1588 * @return The number of routes in this group 1589 */ 1590 public int getRouteCount() { 1591 return mRoutes.size(); 1592 } 1593 1594 /** 1595 * Returns the route in this group at the specified index 1596 * 1597 * @param index Index to fetch 1598 * @return The route at index 1599 */ 1600 public RouteInfo getRouteAt(int index) { 1601 return mRoutes.get(index); 1602 } 1603 1604 /** 1605 * Returns the routes in this group 1606 * 1607 * @return The list of the routes in this group 1608 */ 1609 public List<RouteInfo> getRoutes() { 1610 return mRoutes; 1611 } 1612 1613 @Override 1614 public String toString() { 1615 StringBuilder sb = new StringBuilder(super.toString()); 1616 sb.append('['); 1617 final int count = mRoutes.size(); 1618 for (int i = 0; i < count; i++) { 1619 if (i > 0) sb.append(", "); 1620 sb.append(mRoutes.get(i)); 1621 } 1622 sb.append(']'); 1623 return sb.toString(); 1624 } 1625 1626 @Override 1627 int maybeUpdateDescriptor(MediaRouteDescriptor descriptor) { 1628 boolean changed = false; 1629 if (mDescriptor != descriptor) { 1630 mDescriptor = descriptor; 1631 if (descriptor != null) { 1632 List<String> groupMemberIds = descriptor.getGroupMemberIds(); 1633 List<RouteInfo> routes = new ArrayList<>(); 1634 if (groupMemberIds == null) { 1635 Log.w(TAG, "groupMemberIds shouldn't be null."); 1636 changed = true; 1637 } else { 1638 changed = groupMemberIds.size() != mRoutes.size(); 1639 for (String groupMemberId : groupMemberIds) { 1640 String uniqueId = sGlobal.getUniqueId(getProvider(), groupMemberId); 1641 RouteInfo groupMember = sGlobal.getRoute(uniqueId); 1642 if (groupMember != null) { 1643 routes.add(groupMember); 1644 if (!changed && !mRoutes.contains(groupMember)) { 1645 changed = true; 1646 } 1647 } 1648 } 1649 } 1650 if (changed) { 1651 mRoutes = routes; 1652 } 1653 } 1654 } 1655 return (changed ? CHANGE_GENERAL : 0) | super.updateDescriptor(descriptor); 1656 } 1657 } 1658 1659 /** 1660 * Provides information about a media route provider. 1661 * <p> 1662 * This object may be used to determine which media route provider has 1663 * published a particular route. 1664 * </p> 1665 */ 1666 public static final class ProviderInfo { 1667 private final MediaRouteProvider mProviderInstance; 1668 private final List<RouteInfo> mRoutes = new ArrayList<>(); 1669 1670 private final ProviderMetadata mMetadata; 1671 private MediaRouteProviderDescriptor mDescriptor; 1672 private Resources mResources; 1673 private boolean mResourcesNotAvailable; 1674 1675 ProviderInfo(MediaRouteProvider provider) { 1676 mProviderInstance = provider; 1677 mMetadata = provider.getMetadata(); 1678 } 1679 1680 /** 1681 * Gets the provider's underlying {@link MediaRouteProvider} instance. 1682 */ 1683 public MediaRouteProvider getProviderInstance() { 1684 checkCallingThread(); 1685 return mProviderInstance; 1686 } 1687 1688 /** 1689 * Gets the package name of the media route provider. 1690 */ 1691 public String getPackageName() { 1692 return mMetadata.getPackageName(); 1693 } 1694 1695 /** 1696 * Gets the component name of the media route provider. 1697 */ 1698 public ComponentName getComponentName() { 1699 return mMetadata.getComponentName(); 1700 } 1701 1702 /** 1703 * Gets the {@link MediaRouter.RouteInfo routes} published by this route provider. 1704 */ 1705 public List<RouteInfo> getRoutes() { 1706 checkCallingThread(); 1707 return mRoutes; 1708 } 1709 1710 Resources getResources() { 1711 if (mResources == null && !mResourcesNotAvailable) { 1712 String packageName = getPackageName(); 1713 Context context = sGlobal.getProviderContext(packageName); 1714 if (context != null) { 1715 mResources = context.getResources(); 1716 } else { 1717 Log.w(TAG, "Unable to obtain resources for route provider package: " 1718 + packageName); 1719 mResourcesNotAvailable = true; 1720 } 1721 } 1722 return mResources; 1723 } 1724 1725 boolean updateDescriptor(MediaRouteProviderDescriptor descriptor) { 1726 if (mDescriptor != descriptor) { 1727 mDescriptor = descriptor; 1728 return true; 1729 } 1730 return false; 1731 } 1732 1733 int findRouteByDescriptorId(String id) { 1734 final int count = mRoutes.size(); 1735 for (int i = 0; i < count; i++) { 1736 if (mRoutes.get(i).mDescriptorId.equals(id)) { 1737 return i; 1738 } 1739 } 1740 return -1; 1741 } 1742 1743 @Override 1744 public String toString() { 1745 return "MediaRouter.RouteProviderInfo{ packageName=" + getPackageName() 1746 + " }"; 1747 } 1748 } 1749 1750 /** 1751 * Interface for receiving events about media routing changes. 1752 * All methods of this interface will be called from the application's main thread. 1753 * <p> 1754 * A Callback will only receive events relevant to routes that the callback 1755 * was registered for unless the {@link MediaRouter#CALLBACK_FLAG_UNFILTERED_EVENTS} 1756 * flag was specified in {@link MediaRouter#addCallback(MediaRouteSelector, Callback, int)}. 1757 * </p> 1758 * 1759 * @see MediaRouter#addCallback(MediaRouteSelector, Callback, int) 1760 * @see MediaRouter#removeCallback(Callback) 1761 */ 1762 public static abstract class Callback { 1763 /** 1764 * Called when the supplied media route becomes selected as the active route. 1765 * 1766 * @param router The media router reporting the event. 1767 * @param route The route that has been selected. 1768 */ 1769 public void onRouteSelected(MediaRouter router, RouteInfo route) { 1770 } 1771 1772 /** 1773 * Called when the supplied media route becomes unselected as the active route. 1774 * For detailed reason, override {@link #onRouteUnselected(MediaRouter, RouteInfo, int)} 1775 * instead. 1776 * 1777 * @param router The media router reporting the event. 1778 * @param route The route that has been unselected. 1779 */ 1780 public void onRouteUnselected(MediaRouter router, RouteInfo route) { 1781 } 1782 1783 /** 1784 * Called when the supplied media route becomes unselected as the active route. 1785 * The default implementation calls {@link #onRouteUnselected}. 1786 * <p> 1787 * The reason provided will be one of the following: 1788 * <ul> 1789 * <li>{@link MediaRouter#UNSELECT_REASON_UNKNOWN}</li> 1790 * <li>{@link MediaRouter#UNSELECT_REASON_DISCONNECTED}</li> 1791 * <li>{@link MediaRouter#UNSELECT_REASON_STOPPED}</li> 1792 * <li>{@link MediaRouter#UNSELECT_REASON_ROUTE_CHANGED}</li> 1793 * </ul> 1794 * 1795 * @param router The media router reporting the event. 1796 * @param route The route that has been unselected. 1797 * @param reason The reason for unselecting the route. 1798 */ 1799 public void onRouteUnselected(MediaRouter router, RouteInfo route, int reason) { 1800 onRouteUnselected(router, route); 1801 } 1802 1803 /** 1804 * Called when a media route has been added. 1805 * 1806 * @param router The media router reporting the event. 1807 * @param route The route that has become available for use. 1808 */ 1809 public void onRouteAdded(MediaRouter router, RouteInfo route) { 1810 } 1811 1812 /** 1813 * Called when a media route has been removed. 1814 * 1815 * @param router The media router reporting the event. 1816 * @param route The route that has been removed from availability. 1817 */ 1818 public void onRouteRemoved(MediaRouter router, RouteInfo route) { 1819 } 1820 1821 /** 1822 * Called when a property of the indicated media route has changed. 1823 * 1824 * @param router The media router reporting the event. 1825 * @param route The route that was changed. 1826 */ 1827 public void onRouteChanged(MediaRouter router, RouteInfo route) { 1828 } 1829 1830 /** 1831 * Called when a media route's volume changes. 1832 * 1833 * @param router The media router reporting the event. 1834 * @param route The route whose volume changed. 1835 */ 1836 public void onRouteVolumeChanged(MediaRouter router, RouteInfo route) { 1837 } 1838 1839 /** 1840 * Called when a media route's presentation display changes. 1841 * <p> 1842 * This method is called whenever the route's presentation display becomes 1843 * available, is removed or has changes to some of its properties (such as its size). 1844 * </p> 1845 * 1846 * @param router The media router reporting the event. 1847 * @param route The route whose presentation display changed. 1848 * 1849 * @see RouteInfo#getPresentationDisplay() 1850 */ 1851 public void onRoutePresentationDisplayChanged(MediaRouter router, RouteInfo route) { 1852 } 1853 1854 /** 1855 * Called when a media route provider has been added. 1856 * 1857 * @param router The media router reporting the event. 1858 * @param provider The provider that has become available for use. 1859 */ 1860 public void onProviderAdded(MediaRouter router, ProviderInfo provider) { 1861 } 1862 1863 /** 1864 * Called when a media route provider has been removed. 1865 * 1866 * @param router The media router reporting the event. 1867 * @param provider The provider that has been removed from availability. 1868 */ 1869 public void onProviderRemoved(MediaRouter router, ProviderInfo provider) { 1870 } 1871 1872 /** 1873 * Called when a property of the indicated media route provider has changed. 1874 * 1875 * @param router The media router reporting the event. 1876 * @param provider The provider that was changed. 1877 */ 1878 public void onProviderChanged(MediaRouter router, ProviderInfo provider) { 1879 } 1880 } 1881 1882 /** 1883 * Callback which is invoked with the result of a media control request. 1884 * 1885 * @see RouteInfo#sendControlRequest 1886 */ 1887 public static abstract class ControlRequestCallback { 1888 /** 1889 * Called when a media control request succeeds. 1890 * 1891 * @param data Result data, or null if none. 1892 * Contents depend on the {@link MediaControlIntent media control action}. 1893 */ 1894 public void onResult(Bundle data) { 1895 } 1896 1897 /** 1898 * Called when a media control request fails. 1899 * 1900 * @param error A localized error message which may be shown to the user, or null 1901 * if the cause of the error is unclear. 1902 * @param data Error data, or null if none. 1903 * Contents depend on the {@link MediaControlIntent media control action}. 1904 */ 1905 public void onError(String error, Bundle data) { 1906 } 1907 } 1908 1909 private static final class CallbackRecord { 1910 public final MediaRouter mRouter; 1911 public final Callback mCallback; 1912 public MediaRouteSelector mSelector; 1913 public int mFlags; 1914 1915 public CallbackRecord(MediaRouter router, Callback callback) { 1916 mRouter = router; 1917 mCallback = callback; 1918 mSelector = MediaRouteSelector.EMPTY; 1919 } 1920 1921 public boolean filterRouteEvent(RouteInfo route) { 1922 return (mFlags & CALLBACK_FLAG_UNFILTERED_EVENTS) != 0 1923 || route.matchesSelector(mSelector); 1924 } 1925 } 1926 1927 /** 1928 * Global state for the media router. 1929 * <p> 1930 * Media routes and media route providers are global to the process; their 1931 * state and the bulk of the media router implementation lives here. 1932 * </p> 1933 */ 1934 private static final class GlobalMediaRouter 1935 implements SystemMediaRouteProvider.SyncCallback, 1936 RegisteredMediaRouteProviderWatcher.Callback { 1937 final Context mApplicationContext; 1938 final ArrayList<WeakReference<MediaRouter>> mRouters = new ArrayList<>(); 1939 private final ArrayList<RouteInfo> mRoutes = new ArrayList<>(); 1940 private final Map<Pair<String, String>, String> mUniqueIdMap = new HashMap<>(); 1941 private final ArrayList<ProviderInfo> mProviders = new ArrayList<>(); 1942 private final ArrayList<RemoteControlClientRecord> mRemoteControlClients = 1943 new ArrayList<>(); 1944 final RemoteControlClientCompat.PlaybackInfo mPlaybackInfo = 1945 new RemoteControlClientCompat.PlaybackInfo(); 1946 private final ProviderCallback mProviderCallback = new ProviderCallback(); 1947 final CallbackHandler mCallbackHandler = new CallbackHandler(); 1948 private final DisplayManagerCompat mDisplayManager; 1949 final SystemMediaRouteProvider mSystemProvider; 1950 private final boolean mLowRam; 1951 1952 private RegisteredMediaRouteProviderWatcher mRegisteredProviderWatcher; 1953 private RouteInfo mDefaultRoute; 1954 private RouteInfo mBluetoothRoute; 1955 RouteInfo mSelectedRoute; 1956 private RouteController mSelectedRouteController; 1957 // A map from route descriptor ID to RouteController for the member routes in the currently 1958 // selected route group. 1959 private final Map<String, RouteController> mRouteControllerMap = new HashMap<>(); 1960 private MediaRouteDiscoveryRequest mDiscoveryRequest; 1961 private MediaSessionRecord mMediaSession; 1962 MediaSessionCompat mRccMediaSession; 1963 private MediaSessionCompat mCompatSession; 1964 private MediaSessionCompat.OnActiveChangeListener mSessionActiveListener = 1965 new MediaSessionCompat.OnActiveChangeListener() { 1966 @Override 1967 public void onActiveChanged() { 1968 if(mRccMediaSession != null) { 1969 if (mRccMediaSession.isActive()) { 1970 addRemoteControlClient(mRccMediaSession.getRemoteControlClient()); 1971 } else { 1972 removeRemoteControlClient(mRccMediaSession.getRemoteControlClient()); 1973 } 1974 } 1975 } 1976 }; 1977 1978 GlobalMediaRouter(Context applicationContext) { 1979 mApplicationContext = applicationContext; 1980 mDisplayManager = DisplayManagerCompat.getInstance(applicationContext); 1981 mLowRam = ActivityManagerCompat.isLowRamDevice( 1982 (ActivityManager)applicationContext.getSystemService( 1983 Context.ACTIVITY_SERVICE)); 1984 1985 // Add the system media route provider for interoperating with 1986 // the framework media router. This one is special and receives 1987 // synchronization messages from the media router. 1988 mSystemProvider = SystemMediaRouteProvider.obtain(applicationContext, this); 1989 } 1990 1991 public void start() { 1992 addProvider(mSystemProvider); 1993 1994 // Start watching for routes published by registered media route 1995 // provider services. 1996 mRegisteredProviderWatcher = new RegisteredMediaRouteProviderWatcher( 1997 mApplicationContext, this); 1998 mRegisteredProviderWatcher.start(); 1999 } 2000 2001 public MediaRouter getRouter(Context context) { 2002 MediaRouter router; 2003 for (int i = mRouters.size(); --i >= 0; ) { 2004 router = mRouters.get(i).get(); 2005 if (router == null) { 2006 mRouters.remove(i); 2007 } else if (router.mContext == context) { 2008 return router; 2009 } 2010 } 2011 router = new MediaRouter(context); 2012 mRouters.add(new WeakReference<MediaRouter>(router)); 2013 return router; 2014 } 2015 2016 public ContentResolver getContentResolver() { 2017 return mApplicationContext.getContentResolver(); 2018 } 2019 2020 public Context getProviderContext(String packageName) { 2021 if (packageName.equals(SystemMediaRouteProvider.PACKAGE_NAME)) { 2022 return mApplicationContext; 2023 } 2024 try { 2025 return mApplicationContext.createPackageContext( 2026 packageName, Context.CONTEXT_RESTRICTED); 2027 } catch (NameNotFoundException ex) { 2028 return null; 2029 } 2030 } 2031 2032 public Display getDisplay(int displayId) { 2033 return mDisplayManager.getDisplay(displayId); 2034 } 2035 2036 public void sendControlRequest(RouteInfo route, 2037 Intent intent, ControlRequestCallback callback) { 2038 if (route == mSelectedRoute && mSelectedRouteController != null) { 2039 if (mSelectedRouteController.onControlRequest(intent, callback)) { 2040 return; 2041 } 2042 } 2043 if (callback != null) { 2044 callback.onError(null, null); 2045 } 2046 } 2047 2048 public void requestSetVolume(RouteInfo route, int volume) { 2049 if (route == mSelectedRoute && mSelectedRouteController != null) { 2050 mSelectedRouteController.onSetVolume(volume); 2051 } else if (!mRouteControllerMap.isEmpty()) { 2052 RouteController controller = mRouteControllerMap.get(route.mDescriptorId); 2053 if (controller != null) { 2054 controller.onSetVolume(volume); 2055 } 2056 } 2057 } 2058 2059 public void requestUpdateVolume(RouteInfo route, int delta) { 2060 if (route == mSelectedRoute && mSelectedRouteController != null) { 2061 mSelectedRouteController.onUpdateVolume(delta); 2062 } 2063 } 2064 2065 public RouteInfo getRoute(String uniqueId) { 2066 for (RouteInfo info : mRoutes) { 2067 if (info.mUniqueId.equals(uniqueId)) { 2068 return info; 2069 } 2070 } 2071 return null; 2072 } 2073 2074 public List<RouteInfo> getRoutes() { 2075 return mRoutes; 2076 } 2077 2078 List<ProviderInfo> getProviders() { 2079 return mProviders; 2080 } 2081 2082 @NonNull RouteInfo getDefaultRoute() { 2083 if (mDefaultRoute == null) { 2084 // This should never happen once the media router has been fully 2085 // initialized but it is good to check for the error in case there 2086 // is a bug in provider initialization. 2087 throw new IllegalStateException("There is no default route. " 2088 + "The media router has not yet been fully initialized."); 2089 } 2090 return mDefaultRoute; 2091 } 2092 2093 RouteInfo getBluetoothRoute() { 2094 return mBluetoothRoute; 2095 } 2096 2097 @NonNull RouteInfo getSelectedRoute() { 2098 if (mSelectedRoute == null) { 2099 // This should never happen once the media router has been fully 2100 // initialized but it is good to check for the error in case there 2101 // is a bug in provider initialization. 2102 throw new IllegalStateException("There is no currently selected route. " 2103 + "The media router has not yet been fully initialized."); 2104 } 2105 return mSelectedRoute; 2106 } 2107 2108 void selectRoute(@NonNull RouteInfo route) { 2109 selectRoute(route, MediaRouter.UNSELECT_REASON_ROUTE_CHANGED); 2110 } 2111 2112 void selectRoute(@NonNull RouteInfo route, int unselectReason) { 2113 if (!mRoutes.contains(route)) { 2114 Log.w(TAG, "Ignoring attempt to select removed route: " + route); 2115 return; 2116 } 2117 if (!route.mEnabled) { 2118 Log.w(TAG, "Ignoring attempt to select disabled route: " + route); 2119 return; 2120 } 2121 setSelectedRouteInternal(route, unselectReason); 2122 } 2123 2124 public boolean isRouteAvailable(MediaRouteSelector selector, int flags) { 2125 if (selector.isEmpty()) { 2126 return false; 2127 } 2128 2129 // On low-RAM devices, do not rely on actual discovery results unless asked to. 2130 if ((flags & AVAILABILITY_FLAG_REQUIRE_MATCH) == 0 && mLowRam) { 2131 return true; 2132 } 2133 2134 // Check whether any existing routes match the selector. 2135 final int routeCount = mRoutes.size(); 2136 for (int i = 0; i < routeCount; i++) { 2137 RouteInfo route = mRoutes.get(i); 2138 if ((flags & AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE) != 0 2139 && route.isDefaultOrBluetooth()) { 2140 continue; 2141 } 2142 if (route.matchesSelector(selector)) { 2143 return true; 2144 } 2145 } 2146 2147 // It doesn't look like we can find a matching route right now. 2148 return false; 2149 } 2150 2151 public void updateDiscoveryRequest() { 2152 // Combine all of the callback selectors and active scan flags. 2153 boolean discover = false; 2154 boolean activeScan = false; 2155 MediaRouteSelector.Builder builder = new MediaRouteSelector.Builder(); 2156 for (int i = mRouters.size(); --i >= 0; ) { 2157 MediaRouter router = mRouters.get(i).get(); 2158 if (router == null) { 2159 mRouters.remove(i); 2160 } else { 2161 final int count = router.mCallbackRecords.size(); 2162 for (int j = 0; j < count; j++) { 2163 CallbackRecord callback = router.mCallbackRecords.get(j); 2164 builder.addSelector(callback.mSelector); 2165 if ((callback.mFlags & CALLBACK_FLAG_PERFORM_ACTIVE_SCAN) != 0) { 2166 activeScan = true; 2167 discover = true; // perform active scan implies request discovery 2168 } 2169 if ((callback.mFlags & CALLBACK_FLAG_REQUEST_DISCOVERY) != 0) { 2170 if (!mLowRam) { 2171 discover = true; 2172 } 2173 } 2174 if ((callback.mFlags & CALLBACK_FLAG_FORCE_DISCOVERY) != 0) { 2175 discover = true; 2176 } 2177 } 2178 } 2179 } 2180 MediaRouteSelector selector = discover ? builder.build() : MediaRouteSelector.EMPTY; 2181 2182 // Create a new discovery request. 2183 if (mDiscoveryRequest != null 2184 && mDiscoveryRequest.getSelector().equals(selector) 2185 && mDiscoveryRequest.isActiveScan() == activeScan) { 2186 return; // no change 2187 } 2188 if (selector.isEmpty() && !activeScan) { 2189 // Discovery is not needed. 2190 if (mDiscoveryRequest == null) { 2191 return; // no change 2192 } 2193 mDiscoveryRequest = null; 2194 } else { 2195 // Discovery is needed. 2196 mDiscoveryRequest = new MediaRouteDiscoveryRequest(selector, activeScan); 2197 } 2198 if (DEBUG) { 2199 Log.d(TAG, "Updated discovery request: " + mDiscoveryRequest); 2200 } 2201 if (discover && !activeScan && mLowRam) { 2202 Log.i(TAG, "Forcing passive route discovery on a low-RAM device, " 2203 + "system performance may be affected. Please consider using " 2204 + "CALLBACK_FLAG_REQUEST_DISCOVERY instead of " 2205 + "CALLBACK_FLAG_FORCE_DISCOVERY."); 2206 } 2207 2208 // Notify providers. 2209 final int providerCount = mProviders.size(); 2210 for (int i = 0; i < providerCount; i++) { 2211 mProviders.get(i).mProviderInstance.setDiscoveryRequest(mDiscoveryRequest); 2212 } 2213 } 2214 2215 @Override 2216 public void addProvider(MediaRouteProvider providerInstance) { 2217 int index = findProviderInfo(providerInstance); 2218 if (index < 0) { 2219 // 1. Add the provider to the list. 2220 ProviderInfo provider = new ProviderInfo(providerInstance); 2221 mProviders.add(provider); 2222 if (DEBUG) { 2223 Log.d(TAG, "Provider added: " + provider); 2224 } 2225 mCallbackHandler.post(CallbackHandler.MSG_PROVIDER_ADDED, provider); 2226 // 2. Create the provider's contents. 2227 updateProviderContents(provider, providerInstance.getDescriptor()); 2228 // 3. Register the provider callback. 2229 providerInstance.setCallback(mProviderCallback); 2230 // 4. Set the discovery request. 2231 providerInstance.setDiscoveryRequest(mDiscoveryRequest); 2232 } 2233 } 2234 2235 @Override 2236 public void removeProvider(MediaRouteProvider providerInstance) { 2237 int index = findProviderInfo(providerInstance); 2238 if (index >= 0) { 2239 // 1. Unregister the provider callback. 2240 providerInstance.setCallback(null); 2241 // 2. Clear the discovery request. 2242 providerInstance.setDiscoveryRequest(null); 2243 // 3. Delete the provider's contents. 2244 ProviderInfo provider = mProviders.get(index); 2245 updateProviderContents(provider, null); 2246 // 4. Remove the provider from the list. 2247 if (DEBUG) { 2248 Log.d(TAG, "Provider removed: " + provider); 2249 } 2250 mCallbackHandler.post(CallbackHandler.MSG_PROVIDER_REMOVED, provider); 2251 mProviders.remove(index); 2252 } 2253 } 2254 2255 void updateProviderDescriptor(MediaRouteProvider providerInstance, 2256 MediaRouteProviderDescriptor descriptor) { 2257 int index = findProviderInfo(providerInstance); 2258 if (index >= 0) { 2259 // Update the provider's contents. 2260 ProviderInfo provider = mProviders.get(index); 2261 updateProviderContents(provider, descriptor); 2262 } 2263 } 2264 2265 private int findProviderInfo(MediaRouteProvider providerInstance) { 2266 final int count = mProviders.size(); 2267 for (int i = 0; i < count; i++) { 2268 if (mProviders.get(i).mProviderInstance == providerInstance) { 2269 return i; 2270 } 2271 } 2272 return -1; 2273 } 2274 2275 private void updateProviderContents(ProviderInfo provider, 2276 MediaRouteProviderDescriptor providerDescriptor) { 2277 if (provider.updateDescriptor(providerDescriptor)) { 2278 // Update all existing routes and reorder them to match 2279 // the order of their descriptors. 2280 int targetIndex = 0; 2281 boolean selectedRouteDescriptorChanged = false; 2282 if (providerDescriptor != null) { 2283 if (providerDescriptor.isValid()) { 2284 final List<MediaRouteDescriptor> routeDescriptors = 2285 providerDescriptor.getRoutes(); 2286 final int routeCount = routeDescriptors.size(); 2287 // Updating route group's contents requires all member routes' information. 2288 // Add the groups to the lists and update them later. 2289 List<Pair<RouteInfo, MediaRouteDescriptor>> addedGroups = new ArrayList<>(); 2290 List<Pair<RouteInfo, MediaRouteDescriptor>> updatedGroups = 2291 new ArrayList<>(); 2292 for (int i = 0; i < routeCount; i++) { 2293 final MediaRouteDescriptor routeDescriptor = routeDescriptors.get(i); 2294 final String id = routeDescriptor.getId(); 2295 final int sourceIndex = provider.findRouteByDescriptorId(id); 2296 boolean isGroup = routeDescriptor.getGroupMemberIds() != null; 2297 if (sourceIndex < 0) { 2298 // 1. Add the route to the list. 2299 String uniqueId = assignRouteUniqueId(provider, id); 2300 RouteInfo route = isGroup ? new RouteGroup(provider, id, uniqueId) : 2301 new RouteInfo(provider, id, uniqueId); 2302 provider.mRoutes.add(targetIndex++, route); 2303 mRoutes.add(route); 2304 // 2. Create the route's contents. 2305 if (isGroup) { 2306 addedGroups.add(new Pair<>(route, routeDescriptor)); 2307 } else { 2308 route.maybeUpdateDescriptor(routeDescriptor); 2309 // 3. Notify clients about addition. 2310 if (DEBUG) { 2311 Log.d(TAG, "Route added: " + route); 2312 } 2313 mCallbackHandler.post(CallbackHandler.MSG_ROUTE_ADDED, route); 2314 } 2315 2316 } else if (sourceIndex < targetIndex) { 2317 Log.w(TAG, "Ignoring route descriptor with duplicate id: " 2318 + routeDescriptor); 2319 } else { 2320 RouteInfo route = provider.mRoutes.get(sourceIndex); 2321 // 1. Replace route if a group route becomes a normal route 2322 // or vice versa. 2323 if ((route instanceof RouteGroup) != isGroup) { 2324 route = isGroup ? new RouteGroup(provider, id, route.getId()) : 2325 new RouteInfo(provider, id, route.getId()); 2326 provider.mRoutes.set(sourceIndex, route); 2327 } 2328 // 2. Reorder the route within the list. 2329 Collections.swap(provider.mRoutes, 2330 sourceIndex, targetIndex++); 2331 // 3. Update the route's contents. 2332 if (route instanceof RouteGroup) { 2333 updatedGroups.add(new Pair<>(route, routeDescriptor)); 2334 } else { 2335 // 4. Notify clients about changes. 2336 if (updateRouteDescriptorAndNotify(route, routeDescriptor) 2337 != 0) { 2338 if (route == mSelectedRoute) { 2339 selectedRouteDescriptorChanged = true; 2340 } 2341 } 2342 } 2343 } 2344 } 2345 // Update the new and/or existing groups. 2346 for (Pair<RouteInfo, MediaRouteDescriptor> pair : addedGroups) { 2347 RouteInfo route = pair.first; 2348 route.maybeUpdateDescriptor(pair.second); 2349 if (DEBUG) { 2350 Log.d(TAG, "Route added: " + route); 2351 } 2352 mCallbackHandler.post(CallbackHandler.MSG_ROUTE_ADDED, route); 2353 } 2354 for (Pair<RouteInfo, MediaRouteDescriptor> pair : updatedGroups) { 2355 RouteInfo route = pair.first; 2356 if (updateRouteDescriptorAndNotify(route, pair.second) != 0) { 2357 if (route == mSelectedRoute) { 2358 selectedRouteDescriptorChanged = true; 2359 } 2360 } 2361 } 2362 } else { 2363 Log.w(TAG, "Ignoring invalid provider descriptor: " + providerDescriptor); 2364 } 2365 } 2366 2367 // Dispose all remaining routes that do not have matching descriptors. 2368 for (int i = provider.mRoutes.size() - 1; i >= targetIndex; i--) { 2369 // 1. Delete the route's contents. 2370 RouteInfo route = provider.mRoutes.get(i); 2371 route.maybeUpdateDescriptor(null); 2372 // 2. Remove the route from the list. 2373 mRoutes.remove(route); 2374 } 2375 2376 // Update the selected route if needed. 2377 updateSelectedRouteIfNeeded(selectedRouteDescriptorChanged); 2378 2379 // Now notify clients about routes that were removed. 2380 // We do this after updating the selected route to ensure 2381 // that the framework media router observes the new route 2382 // selection before the removal since removing the currently 2383 // selected route may have side-effects. 2384 for (int i = provider.mRoutes.size() - 1; i >= targetIndex; i--) { 2385 RouteInfo route = provider.mRoutes.remove(i); 2386 if (DEBUG) { 2387 Log.d(TAG, "Route removed: " + route); 2388 } 2389 mCallbackHandler.post(CallbackHandler.MSG_ROUTE_REMOVED, route); 2390 } 2391 2392 // Notify provider changed. 2393 if (DEBUG) { 2394 Log.d(TAG, "Provider changed: " + provider); 2395 } 2396 mCallbackHandler.post(CallbackHandler.MSG_PROVIDER_CHANGED, provider); 2397 } 2398 } 2399 2400 private int updateRouteDescriptorAndNotify(RouteInfo route, 2401 MediaRouteDescriptor routeDescriptor) { 2402 int changes = route.maybeUpdateDescriptor(routeDescriptor); 2403 if (changes != 0) { 2404 if ((changes & RouteInfo.CHANGE_GENERAL) != 0) { 2405 if (DEBUG) { 2406 Log.d(TAG, "Route changed: " + route); 2407 } 2408 mCallbackHandler.post( 2409 CallbackHandler.MSG_ROUTE_CHANGED, route); 2410 } 2411 if ((changes & RouteInfo.CHANGE_VOLUME) != 0) { 2412 if (DEBUG) { 2413 Log.d(TAG, "Route volume changed: " + route); 2414 } 2415 mCallbackHandler.post( 2416 CallbackHandler.MSG_ROUTE_VOLUME_CHANGED, route); 2417 } 2418 if ((changes & RouteInfo.CHANGE_PRESENTATION_DISPLAY) != 0) { 2419 if (DEBUG) { 2420 Log.d(TAG, "Route presentation display changed: " 2421 + route); 2422 } 2423 mCallbackHandler.post(CallbackHandler. 2424 MSG_ROUTE_PRESENTATION_DISPLAY_CHANGED, route); 2425 } 2426 } 2427 return changes; 2428 } 2429 2430 private String assignRouteUniqueId(ProviderInfo provider, String routeDescriptorId) { 2431 // Although route descriptor ids are unique within a provider, it's 2432 // possible for there to be two providers with the same package name. 2433 // Therefore we must dedupe the composite id. 2434 String componentName = provider.getComponentName().flattenToShortString(); 2435 String uniqueId = componentName + ":" + routeDescriptorId; 2436 if (findRouteByUniqueId(uniqueId) < 0) { 2437 mUniqueIdMap.put(new Pair<>(componentName, routeDescriptorId), uniqueId); 2438 return uniqueId; 2439 } 2440 Log.w(TAG, "Either " + routeDescriptorId + " isn't unique in " + componentName 2441 + " or we're trying to assign a unique ID for an already added route"); 2442 for (int i = 2; ; i++) { 2443 String newUniqueId = String.format(Locale.US, "%s_%d", uniqueId, i); 2444 if (findRouteByUniqueId(newUniqueId) < 0) { 2445 mUniqueIdMap.put(new Pair<>(componentName, routeDescriptorId), newUniqueId); 2446 return newUniqueId; 2447 } 2448 } 2449 } 2450 2451 private int findRouteByUniqueId(String uniqueId) { 2452 final int count = mRoutes.size(); 2453 for (int i = 0; i < count; i++) { 2454 if (mRoutes.get(i).mUniqueId.equals(uniqueId)) { 2455 return i; 2456 } 2457 } 2458 return -1; 2459 } 2460 2461 private String getUniqueId(ProviderInfo provider, String routeDescriptorId) { 2462 String componentName = provider.getComponentName().flattenToShortString(); 2463 return mUniqueIdMap.get(new Pair<>(componentName, routeDescriptorId)); 2464 } 2465 2466 private void updateSelectedRouteIfNeeded(boolean selectedRouteDescriptorChanged) { 2467 // Update default route. 2468 if (mDefaultRoute != null && !mDefaultRoute.isSelectable()) { 2469 Log.i(TAG, "Clearing the default route because it " 2470 + "is no longer selectable: " + mDefaultRoute); 2471 mDefaultRoute = null; 2472 } 2473 if (mDefaultRoute == null && !mRoutes.isEmpty()) { 2474 for (RouteInfo route : mRoutes) { 2475 if (isSystemDefaultRoute(route) && route.isSelectable()) { 2476 mDefaultRoute = route; 2477 Log.i(TAG, "Found default route: " + mDefaultRoute); 2478 break; 2479 } 2480 } 2481 } 2482 2483 // Update bluetooth route. 2484 if (mBluetoothRoute != null && !mBluetoothRoute.isSelectable()) { 2485 Log.i(TAG, "Clearing the bluetooth route because it " 2486 + "is no longer selectable: " + mBluetoothRoute); 2487 mBluetoothRoute = null; 2488 } 2489 if (mBluetoothRoute == null && !mRoutes.isEmpty()) { 2490 for (RouteInfo route : mRoutes) { 2491 if (isSystemLiveAudioOnlyRoute(route) && route.isSelectable()) { 2492 mBluetoothRoute = route; 2493 Log.i(TAG, "Found bluetooth route: " + mBluetoothRoute); 2494 break; 2495 } 2496 } 2497 } 2498 2499 // Update selected route. 2500 if (mSelectedRoute == null || !mSelectedRoute.isSelectable()) { 2501 Log.i(TAG, "Unselecting the current route because it " 2502 + "is no longer selectable: " + mSelectedRoute); 2503 setSelectedRouteInternal(chooseFallbackRoute(), 2504 MediaRouter.UNSELECT_REASON_UNKNOWN); 2505 } else if (selectedRouteDescriptorChanged) { 2506 // In case the selected route is a route group, select/unselect route controllers 2507 // for the added/removed route members. 2508 if (mSelectedRoute instanceof RouteGroup) { 2509 List<RouteInfo> routes = ((RouteGroup) mSelectedRoute).getRoutes(); 2510 // Build a set of descriptor IDs for the new route group. 2511 Set<String> idSet = new HashSet<>(); 2512 for (RouteInfo route : routes) { 2513 idSet.add(route.mDescriptorId); 2514 } 2515 // Unselect route controllers for the removed routes. 2516 Iterator<Map.Entry<String, RouteController>> iter = 2517 mRouteControllerMap.entrySet().iterator(); 2518 while (iter.hasNext()) { 2519 Map.Entry<String, RouteController> entry = iter.next(); 2520 if (!idSet.contains(entry.getKey())) { 2521 RouteController controller = entry.getValue(); 2522 controller.onUnselect(); 2523 controller.onRelease(); 2524 iter.remove(); 2525 } 2526 } 2527 // Select route controllers for the added routes. 2528 for (RouteInfo route : routes) { 2529 if (!mRouteControllerMap.containsKey(route.mDescriptorId)) { 2530 RouteController controller = route.getProviderInstance() 2531 .onCreateRouteController( 2532 route.mDescriptorId, mSelectedRoute.mDescriptorId); 2533 controller.onSelect(); 2534 mRouteControllerMap.put(route.mDescriptorId, controller); 2535 } 2536 } 2537 } 2538 // Update the playback info because the properties of the route have changed. 2539 updatePlaybackInfoFromSelectedRoute(); 2540 } 2541 } 2542 2543 RouteInfo chooseFallbackRoute() { 2544 // When the current route is removed or no longer selectable, 2545 // we want to revert to a live audio route if there is 2546 // one (usually Bluetooth A2DP). Failing that, use 2547 // the default route. 2548 for (RouteInfo route : mRoutes) { 2549 if (route != mDefaultRoute 2550 && isSystemLiveAudioOnlyRoute(route) 2551 && route.isSelectable()) { 2552 return route; 2553 } 2554 } 2555 return mDefaultRoute; 2556 } 2557 2558 private boolean isSystemLiveAudioOnlyRoute(RouteInfo route) { 2559 return route.getProviderInstance() == mSystemProvider 2560 && route.supportsControlCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO) 2561 && !route.supportsControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO); 2562 } 2563 2564 private boolean isSystemDefaultRoute(RouteInfo route) { 2565 return route.getProviderInstance() == mSystemProvider 2566 && route.mDescriptorId.equals( 2567 SystemMediaRouteProvider.DEFAULT_ROUTE_ID); 2568 } 2569 2570 private void setSelectedRouteInternal(@NonNull RouteInfo route, int unselectReason) { 2571 // TODO: Remove the following logging when no longer needed. 2572 if (sGlobal == null || (mBluetoothRoute != null && route.isDefault())) { 2573 final StackTraceElement[] callStack = Thread.currentThread().getStackTrace(); 2574 StringBuilder sb = new StringBuilder(); 2575 // callStack[3] is the caller of this method. 2576 for (int i = 3; i < callStack.length; i++) { 2577 StackTraceElement caller = callStack[i]; 2578 sb.append(caller.getClassName()) 2579 .append(".") 2580 .append(caller.getMethodName()) 2581 .append(":") 2582 .append(caller.getLineNumber()) 2583 .append(" "); 2584 } 2585 if (sGlobal == null) { 2586 Log.w(TAG, "setSelectedRouteInternal is called while sGlobal is null: pkgName=" 2587 + mApplicationContext.getPackageName() + ", callers=" + sb.toString()); 2588 } else { 2589 Log.w(TAG, "Default route is selected while a BT route is available: pkgName=" 2590 + mApplicationContext.getPackageName() + ", callers=" + sb.toString()); 2591 } 2592 } 2593 2594 if (mSelectedRoute != route) { 2595 if (mSelectedRoute != null) { 2596 if (DEBUG) { 2597 Log.d(TAG, "Route unselected: " + mSelectedRoute + " reason: " 2598 + unselectReason); 2599 } 2600 mCallbackHandler.post(CallbackHandler.MSG_ROUTE_UNSELECTED, mSelectedRoute, 2601 unselectReason); 2602 if (mSelectedRouteController != null) { 2603 mSelectedRouteController.onUnselect(unselectReason); 2604 mSelectedRouteController.onRelease(); 2605 mSelectedRouteController = null; 2606 } 2607 if (!mRouteControllerMap.isEmpty()) { 2608 for (RouteController controller : mRouteControllerMap.values()) { 2609 controller.onUnselect(unselectReason); 2610 controller.onRelease(); 2611 } 2612 mRouteControllerMap.clear(); 2613 } 2614 } 2615 2616 mSelectedRoute = route; 2617 mSelectedRouteController = route.getProviderInstance().onCreateRouteController( 2618 route.mDescriptorId); 2619 if (mSelectedRouteController != null) { 2620 mSelectedRouteController.onSelect(); 2621 } 2622 if (DEBUG) { 2623 Log.d(TAG, "Route selected: " + mSelectedRoute); 2624 } 2625 mCallbackHandler.post(CallbackHandler.MSG_ROUTE_SELECTED, mSelectedRoute); 2626 2627 if (mSelectedRoute instanceof RouteGroup) { 2628 List<RouteInfo> routes = ((RouteGroup) mSelectedRoute).getRoutes(); 2629 mRouteControllerMap.clear(); 2630 for (RouteInfo r : routes) { 2631 RouteController controller = 2632 r.getProviderInstance().onCreateRouteController( 2633 r.mDescriptorId, mSelectedRoute.mDescriptorId); 2634 controller.onSelect(); 2635 mRouteControllerMap.put(r.mDescriptorId, controller); 2636 } 2637 } 2638 2639 updatePlaybackInfoFromSelectedRoute(); 2640 } 2641 } 2642 2643 @Override 2644 public void onSystemRouteSelectedByDescriptorId(String id) { 2645 // System route is selected, do not sync the route we selected before. 2646 mCallbackHandler.removeMessages(CallbackHandler.MSG_ROUTE_SELECTED); 2647 int providerIndex = findProviderInfo(mSystemProvider); 2648 if (providerIndex >= 0) { 2649 ProviderInfo provider = mProviders.get(providerIndex); 2650 int routeIndex = provider.findRouteByDescriptorId(id); 2651 if (routeIndex >= 0) { 2652 provider.mRoutes.get(routeIndex).select(); 2653 } 2654 } 2655 } 2656 2657 public void addRemoteControlClient(Object rcc) { 2658 int index = findRemoteControlClientRecord(rcc); 2659 if (index < 0) { 2660 RemoteControlClientRecord record = new RemoteControlClientRecord(rcc); 2661 mRemoteControlClients.add(record); 2662 } 2663 } 2664 2665 public void removeRemoteControlClient(Object rcc) { 2666 int index = findRemoteControlClientRecord(rcc); 2667 if (index >= 0) { 2668 RemoteControlClientRecord record = mRemoteControlClients.remove(index); 2669 record.disconnect(); 2670 } 2671 } 2672 2673 public void setMediaSession(Object session) { 2674 setMediaSessionRecord(session != null ? new MediaSessionRecord(session) : null); 2675 } 2676 2677 public void setMediaSessionCompat(final MediaSessionCompat session) { 2678 mCompatSession = session; 2679 if (android.os.Build.VERSION.SDK_INT >= 21) { 2680 setMediaSessionRecord(session != null ? new MediaSessionRecord(session) : null); 2681 } else if (android.os.Build.VERSION.SDK_INT >= 14) { 2682 if (mRccMediaSession != null) { 2683 removeRemoteControlClient(mRccMediaSession.getRemoteControlClient()); 2684 mRccMediaSession.removeOnActiveChangeListener(mSessionActiveListener); 2685 } 2686 mRccMediaSession = session; 2687 if (session != null) { 2688 session.addOnActiveChangeListener(mSessionActiveListener); 2689 if (session.isActive()) { 2690 addRemoteControlClient(session.getRemoteControlClient()); 2691 } 2692 } 2693 } 2694 } 2695 2696 private void setMediaSessionRecord(MediaSessionRecord mediaSessionRecord) { 2697 if (mMediaSession != null) { 2698 mMediaSession.clearVolumeHandling(); 2699 } 2700 mMediaSession = mediaSessionRecord; 2701 if (mediaSessionRecord != null) { 2702 updatePlaybackInfoFromSelectedRoute(); 2703 } 2704 } 2705 2706 public MediaSessionCompat.Token getMediaSessionToken() { 2707 if (mMediaSession != null) { 2708 return mMediaSession.getToken(); 2709 } else if (mCompatSession != null) { 2710 return mCompatSession.getSessionToken(); 2711 } 2712 return null; 2713 } 2714 2715 private int findRemoteControlClientRecord(Object rcc) { 2716 final int count = mRemoteControlClients.size(); 2717 for (int i = 0; i < count; i++) { 2718 RemoteControlClientRecord record = mRemoteControlClients.get(i); 2719 if (record.getRemoteControlClient() == rcc) { 2720 return i; 2721 } 2722 } 2723 return -1; 2724 } 2725 2726 private void updatePlaybackInfoFromSelectedRoute() { 2727 if (mSelectedRoute != null) { 2728 mPlaybackInfo.volume = mSelectedRoute.getVolume(); 2729 mPlaybackInfo.volumeMax = mSelectedRoute.getVolumeMax(); 2730 mPlaybackInfo.volumeHandling = mSelectedRoute.getVolumeHandling(); 2731 mPlaybackInfo.playbackStream = mSelectedRoute.getPlaybackStream(); 2732 mPlaybackInfo.playbackType = mSelectedRoute.getPlaybackType(); 2733 2734 final int count = mRemoteControlClients.size(); 2735 for (int i = 0; i < count; i++) { 2736 RemoteControlClientRecord record = mRemoteControlClients.get(i); 2737 record.updatePlaybackInfo(); 2738 } 2739 if (mMediaSession != null) { 2740 if (mSelectedRoute == getDefaultRoute() 2741 || mSelectedRoute == getBluetoothRoute()) { 2742 // Local route 2743 mMediaSession.clearVolumeHandling(); 2744 } else { 2745 @VolumeProviderCompat.ControlType int controlType = 2746 VolumeProviderCompat.VOLUME_CONTROL_FIXED; 2747 if (mPlaybackInfo.volumeHandling 2748 == MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE) { 2749 controlType = VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE; 2750 } 2751 mMediaSession.configureVolume(controlType, mPlaybackInfo.volumeMax, 2752 mPlaybackInfo.volume); 2753 } 2754 } 2755 } else { 2756 if (mMediaSession != null) { 2757 mMediaSession.clearVolumeHandling(); 2758 } 2759 } 2760 } 2761 2762 private final class ProviderCallback extends MediaRouteProvider.Callback { 2763 ProviderCallback() { 2764 } 2765 2766 @Override 2767 public void onDescriptorChanged(MediaRouteProvider provider, 2768 MediaRouteProviderDescriptor descriptor) { 2769 updateProviderDescriptor(provider, descriptor); 2770 } 2771 } 2772 2773 private final class MediaSessionRecord { 2774 private final MediaSessionCompat mMsCompat; 2775 2776 private @VolumeProviderCompat.ControlType int mControlType; 2777 private int mMaxVolume; 2778 private VolumeProviderCompat mVpCompat; 2779 2780 public MediaSessionRecord(Object mediaSession) { 2781 mMsCompat = MediaSessionCompat.fromMediaSession(mApplicationContext, mediaSession); 2782 } 2783 2784 public MediaSessionRecord(MediaSessionCompat mediaSessionCompat) { 2785 mMsCompat = mediaSessionCompat; 2786 } 2787 2788 public void configureVolume(@VolumeProviderCompat.ControlType int controlType, 2789 int max, int current) { 2790 if (mVpCompat != null && controlType == mControlType && max == mMaxVolume) { 2791 // If we haven't changed control type or max just set the 2792 // new current volume 2793 mVpCompat.setCurrentVolume(current); 2794 } else { 2795 // Otherwise create a new provider and update 2796 mVpCompat = new VolumeProviderCompat(controlType, max, current) { 2797 @Override 2798 public void onSetVolumeTo(final int volume) { 2799 mCallbackHandler.post(new Runnable() { 2800 @Override 2801 public void run() { 2802 if (mSelectedRoute != null) { 2803 mSelectedRoute.requestSetVolume(volume); 2804 } 2805 } 2806 }); 2807 } 2808 2809 @Override 2810 public void onAdjustVolume(final int direction) { 2811 mCallbackHandler.post(new Runnable() { 2812 @Override 2813 public void run() { 2814 if (mSelectedRoute != null) { 2815 mSelectedRoute.requestUpdateVolume(direction); 2816 } 2817 } 2818 }); 2819 } 2820 }; 2821 mMsCompat.setPlaybackToRemote(mVpCompat); 2822 } 2823 } 2824 2825 public void clearVolumeHandling() { 2826 mMsCompat.setPlaybackToLocal(mPlaybackInfo.playbackStream); 2827 mVpCompat = null; 2828 } 2829 2830 public MediaSessionCompat.Token getToken() { 2831 return mMsCompat.getSessionToken(); 2832 } 2833 } 2834 2835 private final class RemoteControlClientRecord 2836 implements RemoteControlClientCompat.VolumeCallback { 2837 private final RemoteControlClientCompat mRccCompat; 2838 private boolean mDisconnected; 2839 2840 public RemoteControlClientRecord(Object rcc) { 2841 mRccCompat = RemoteControlClientCompat.obtain(mApplicationContext, rcc); 2842 mRccCompat.setVolumeCallback(this); 2843 updatePlaybackInfo(); 2844 } 2845 2846 public Object getRemoteControlClient() { 2847 return mRccCompat.getRemoteControlClient(); 2848 } 2849 2850 public void disconnect() { 2851 mDisconnected = true; 2852 mRccCompat.setVolumeCallback(null); 2853 } 2854 2855 public void updatePlaybackInfo() { 2856 mRccCompat.setPlaybackInfo(mPlaybackInfo); 2857 } 2858 2859 @Override 2860 public void onVolumeSetRequest(int volume) { 2861 if (!mDisconnected && mSelectedRoute != null) { 2862 mSelectedRoute.requestSetVolume(volume); 2863 } 2864 } 2865 2866 @Override 2867 public void onVolumeUpdateRequest(int direction) { 2868 if (!mDisconnected && mSelectedRoute != null) { 2869 mSelectedRoute.requestUpdateVolume(direction); 2870 } 2871 } 2872 } 2873 2874 private final class CallbackHandler extends Handler { 2875 private final ArrayList<CallbackRecord> mTempCallbackRecords = 2876 new ArrayList<CallbackRecord>(); 2877 2878 private static final int MSG_TYPE_MASK = 0xff00; 2879 private static final int MSG_TYPE_ROUTE = 0x0100; 2880 private static final int MSG_TYPE_PROVIDER = 0x0200; 2881 2882 public static final int MSG_ROUTE_ADDED = MSG_TYPE_ROUTE | 1; 2883 public static final int MSG_ROUTE_REMOVED = MSG_TYPE_ROUTE | 2; 2884 public static final int MSG_ROUTE_CHANGED = MSG_TYPE_ROUTE | 3; 2885 public static final int MSG_ROUTE_VOLUME_CHANGED = MSG_TYPE_ROUTE | 4; 2886 public static final int MSG_ROUTE_PRESENTATION_DISPLAY_CHANGED = MSG_TYPE_ROUTE | 5; 2887 public static final int MSG_ROUTE_SELECTED = MSG_TYPE_ROUTE | 6; 2888 public static final int MSG_ROUTE_UNSELECTED = MSG_TYPE_ROUTE | 7; 2889 2890 public static final int MSG_PROVIDER_ADDED = MSG_TYPE_PROVIDER | 1; 2891 public static final int MSG_PROVIDER_REMOVED = MSG_TYPE_PROVIDER | 2; 2892 public static final int MSG_PROVIDER_CHANGED = MSG_TYPE_PROVIDER | 3; 2893 2894 CallbackHandler() { 2895 } 2896 2897 public void post(int msg, Object obj) { 2898 obtainMessage(msg, obj).sendToTarget(); 2899 } 2900 2901 public void post(int msg, Object obj, int arg) { 2902 Message message = obtainMessage(msg, obj); 2903 message.arg1 = arg; 2904 message.sendToTarget(); 2905 } 2906 2907 @Override 2908 public void handleMessage(Message msg) { 2909 final int what = msg.what; 2910 final Object obj = msg.obj; 2911 final int arg = msg.arg1; 2912 2913 if (what == MSG_ROUTE_CHANGED 2914 && getSelectedRoute().getId().equals(((RouteInfo) obj).getId())) { 2915 updateSelectedRouteIfNeeded(true); 2916 } 2917 2918 // Synchronize state with the system media router. 2919 syncWithSystemProvider(what, obj); 2920 2921 // Invoke all registered callbacks. 2922 // Build a list of callbacks before invoking them in case callbacks 2923 // are added or removed during dispatch. 2924 try { 2925 for (int i = mRouters.size(); --i >= 0; ) { 2926 MediaRouter router = mRouters.get(i).get(); 2927 if (router == null) { 2928 mRouters.remove(i); 2929 } else { 2930 mTempCallbackRecords.addAll(router.mCallbackRecords); 2931 } 2932 } 2933 2934 final int callbackCount = mTempCallbackRecords.size(); 2935 for (int i = 0; i < callbackCount; i++) { 2936 invokeCallback(mTempCallbackRecords.get(i), what, obj, arg); 2937 } 2938 } finally { 2939 mTempCallbackRecords.clear(); 2940 } 2941 } 2942 2943 private void syncWithSystemProvider(int what, Object obj) { 2944 switch (what) { 2945 case MSG_ROUTE_ADDED: 2946 mSystemProvider.onSyncRouteAdded((RouteInfo) obj); 2947 break; 2948 case MSG_ROUTE_REMOVED: 2949 mSystemProvider.onSyncRouteRemoved((RouteInfo) obj); 2950 break; 2951 case MSG_ROUTE_CHANGED: 2952 mSystemProvider.onSyncRouteChanged((RouteInfo) obj); 2953 break; 2954 case MSG_ROUTE_SELECTED: 2955 mSystemProvider.onSyncRouteSelected((RouteInfo) obj); 2956 break; 2957 } 2958 } 2959 2960 private void invokeCallback(CallbackRecord record, int what, Object obj, int arg) { 2961 final MediaRouter router = record.mRouter; 2962 final MediaRouter.Callback callback = record.mCallback; 2963 switch (what & MSG_TYPE_MASK) { 2964 case MSG_TYPE_ROUTE: { 2965 final RouteInfo route = (RouteInfo)obj; 2966 if (!record.filterRouteEvent(route)) { 2967 break; 2968 } 2969 switch (what) { 2970 case MSG_ROUTE_ADDED: 2971 callback.onRouteAdded(router, route); 2972 break; 2973 case MSG_ROUTE_REMOVED: 2974 callback.onRouteRemoved(router, route); 2975 break; 2976 case MSG_ROUTE_CHANGED: 2977 callback.onRouteChanged(router, route); 2978 break; 2979 case MSG_ROUTE_VOLUME_CHANGED: 2980 callback.onRouteVolumeChanged(router, route); 2981 break; 2982 case MSG_ROUTE_PRESENTATION_DISPLAY_CHANGED: 2983 callback.onRoutePresentationDisplayChanged(router, route); 2984 break; 2985 case MSG_ROUTE_SELECTED: 2986 callback.onRouteSelected(router, route); 2987 break; 2988 case MSG_ROUTE_UNSELECTED: 2989 callback.onRouteUnselected(router, route, arg); 2990 break; 2991 } 2992 break; 2993 } 2994 case MSG_TYPE_PROVIDER: { 2995 final ProviderInfo provider = (ProviderInfo)obj; 2996 switch (what) { 2997 case MSG_PROVIDER_ADDED: 2998 callback.onProviderAdded(router, provider); 2999 break; 3000 case MSG_PROVIDER_REMOVED: 3001 callback.onProviderRemoved(router, provider); 3002 break; 3003 case MSG_PROVIDER_CHANGED: 3004 callback.onProviderChanged(router, provider); 3005 break; 3006 } 3007 } 3008 } 3009 } 3010 } 3011 } 3012} 3013