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