MediaRouter.java revision b507e525a61ed761eecfc2eaaf19af7e8db5dca5
1/* 2 * Copyright (C) 2013 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package android.support.v7.media; 18 19import android.content.ContentResolver; 20import android.content.Context; 21import android.content.Intent; 22import android.content.IntentFilter; 23import android.content.pm.PackageManager.NameNotFoundException; 24import android.content.res.Resources; 25import android.graphics.drawable.Drawable; 26import android.os.Bundle; 27import android.os.Handler; 28import android.os.Looper; 29import android.os.Message; 30import android.support.v4.hardware.display.DisplayManagerCompat; 31import android.support.v7.media.MediaRouteProvider.RouteDescriptor; 32import android.support.v7.media.MediaRouteProvider.ProviderDescriptor; 33import android.support.v7.media.MediaRouteProvider.ProviderMetadata; 34import android.util.Log; 35import android.view.Display; 36 37import java.util.ArrayList; 38import java.util.Collections; 39import java.util.List; 40import java.util.WeakHashMap; 41import java.util.concurrent.CopyOnWriteArrayList; 42 43/** 44 * MediaRouter allows applications to control the routing of media channels 45 * and streams from the current device to external speakers and destination devices. 46 * <p> 47 * A MediaRouter instance is retrieved through {@link #getInstance}. Applications 48 * can query the media router about the currently selected route and its capabilities 49 * to determine how to send content to the route's destination. Applications can 50 * also {@link RouteInfo#sendControlRequest send control requests} to the route 51 * to ask the route's destination to perform certain remote control functions 52 * such as playing media. 53 * </p><p> 54 * See also {@link MediaRouteProvider} for information on how an application 55 * can publish new media routes to the media router. 56 * </p><p> 57 * The media router API is not thread-safe; all interactions with it must be 58 * done from the main thread of the process. 59 * </p> 60 */ 61public final class MediaRouter { 62 private static final String TAG = "MediaRouter"; 63 64 // Maintains global media router state for the process. 65 // This field is initialized in MediaRouter.getInstance() before any 66 // MediaRouter objects are instantiated so it is guaranteed to be 67 // valid whenever any instance method is invoked. 68 static GlobalMediaRouter sGlobal; 69 70 // Context-bound state of the media router. 71 final Context mContext; 72 final CopyOnWriteArrayList<Callback> mCallbacks = new CopyOnWriteArrayList<Callback>(); 73 74 MediaRouter(Context context) { 75 mContext = context; 76 } 77 78 /** 79 * Gets an instance of the media router service from the context. 80 */ 81 public static MediaRouter getInstance(Context context) { 82 if (context == null) { 83 throw new IllegalArgumentException("context must not be null"); 84 } 85 checkCallingThread(); 86 87 if (sGlobal == null) { 88 sGlobal = new GlobalMediaRouter(context.getApplicationContext()); 89 sGlobal.start(); 90 } 91 return sGlobal.getRouter(context); 92 } 93 94 /** 95 * Gets information about the {@link MediaRouter.RouteInfo routes} currently known to 96 * this media router. 97 */ 98 public List<RouteInfo> getRoutes() { 99 checkCallingThread(); 100 return sGlobal.getRoutes(); 101 } 102 103 /** 104 * Gets information about the {@link MediaRouter.ProviderInfo route providers} 105 * currently known to this media router. 106 */ 107 public List<ProviderInfo> getProviders() { 108 checkCallingThread(); 109 return sGlobal.getProviders(); 110 } 111 112 /** 113 * Gets the default route for playing media content on the system. 114 * <p> 115 * The system always provides a default route. 116 * </p> 117 * 118 * @return The default route, which is guaranteed to never be null. 119 */ 120 public RouteInfo getDefaultRoute() { 121 checkCallingThread(); 122 return sGlobal.getDefaultRoute(); 123 } 124 125 /** 126 * Gets the currently selected route. 127 * <p> 128 * The application should examine the route's 129 * {@link RouteInfo#getControlFilters media control intent filters} to assess the 130 * capabilities of the route before attempting to use it. 131 * </p> 132 * 133 * <h3>Example</h3> 134 * <pre> 135 * public boolean playMovie() { 136 * MediaRouter mediaRouter = MediaRouter.getInstance(context); 137 * MediaRouter.RouteInfo route = mediaRouter.getSelectedRoute(); 138 * 139 * // First try using the remote playback interface, if supported. 140 * if (route.supportsControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)) { 141 * // The route supports remote playback. 142 * // Try to send it the Uri of the movie to play. 143 * Intent intent = new Intent(MediaControlIntent.ACTION_PLAY); 144 * intent.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK); 145 * intent.setDataAndType("http://example.com/videos/movie.mp4", "video/mp4"); 146 * if (route.supportsControlRequest(intent)) { 147 * route.sendControlRequest(intent, null); 148 * return true; // sent the request to play the movie 149 * } 150 * } 151 * 152 * // If remote playback was not possible, then play locally. 153 * if (route.supportsControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO)) { 154 * // The route supports live video streaming. 155 * // Prepare to play content locally in a window or in a presentation. 156 * return playMovieInWindow(); 157 * } 158 * 159 * // Neither interface is supported, so we can't play the movie to this route. 160 * return false; 161 * } 162 * </pre> 163 * 164 * @return The selected route, which is guaranteed to never be null. 165 * 166 * @see RouteInfo#getControlFilters 167 * @see RouteInfo#supportsControlCategory 168 * @see RouteInfo#supportsControlRequest 169 */ 170 public RouteInfo getSelectedRoute() { 171 checkCallingThread(); 172 return sGlobal.getSelectedRoute(); 173 } 174 175 /** 176 * Selects the specified route. 177 * 178 * @param route The route to select. 179 */ 180 public void selectRoute(RouteInfo route) { 181 if (route == null) { 182 throw new IllegalArgumentException("route must not be null"); 183 } 184 checkCallingThread(); 185 186 sGlobal.selectRoute(route); 187 } 188 189 /** 190 * Adds a callback to listen to changes to media routes. 191 * 192 * @param callback The callback to add. 193 */ 194 public void addCallback(Callback callback) { 195 if (callback == null) { 196 throw new IllegalArgumentException("callback must not be null"); 197 } 198 checkCallingThread(); 199 200 if (!mCallbacks.contains(callback)) { 201 mCallbacks.add(callback); 202 } 203 } 204 205 /** 206 * Removes the specified callback. It will no longer receive information about 207 * changes to media routes. 208 * 209 * @param callback The callback to remove. 210 */ 211 public void removeCallback(Callback callback) { 212 if (callback == null) { 213 throw new IllegalArgumentException("callback must not be null"); 214 } 215 checkCallingThread(); 216 217 mCallbacks.remove(callback); 218 } 219 220 /** 221 * Registers a media route provider globally for this application process. 222 * 223 * @param providerInstance The media route provider instance to add. 224 * 225 * @see MediaRouteProvider 226 */ 227 public void addProvider(MediaRouteProvider providerInstance) { 228 if (providerInstance == null) { 229 throw new IllegalArgumentException("providerInstance must not be null"); 230 } 231 checkCallingThread(); 232 233 sGlobal.addProvider(providerInstance); 234 } 235 236 /** 237 * Unregisters a media route provider globally for this application process. 238 * 239 * @param providerInstance The media route provider instance to remove. 240 * 241 * @see MediaRouteProvider 242 */ 243 public void removeProvider(MediaRouteProvider providerInstance) { 244 if (providerInstance == null) { 245 throw new IllegalArgumentException("providerInstance must not be null"); 246 } 247 checkCallingThread(); 248 249 sGlobal.removeProvider(providerInstance); 250 } 251 252 /** 253 * Ensures that calls into the media router are on the correct thread. 254 * It pays to be a little paranoid when global state invariants are at risk. 255 */ 256 static void checkCallingThread() { 257 if (Looper.myLooper() != Looper.getMainLooper()) { 258 throw new IllegalStateException("The media router service must only be " 259 + "accessed on the application's main thread."); 260 } 261 } 262 263 static <T> boolean equal(T a, T b) { 264 return a == b || (a != null && b != null && a.equals(b)); 265 } 266 267 /** 268 * Provides information about a media route. 269 * <p> 270 * Each media route has a list of {@link MediaControlIntent media control} 271 * {@link #getControlFilters intent filters} that describe the capabilities of the 272 * route and the manner in which it is used and controlled. 273 * </p> 274 */ 275 public static final class RouteInfo { 276 private final ProviderInfo mProvider; 277 private final String mDescriptorId; 278 private String mName; 279 private String mStatus; 280 private Drawable mIconDrawable; 281 private int mIconResource; 282 private boolean mEnabled; 283 private final ArrayList<IntentFilter> mControlFilters = new ArrayList<IntentFilter>(); 284 private int mPlaybackType; 285 private int mPlaybackStream; 286 private int mVolumeHandling; 287 private int mVolume; 288 private int mVolumeMax; 289 private Display mPresentationDisplay; 290 private int mPresentationDisplayId = -1; 291 private Bundle mExtras; 292 private RouteDescriptor mDescriptor; 293 294 /** 295 * The default playback type, "local", indicating the presentation of the media 296 * is happening on the same device (e.g. a phone, a tablet) as where it is 297 * controlled from. 298 * 299 * @see #getPlaybackType 300 */ 301 public static final int PLAYBACK_TYPE_LOCAL = 0; 302 303 /** 304 * A playback type indicating the presentation of the media is happening on 305 * a different device (i.e. the remote device) than where it is controlled from. 306 * 307 * @see #getPlaybackType 308 */ 309 public static final int PLAYBACK_TYPE_REMOTE = 1; 310 311 /** 312 * Playback information indicating the playback volume is fixed, i.e. it cannot be 313 * controlled from this object. An example of fixed playback volume is a remote player, 314 * playing over HDMI where the user prefers to control the volume on the HDMI sink, rather 315 * than attenuate at the source. 316 * 317 * @see #getVolumeHandling 318 */ 319 public static final int PLAYBACK_VOLUME_FIXED = 0; 320 321 /** 322 * Playback information indicating the playback volume is variable and can be controlled 323 * from this object. 324 * 325 * @see #getVolumeHandling 326 */ 327 public static final int PLAYBACK_VOLUME_VARIABLE = 1; 328 329 static final int CHANGE_GENERAL = 1 << 0; 330 static final int CHANGE_VOLUME = 1 << 1; 331 static final int CHANGE_PRESENTATION_DISPLAY = 1 << 2; 332 333 RouteInfo(ProviderInfo provider, String descriptorId) { 334 mProvider = provider; 335 mDescriptorId = descriptorId; 336 } 337 338 /** 339 * Gets information about the provider of this media route. 340 */ 341 public ProviderInfo getProvider() { 342 return mProvider; 343 } 344 345 /** 346 * Gets the name of this route. 347 * 348 * @return The user-friendly name of a media route. This is the string presented 349 * to users who may select this as the active route. 350 */ 351 public String getName() { 352 return mName; 353 } 354 355 /** 356 * Gets the status of this route. 357 * 358 * @return The user-friendly status for a media route. This may include a description 359 * of the currently playing media, if available. 360 */ 361 public String getStatus() { 362 return mStatus; 363 } 364 365 /** 366 * Get the icon representing this route. 367 * This icon will be used in picker UIs if available. 368 * 369 * @return The icon representing this route or null if no icon is available. 370 */ 371 public Drawable getIconDrawable() { 372 checkCallingThread(); 373 if (mIconDrawable == null) { 374 if (mIconResource != 0) { 375 Resources resources = mProvider.getResources(); 376 if (resources != null) { 377 try { 378 mIconDrawable = resources.getDrawable(mIconResource); 379 } catch (Resources.NotFoundException ex) { 380 Log.w(TAG, "Unable to load media route icon drawable resource " 381 + "from provider.", ex); 382 } 383 } 384 } 385 } 386 return mIconDrawable; 387 } 388 389 /** 390 * Returns true if this route is enabled and may be selected. 391 * 392 * @return true if this route is enabled and may be selected. 393 */ 394 public boolean isEnabled() { 395 return mEnabled; 396 } 397 398 /** 399 * Returns true if this route is currently selected. 400 * 401 * @return true if this route is currently selected. 402 * 403 * @see MediaRouter#getSelectedRoute 404 */ 405 public boolean isSelected() { 406 checkCallingThread(); 407 return sGlobal.getSelectedRoute() == this; 408 } 409 410 /** 411 * Returns true if this route is the default route. 412 * 413 * @return true if this route is the default route. 414 * 415 * @see MediaRouter#getDefaultRoute 416 */ 417 public boolean isDefault() { 418 checkCallingThread(); 419 return sGlobal.getDefaultRoute() == this; 420 } 421 422 /** 423 * Gets a list of {@link MediaControlIntent media control intent} filters that 424 * describe the capabilities of this route and the media control actions that 425 * it supports. 426 * 427 * @return A list of intent filters that specifies the media control intents that 428 * this route supports. 429 * 430 * @see MediaControlIntent 431 * @see #supportsControlCategory 432 * @see #supportsControlRequest 433 */ 434 public List<IntentFilter> getControlFilters() { 435 return mControlFilters; 436 } 437 438 /** 439 * Returns true if the route supports the specified 440 * {@link MediaControlIntent media control} category. 441 * <p> 442 * Media control categories describe the capabilities of this route 443 * such as whether it supports live audio streaming or remote playback. 444 * </p> 445 * 446 * @param category A {@link MediaControlIntent media control} category 447 * such as {@link MediaControlIntent#CATEGORY_LIVE_AUDIO}, 448 * {@link MediaControlIntent#CATEGORY_LIVE_VIDEO}, 449 * {@link MediaControlIntent#CATEGORY_REMOTE_PLAYBACK}, or a provider-defined 450 * media control category. 451 * 452 * @see MediaControlIntent 453 * @see #getControlFilters 454 */ 455 public boolean supportsControlCategory(String category) { 456 if (category == null) { 457 throw new IllegalArgumentException("category must not be null"); 458 } 459 checkCallingThread(); 460 461 int count = mControlFilters.size(); 462 for (int i = 0; i < count; i++) { 463 if (mControlFilters.get(i).hasCategory(category)) { 464 return true; 465 } 466 } 467 return false; 468 } 469 470 /** 471 * Returns true if the route supports the specified 472 * {@link MediaControlIntent media control} request. 473 * <p> 474 * Media control requests are used to request the route to perform 475 * actions such as starting remote playback of a content stream. 476 * </p> 477 * 478 * @param intent A {@link MediaControlIntent media control intent}. 479 * @return True if the route can handle the specified intent. 480 * 481 * @see MediaControlIntent 482 * @see #getControlFilters 483 */ 484 public boolean supportsControlRequest(Intent intent) { 485 if (intent == null) { 486 throw new IllegalArgumentException("intent must not be null"); 487 } 488 checkCallingThread(); 489 490 ContentResolver contentResolver = sGlobal.getContentResolver(); 491 int count = mControlFilters.size(); 492 for (int i = 0; i < count; i++) { 493 if (mControlFilters.get(i).match(contentResolver, intent, true, TAG) >= 0) { 494 return true; 495 } 496 } 497 return false; 498 } 499 500 /** 501 * Sends a {@link MediaControlIntent media control} request to be performed 502 * asynchronously by the route's destination. 503 * <p> 504 * Media control requests are used to request the route to perform 505 * actions such as starting remote playback of a content stream. 506 * </p><p> 507 * This function may only be called on a selected route. Control requests 508 * sent to unselected routes will fail. 509 * </p> 510 * 511 * @param intent A {@link MediaControlIntent media control intent}. 512 * @param callback A {@link ControlRequestCallback} to invoke with the result 513 * of the request, or null if no result is required. 514 * 515 * @see MediaControlIntent 516 */ 517 public void sendControlRequest(Intent intent, ControlRequestCallback callback) { 518 if (intent == null) { 519 throw new IllegalArgumentException("intent must not be null"); 520 } 521 checkCallingThread(); 522 523 sGlobal.sendControlRequest(this, intent, callback); 524 } 525 526 /** 527 * Gets the type of playback associated with this route. 528 * 529 * @return The type of playback associated with this route: {@link #PLAYBACK_TYPE_LOCAL} 530 * or {@link #PLAYBACK_TYPE_REMOTE}. 531 */ 532 public int getPlaybackType() { 533 return mPlaybackType; 534 } 535 536 /** 537 * Gets the the stream over which the playback associated with this route is performed. 538 * 539 * @return The stream over which the playback associated with this route is performed. 540 */ 541 public int getPlaybackStream() { 542 return mPlaybackStream; 543 } 544 545 /** 546 * Gets information about how volume is handled on the route. 547 * 548 * @return How volume is handled on the route: {@link #PLAYBACK_VOLUME_FIXED} 549 * or {@link #PLAYBACK_VOLUME_VARIABLE}. 550 */ 551 public int getVolumeHandling() { 552 return mVolumeHandling; 553 } 554 555 /** 556 * Gets the current volume for this route. Depending on the route, this may only 557 * be valid if the route is currently selected. 558 * 559 * @return The volume at which the playback associated with this route is performed. 560 */ 561 public int getVolume() { 562 return mVolume; 563 } 564 565 /** 566 * Gets the maximum volume at which the playback associated with this route is performed. 567 * 568 * @return The maximum volume at which the playback associated with 569 * this route is performed. 570 */ 571 public int getVolumeMax() { 572 return mVolumeMax; 573 } 574 575 /** 576 * Requests a volume change for this route asynchronously. 577 * <p> 578 * This function may only be called on a selected route. It will have 579 * no effect if the route is currently unselected. 580 * </p> 581 * 582 * @param volume The new volume value between 0 and {@link #getVolumeMax}. 583 */ 584 public void requestSetVolume(int volume) { 585 checkCallingThread(); 586 sGlobal.requestSetVolume(this, Math.min(mVolumeMax, Math.max(0, volume))); 587 } 588 589 /** 590 * Requests an incremental volume update for this route asynchronously. 591 * <p> 592 * This function may only be called on a selected route. It will have 593 * no effect if the route is currently unselected. 594 * </p> 595 * 596 * @param delta The delta to add to the current volume. 597 */ 598 public void requestUpdateVolume(int delta) { 599 checkCallingThread(); 600 if (delta != 0) { 601 sGlobal.requestUpdateVolume(this, delta); 602 } 603 } 604 605 /** 606 * Gets the {@link Display} that should be used by the application to show 607 * a {@link android.app.Presentation} on an external display when this route is selected. 608 * Depending on the route, this may only be valid if the route is currently 609 * selected. 610 * <p> 611 * The preferred presentation display may change independently of the route 612 * being selected or unselected. For example, the presentation display 613 * of the default system route may change when an external HDMI display is connected 614 * or disconnected even though the route itself has not changed. 615 * </p><p> 616 * This method may return null if there is no external display associated with 617 * the route or if the display is not ready to show UI yet. 618 * </p><p> 619 * The application should listen for changes to the presentation display 620 * using the {@link Callback#onRoutePresentationDisplayChanged} callback and 621 * show or dismiss its {@link android.app.Presentation} accordingly when the display 622 * becomes available or is removed. 623 * </p><p> 624 * This method only makes sense for 625 * {@link MediaControlIntent#CATEGORY_LIVE_VIDEO live video} routes. 626 * </p> 627 * 628 * @return The preferred presentation display to use when this route is 629 * selected or null if none. 630 * 631 * @see MediaControlIntent#CATEGORY_LIVE_VIDEO 632 * @see android.app.Presentation 633 */ 634 public Display getPresentationDisplay() { 635 checkCallingThread(); 636 if (mPresentationDisplayId >= 0 && mPresentationDisplay == null) { 637 mPresentationDisplay = sGlobal.getDisplay(mPresentationDisplayId); 638 } 639 return mPresentationDisplay; 640 } 641 642 /** 643 * Gets a collection of extra properties about this route that were supplied 644 * by its media route provider, or null if none. 645 */ 646 public Bundle getExtras() { 647 return mExtras; 648 } 649 650 /** 651 * Selects this media route. 652 */ 653 public void select() { 654 checkCallingThread(); 655 sGlobal.selectRoute(this); 656 } 657 658 @Override 659 public String toString() { 660 return "MediaRouter.RouteInfo{ name=" + mName 661 + ", status=" + mStatus 662 + ", enabled=" + mEnabled 663 + ", playbackType=" + mPlaybackType 664 + ", playbackStream=" + mPlaybackStream 665 + ", volumeHandling=" + mVolumeHandling 666 + ", volume=" + mVolume 667 + ", volumeMax=" + mVolumeMax 668 + ", presentationDisplayId=" + mPresentationDisplayId 669 + ", extras=" + mExtras 670 + ", providerPackageName=" + mProvider.getPackageName() 671 + " }"; 672 } 673 674 int updateDescriptor(RouteDescriptor descriptor) { 675 int changes = 0; 676 if (mDescriptor != descriptor) { 677 mDescriptor = descriptor; 678 if (descriptor != null) { 679 if (!equal(mName, descriptor.getName())) { 680 mName = descriptor.getName(); 681 changes |= CHANGE_GENERAL; 682 } 683 if (!equal(mStatus, descriptor.getStatus())) { 684 mStatus = descriptor.getStatus(); 685 changes |= CHANGE_GENERAL; 686 } 687 if (mIconResource != descriptor.getIconResource()) { 688 mIconResource = descriptor.getIconResource(); 689 mIconDrawable = null; 690 changes |= CHANGE_GENERAL; 691 } 692 if (mIconResource == 0 693 && mIconDrawable != descriptor.getIconDrawable()) { 694 mIconDrawable = descriptor.getIconDrawable(); 695 changes |= CHANGE_GENERAL; 696 } 697 if (mEnabled != descriptor.isEnabled()) { 698 mEnabled = descriptor.isEnabled(); 699 changes |= CHANGE_GENERAL; 700 } 701 IntentFilter[] descriptorControlFilters = descriptor.getControlFilters(); 702 if (!hasSameControlFilters(descriptorControlFilters)) { 703 mControlFilters.clear(); 704 for (IntentFilter f : descriptorControlFilters) { 705 mControlFilters.add(f); 706 } 707 changes |= CHANGE_GENERAL; 708 } 709 if (mPlaybackType != descriptor.getPlaybackType()) { 710 mPlaybackType = descriptor.getPlaybackType(); 711 changes |= CHANGE_GENERAL; 712 } 713 if (mPlaybackStream != descriptor.getPlaybackStream()) { 714 mPlaybackStream = descriptor.getPlaybackStream(); 715 changes |= CHANGE_GENERAL; 716 } 717 if (mVolumeHandling != descriptor.getVolumeHandling()) { 718 mVolumeHandling = descriptor.getVolumeHandling(); 719 changes |= CHANGE_GENERAL | CHANGE_VOLUME; 720 } 721 if (mVolume != descriptor.getVolume()) { 722 mVolume = descriptor.getVolume(); 723 changes |= CHANGE_GENERAL | CHANGE_VOLUME; 724 } 725 if (mVolumeMax != descriptor.getVolumeMax()) { 726 mVolumeMax = descriptor.getVolumeMax(); 727 changes |= CHANGE_GENERAL | CHANGE_VOLUME; 728 } 729 if (mPresentationDisplayId != descriptor.getPresentationDisplayId()) { 730 mPresentationDisplayId = descriptor.getPresentationDisplayId(); 731 mPresentationDisplay = null; 732 changes |= CHANGE_GENERAL | CHANGE_PRESENTATION_DISPLAY; 733 } 734 if (!equal(mExtras, descriptor.getExtras())) { 735 mExtras = descriptor.getExtras(); 736 changes |= CHANGE_GENERAL; 737 } 738 } 739 } 740 return changes; 741 } 742 743 boolean hasSameControlFilters(IntentFilter[] controlFilters) { 744 final int count = mControlFilters.size(); 745 if (count != controlFilters.length) { 746 return false; 747 } 748 for (int i = 0; i < count; i++) { 749 if (!mControlFilters.get(i).equals(controlFilters[i])) { 750 return false; 751 } 752 } 753 return true; 754 } 755 756 String getDescriptorId() { 757 return mDescriptorId; 758 } 759 760 MediaRouteProvider getProviderInstance() { 761 return mProvider.getProviderInstance(); 762 } 763 } 764 765 /** 766 * Provides information about a media route provider. 767 * <p> 768 * This object may be used to determine which media route provider has 769 * published a particular route. 770 * </p> 771 */ 772 public static final class ProviderInfo { 773 private final MediaRouteProvider mProviderInstance; 774 private final ArrayList<RouteInfo> mRoutes = new ArrayList<RouteInfo>(); 775 776 private final ProviderMetadata mMetadata; 777 private ProviderDescriptor mDescriptor; 778 private Resources mResources; 779 private boolean mResourcesNotAvailable; 780 781 ProviderInfo(MediaRouteProvider provider) { 782 mProviderInstance = provider; 783 mMetadata = provider.getMetadata(); 784 } 785 786 /** 787 * Gets the provider's underlying {@link MediaRouteProvider} instance. 788 */ 789 public MediaRouteProvider getProviderInstance() { 790 checkCallingThread(); 791 return mProviderInstance; 792 } 793 794 /** 795 * Gets the package name of the media route provider service. 796 */ 797 public String getPackageName() { 798 return mMetadata.getPackageName(); 799 } 800 801 /** 802 * Gets the {@link MediaRouter.RouteInfo routes} published by this route provider. 803 */ 804 public List<RouteInfo> getRoutes() { 805 checkCallingThread(); 806 return mRoutes; 807 } 808 809 Resources getResources() { 810 if (mResources == null && !mResourcesNotAvailable) { 811 String packageName = getPackageName(); 812 Context context = sGlobal.getProviderContext(packageName); 813 if (context != null) { 814 mResources = context.getResources(); 815 } else { 816 Log.w(TAG, "Unable to obtain resources for route provider package: " 817 + packageName); 818 mResourcesNotAvailable = true; 819 } 820 } 821 return mResources; 822 } 823 824 boolean updateDescriptor(ProviderDescriptor descriptor) { 825 if (mDescriptor != descriptor) { 826 mDescriptor = descriptor; 827 return true; 828 } 829 return false; 830 } 831 832 int findRouteByDescriptorId(String id) { 833 final int count = mRoutes.size(); 834 for (int i = 0; i < count; i++) { 835 if (mRoutes.get(i).mDescriptorId.equals(id)) { 836 return i; 837 } 838 } 839 return -1; 840 } 841 842 @Override 843 public String toString() { 844 return "MediaRouter.RouteProviderInfo{ packageName=" + getPackageName() 845 + " }"; 846 } 847 } 848 849 /** 850 * Interface for receiving events about media routing changes. 851 * All methods of this interface will be called from the application's main thread. 852 * <p> 853 * A Callback will only receive events relevant to routes that the callback 854 * was registered for. 855 * </p> 856 * 857 * @see MediaRouter#addCallback(Callback) 858 * @see MediaRouter#removeCallback(Callback) 859 */ 860 public static abstract class Callback { 861 /** 862 * Called when the supplied media route becomes selected as the active route. 863 * 864 * @param router The media router reporting the event. 865 * @param route The route that has been selected. 866 */ 867 public void onRouteSelected(MediaRouter router, RouteInfo route) { 868 } 869 870 /** 871 * Called when the supplied media route becomes unselected as the active route. 872 * 873 * @param router The media router reporting the event. 874 * @param route The route that has been unselected. 875 */ 876 public void onRouteUnselected(MediaRouter router, RouteInfo route) { 877 } 878 879 /** 880 * Called when a media route has been added. 881 * 882 * @param router The media router reporting the event. 883 * @param route The route that has become available for use. 884 */ 885 public void onRouteAdded(MediaRouter router, RouteInfo route) { 886 } 887 888 /** 889 * Called when a media route has been removed. 890 * 891 * @param router The media router reporting the event. 892 * @param route The route that has been removed from availability. 893 */ 894 public void onRouteRemoved(MediaRouter router, RouteInfo route) { 895 } 896 897 /** 898 * Called when a property of the indicated media route has changed. 899 * 900 * @param router The media router reporting the event. 901 * @param route The route that was changed. 902 */ 903 public void onRouteChanged(MediaRouter router, RouteInfo route) { 904 } 905 906 /** 907 * Called when a media route's volume changes. 908 * 909 * @param router The media router reporting the event. 910 * @param route The route whose volume changed. 911 */ 912 public void onRouteVolumeChanged(MediaRouter router, RouteInfo route) { 913 } 914 915 /** 916 * Called when a media route's presentation display changes. 917 * <p> 918 * This method is called whenever the route's presentation display becomes 919 * available, is removed or has changes to some of its properties (such as its size). 920 * </p> 921 * 922 * @param router The media router reporting the event. 923 * @param route The route whose presentation display changed. 924 * 925 * @see RouteInfo#getPresentationDisplay() 926 */ 927 public void onRoutePresentationDisplayChanged(MediaRouter router, RouteInfo route) { 928 } 929 930 /** 931 * Called when a media route provider has been added. 932 * 933 * @param router The media router reporting the event. 934 * @param provider The provider that has become available for use. 935 */ 936 public void onProviderAdded(MediaRouter router, ProviderInfo provider) { 937 } 938 939 /** 940 * Called when a media route provider has been removed. 941 * 942 * @param router The media router reporting the event. 943 * @param provider The provider that has been removed from availability. 944 */ 945 public void onProviderRemoved(MediaRouter router, ProviderInfo provider) { 946 } 947 } 948 949 /** 950 * Callback which is invoked with the result of a media control request. 951 * 952 * @see RouteInfo#sendControlRequest 953 */ 954 public static abstract class ControlRequestCallback { 955 /** 956 * Result code: The media control action succeeded. 957 */ 958 public static final int REQUEST_SUCCEEDED = 0; 959 960 /** 961 * Result code: The media control action failed. 962 */ 963 public static final int REQUEST_FAILED = -1; 964 965 /** 966 * Called with the result of the media control request. 967 * 968 * @param result The result code: {@link #REQUEST_SUCCEEDED}, or {@link #REQUEST_FAILED}. 969 * @param data Additional result data. Contents depend on the media control action. 970 */ 971 public void onResult(int result, Bundle data) { 972 } 973 } 974 975 /** 976 * Global state for the media router. 977 * <p> 978 * Media routes and media route providers are global to the process; their 979 * state and the bulk of the media router implementation lives here. 980 * </p> 981 */ 982 private static final class GlobalMediaRouter implements SystemMediaRouteProvider.SyncCallback { 983 private final Context mApplicationContext; 984 private final MediaRouter mApplicationRouter; 985 private final WeakHashMap<Context, MediaRouter> mRouters = 986 new WeakHashMap<Context, MediaRouter>(); 987 private final ArrayList<RouteInfo> mRoutes = new ArrayList<RouteInfo>(); 988 private final ArrayList<ProviderInfo> mProviders = 989 new ArrayList<ProviderInfo>(); 990 private final ProviderCallback mProviderCallback = new ProviderCallback(); 991 private final CallbackHandler mCallbackHandler = new CallbackHandler(); 992 private final DisplayManagerCompat mDisplayManager; 993 private final SystemMediaRouteProvider mSystemProvider; 994 995 private RegisteredMediaRouteProviderWatcher mRegisteredProviderWatcher; 996 private RouteInfo mDefaultRoute; 997 private RouteInfo mSelectedRoute; 998 private MediaRouteProvider.RouteController mSelectedRouteController; 999 1000 GlobalMediaRouter(Context applicationContext) { 1001 mApplicationContext = applicationContext; 1002 mDisplayManager = DisplayManagerCompat.getInstance(applicationContext); 1003 mApplicationRouter = getRouter(applicationContext); 1004 1005 // Add the system media route provider for interoperating with 1006 // the framework media router. This one is special and receives 1007 // synchronization messages from the media router. 1008 mSystemProvider = SystemMediaRouteProvider.obtain(applicationContext, this); 1009 addProvider(mSystemProvider); 1010 } 1011 1012 public void start() { 1013 // Start watching for routes published by registered media route 1014 // provider services. 1015 mRegisteredProviderWatcher = new RegisteredMediaRouteProviderWatcher( 1016 mApplicationContext, mApplicationRouter); 1017 mRegisteredProviderWatcher.start(); 1018 } 1019 1020 public MediaRouter getRouter(Context context) { 1021 MediaRouter router = mRouters.get(context); 1022 if (router == null) { 1023 router = new MediaRouter(context); 1024 mRouters.put(context, router); 1025 } 1026 return router; 1027 } 1028 1029 public ContentResolver getContentResolver() { 1030 return mApplicationContext.getContentResolver(); 1031 } 1032 1033 public Context getProviderContext(String packageName) { 1034 if (packageName.equals(SystemMediaRouteProvider.PACKAGE_NAME)) { 1035 return mApplicationContext; 1036 } 1037 try { 1038 return mApplicationContext.createPackageContext( 1039 packageName, Context.CONTEXT_RESTRICTED); 1040 } catch (NameNotFoundException ex) { 1041 return null; 1042 } 1043 } 1044 1045 public Display getDisplay(int displayId) { 1046 return mDisplayManager.getDisplay(displayId); 1047 } 1048 1049 public void sendControlRequest(RouteInfo route, 1050 Intent intent, ControlRequestCallback callback) { 1051 if (route == mSelectedRoute && mSelectedRouteController != null) { 1052 if (mSelectedRouteController.sendControlRequest(intent, callback)) { 1053 return; 1054 } 1055 } 1056 if (callback != null) { 1057 callback.onResult(ControlRequestCallback.REQUEST_FAILED, null); 1058 } 1059 } 1060 1061 public void requestSetVolume(RouteInfo route, int volume) { 1062 if (route == mSelectedRoute && mSelectedRouteController != null) { 1063 mSelectedRouteController.setVolume(volume); 1064 } 1065 } 1066 1067 public void requestUpdateVolume(RouteInfo route, int delta) { 1068 if (route == mSelectedRoute && mSelectedRouteController != null) { 1069 mSelectedRouteController.updateVolume(delta); 1070 } 1071 } 1072 1073 public List<RouteInfo> getRoutes() { 1074 return mRoutes; 1075 } 1076 1077 public List<ProviderInfo> getProviders() { 1078 return mProviders; 1079 } 1080 1081 public RouteInfo getDefaultRoute() { 1082 if (mDefaultRoute == null) { 1083 // This should never happen once the media router has been fully 1084 // initialized but it is good to check for the error in case there 1085 // is a bug in provider initialization. 1086 throw new IllegalStateException("There is no default route. " 1087 + "The media router has not yet been fully initialized."); 1088 } 1089 return mDefaultRoute; 1090 } 1091 1092 public RouteInfo getSelectedRoute() { 1093 if (mSelectedRoute == null) { 1094 // This should never happen once the media router has been fully 1095 // initialized but it is good to check for the error in case there 1096 // is a bug in provider initialization. 1097 throw new IllegalStateException("There is no currently selected route. " 1098 + "The media router has not yet been fully initialized."); 1099 } 1100 return mSelectedRoute; 1101 } 1102 1103 public void selectRoute(RouteInfo route) { 1104 if (!mRoutes.contains(route)) { 1105 Log.w(TAG, "Ignoring attempt to select removed route: " + route); 1106 return; 1107 } 1108 if (!route.mEnabled) { 1109 Log.w(TAG, "Ignoring attempt to select disabled route: " + route); 1110 return; 1111 } 1112 1113 setSelectedRouteInternal(route); 1114 } 1115 1116 public void addProvider(MediaRouteProvider providerInstance) { 1117 int index = findProviderInfo(providerInstance); 1118 if (index < 0) { 1119 // 1. Add the provider to the list. 1120 ProviderInfo provider = new ProviderInfo(providerInstance); 1121 mProviders.add(provider); 1122 mCallbackHandler.post(CallbackHandler.MSG_PROVIDER_ADDED, provider); 1123 // 2. Create the provider's contents. 1124 updateProviderContents(provider, providerInstance.getDescriptor()); 1125 // 3. Register the provider callback. 1126 providerInstance.addCallback(mProviderCallback); 1127 } 1128 } 1129 1130 public void removeProvider(MediaRouteProvider providerInstance) { 1131 int index = findProviderInfo(providerInstance); 1132 if (index >= 0) { 1133 // 1. Unregister the provider callback. 1134 providerInstance.removeCallback(mProviderCallback); 1135 // 2. Delete the provider's contents. 1136 ProviderInfo provider = mProviders.get(index); 1137 updateProviderContents(provider, null); 1138 // 3. Remove the provider from the list. 1139 mCallbackHandler.post(CallbackHandler.MSG_PROVIDER_REMOVED, provider); 1140 mProviders.remove(index); 1141 } 1142 } 1143 1144 private void updateProviderDescriptor(MediaRouteProvider providerInstance, 1145 ProviderDescriptor descriptor) { 1146 int index = findProviderInfo(providerInstance); 1147 if (index >= 0) { 1148 // Update the provider's contents. 1149 ProviderInfo provider = mProviders.get(index); 1150 updateProviderContents(provider, descriptor); 1151 } 1152 } 1153 1154 private int findProviderInfo(MediaRouteProvider providerInstance) { 1155 final int count = mProviders.size(); 1156 for (int i = 0; i < count; i++) { 1157 if (mProviders.get(i).mProviderInstance == providerInstance) { 1158 return i; 1159 } 1160 } 1161 return -1; 1162 } 1163 1164 private void updateProviderContents(ProviderInfo provider, 1165 ProviderDescriptor providerDescriptor) { 1166 if (provider.updateDescriptor(providerDescriptor)) { 1167 // Update all existing routes and reorder them to match 1168 // the order of their descriptors. 1169 int targetIndex = 0; 1170 if (providerDescriptor != null) { 1171 if (providerDescriptor.isValid()) { 1172 final RouteDescriptor[] routeDescriptors = providerDescriptor.getRoutes(); 1173 for (int i = 0; i < routeDescriptors.length; i++) { 1174 final RouteDescriptor routeDescriptor = routeDescriptors[i]; 1175 final String id = routeDescriptor.getId(); 1176 final int sourceIndex = provider.findRouteByDescriptorId(id); 1177 if (sourceIndex < 0) { 1178 // 1. Add the route to the list. 1179 RouteInfo route = new RouteInfo(provider, id); 1180 provider.mRoutes.add(targetIndex++, route); 1181 mRoutes.add(route); 1182 // 2. Create the route's contents. 1183 route.updateDescriptor(routeDescriptor); 1184 // 3. Notify clients about addition. 1185 mCallbackHandler.post(CallbackHandler.MSG_ROUTE_ADDED, route); 1186 } else if (sourceIndex < targetIndex) { 1187 Log.w(TAG, "Ignoring route descriptor with duplicate id: " 1188 + routeDescriptor); 1189 } else { 1190 // 1. Reorder the route within the list. 1191 RouteInfo route = provider.mRoutes.get(sourceIndex); 1192 Collections.swap(provider.mRoutes, 1193 sourceIndex, targetIndex++); 1194 // 2. Update the route's contents. 1195 int changes = route.updateDescriptor(routeDescriptor); 1196 // 3. Unselect route if needed before notifying about changes. 1197 unselectRouteIfNeeded(route); 1198 // 4. Notify clients about changes. 1199 if ((changes & RouteInfo.CHANGE_GENERAL) != 0) { 1200 mCallbackHandler.post( 1201 CallbackHandler.MSG_ROUTE_CHANGED, route); 1202 } 1203 if ((changes & RouteInfo.CHANGE_VOLUME) != 0) { 1204 mCallbackHandler.post( 1205 CallbackHandler.MSG_ROUTE_VOLUME_CHANGED, route); 1206 } 1207 if ((changes & RouteInfo.CHANGE_PRESENTATION_DISPLAY) != 0) { 1208 mCallbackHandler.post(CallbackHandler. 1209 MSG_ROUTE_PRESENTATION_DISPLAY_CHANGED, route); 1210 } 1211 } 1212 } 1213 } else { 1214 Log.w(TAG, "Ignoring invalid provider descriptor: " + providerDescriptor); 1215 } 1216 } 1217 1218 // Dispose all remaining routes that do not have matching descriptors. 1219 for (int i = provider.mRoutes.size() - 1; i >= targetIndex; i--) { 1220 // 1. Delete the route's contents. 1221 RouteInfo route = provider.mRoutes.get(i); 1222 route.updateDescriptor(null); 1223 // 2. Remove the route from the list. 1224 mRoutes.remove(provider); 1225 provider.mRoutes.remove(i); 1226 // 3. Unselect route if needed before notifying about removal. 1227 unselectRouteIfNeeded(route); 1228 // 4. Notify clients about removal. 1229 mCallbackHandler.post(CallbackHandler.MSG_ROUTE_REMOVED, route); 1230 } 1231 1232 // Choose a new selected route if needed. 1233 selectRouteIfNeeded(); 1234 } 1235 } 1236 1237 private void unselectRouteIfNeeded(RouteInfo route) { 1238 if (mDefaultRoute == route && !isRouteSelectable(route)) { 1239 Log.i(TAG, "Choosing a new default route because the current one " 1240 + "is no longer selectable: " + route); 1241 mDefaultRoute = null; 1242 } 1243 if (mSelectedRoute == route && !isRouteSelectable(route)) { 1244 Log.i(TAG, "Choosing a new selected route because the current one " 1245 + "is no longer selectable: " + route); 1246 setSelectedRouteInternal(null); 1247 } 1248 } 1249 1250 private void selectRouteIfNeeded() { 1251 if (mDefaultRoute == null && !mRoutes.isEmpty()) { 1252 for (RouteInfo route : mRoutes) { 1253 if (isSystemDefaultRoute(route) && isRouteSelectable(route)) { 1254 mDefaultRoute = route; 1255 break; 1256 } 1257 } 1258 } 1259 if (mSelectedRoute == null) { 1260 setSelectedRouteInternal(mDefaultRoute); 1261 } 1262 } 1263 1264 private boolean isRouteSelectable(RouteInfo route) { 1265 // This tests whether the route is still valid and enabled. 1266 // The route descriptor field is set to null when the route is removed. 1267 return route.mDescriptor != null && route.mEnabled; 1268 } 1269 1270 private boolean isSystemDefaultRoute(RouteInfo route) { 1271 return route.getProviderInstance() == mSystemProvider 1272 && route.mDescriptorId.equals( 1273 SystemMediaRouteProvider.DEFAULT_ROUTE_ID); 1274 } 1275 1276 private void setSelectedRouteInternal(RouteInfo route) { 1277 if (mSelectedRoute != route) { 1278 if (mSelectedRoute != null) { 1279 mCallbackHandler.post(CallbackHandler.MSG_ROUTE_UNSELECTED, mSelectedRoute); 1280 if (mSelectedRouteController != null) { 1281 mSelectedRouteController.unselect(); 1282 mSelectedRouteController.release(); 1283 mSelectedRouteController = null; 1284 } 1285 } 1286 1287 mSelectedRoute = route; 1288 1289 if (mSelectedRoute != null) { 1290 mSelectedRouteController = route.getProviderInstance().onCreateRouteController( 1291 route.mDescriptorId); 1292 if (mSelectedRouteController != null) { 1293 mSelectedRouteController.select(); 1294 } 1295 mCallbackHandler.post(CallbackHandler.MSG_ROUTE_SELECTED, mSelectedRoute); 1296 } 1297 } 1298 } 1299 1300 @Override 1301 public RouteInfo getSystemRouteByDescriptorId(String id) { 1302 int providerIndex = findProviderInfo(mSystemProvider); 1303 if (providerIndex >= 0) { 1304 ProviderInfo provider = mProviders.get(providerIndex); 1305 int routeIndex = provider.findRouteByDescriptorId(id); 1306 if (routeIndex >= 0) { 1307 return provider.mRoutes.get(routeIndex); 1308 } 1309 } 1310 return null; 1311 } 1312 1313 private final class ProviderCallback extends MediaRouteProvider.Callback { 1314 @Override 1315 public void onDescriptorChanged(MediaRouteProvider provider, 1316 ProviderDescriptor descriptor) { 1317 updateProviderDescriptor(provider, descriptor); 1318 } 1319 } 1320 1321 private final class CallbackHandler extends Handler { 1322 private final ArrayList<MediaRouter> mTempMediaRouters = 1323 new ArrayList<MediaRouter>(); 1324 1325 public static final int MSG_ROUTE_ADDED = 1; 1326 public static final int MSG_ROUTE_REMOVED = 2; 1327 public static final int MSG_ROUTE_CHANGED = 3; 1328 public static final int MSG_ROUTE_VOLUME_CHANGED = 4; 1329 public static final int MSG_ROUTE_PRESENTATION_DISPLAY_CHANGED = 5; 1330 public static final int MSG_ROUTE_SELECTED = 6; 1331 public static final int MSG_ROUTE_UNSELECTED = 7; 1332 public static final int MSG_PROVIDER_ADDED = 8; 1333 public static final int MSG_PROVIDER_REMOVED = 9; 1334 1335 public void post(int msg, Object obj) { 1336 obtainMessage(msg, obj).sendToTarget(); 1337 } 1338 1339 @Override 1340 public void handleMessage(Message msg) { 1341 final int what = msg.what; 1342 final Object obj = msg.obj; 1343 1344 // Synchronize state with the system media router. 1345 syncWithSystemProvider(what, obj); 1346 1347 // Invoke all registered callbacks. 1348 mTempMediaRouters.addAll(mRouters.values()); 1349 try { 1350 final int routerCount = mTempMediaRouters.size(); 1351 for (int i = 0; i < routerCount; i++) { 1352 final MediaRouter router = mTempMediaRouters.get(i); 1353 if (!router.mCallbacks.isEmpty()) { 1354 for (MediaRouter.Callback callback : router.mCallbacks) { 1355 invokeCallback(router, callback, what, obj); 1356 } 1357 } 1358 } 1359 } finally { 1360 mTempMediaRouters.clear(); 1361 } 1362 } 1363 1364 private void syncWithSystemProvider(int what, Object obj) { 1365 switch (what) { 1366 case MSG_ROUTE_ADDED: 1367 mSystemProvider.onSyncRouteAdded((RouteInfo)obj); 1368 break; 1369 case MSG_ROUTE_REMOVED: 1370 mSystemProvider.onSyncRouteRemoved((RouteInfo)obj); 1371 break; 1372 case MSG_ROUTE_CHANGED: 1373 mSystemProvider.onSyncRouteChanged((RouteInfo)obj); 1374 break; 1375 case MSG_ROUTE_SELECTED: 1376 mSystemProvider.onSyncRouteSelected((RouteInfo)obj); 1377 break; 1378 } 1379 } 1380 1381 private void invokeCallback(MediaRouter router, MediaRouter.Callback callback, 1382 int what, Object obj) { 1383 switch (what) { 1384 case MSG_ROUTE_ADDED: 1385 callback.onRouteAdded(router, (RouteInfo)obj); 1386 break; 1387 case MSG_ROUTE_REMOVED: 1388 callback.onRouteRemoved(router, (RouteInfo)obj); 1389 break; 1390 case MSG_ROUTE_CHANGED: 1391 callback.onRouteChanged(router, (RouteInfo)obj); 1392 break; 1393 case MSG_ROUTE_VOLUME_CHANGED: 1394 callback.onRouteVolumeChanged(router, (RouteInfo)obj); 1395 break; 1396 case MSG_ROUTE_PRESENTATION_DISPLAY_CHANGED: 1397 callback.onRoutePresentationDisplayChanged(router, (RouteInfo)obj); 1398 break; 1399 case MSG_ROUTE_SELECTED: 1400 callback.onRouteSelected(router, (RouteInfo)obj); 1401 break; 1402 case MSG_ROUTE_UNSELECTED: 1403 callback.onRouteUnselected(router, (RouteInfo)obj); 1404 break; 1405 case MSG_PROVIDER_ADDED: 1406 callback.onProviderAdded(router, (ProviderInfo)obj); 1407 break; 1408 case MSG_PROVIDER_REMOVED: 1409 callback.onProviderRemoved(router, (ProviderInfo)obj); 1410 break; 1411 } 1412 } 1413 } 1414 } 1415} 1416