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