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