MediaRouter.java revision 70e11e50eecfc8f1dfb76316d099e4331ebd28f7
1/* 2 * Copyright (C) 2012 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.media; 18 19import android.bluetooth.BluetoothA2dp; 20import android.content.BroadcastReceiver; 21import android.content.Context; 22import android.content.Intent; 23import android.content.IntentFilter; 24import android.content.res.Resources; 25import android.graphics.drawable.Drawable; 26import android.os.Handler; 27import android.util.Log; 28 29import java.util.ArrayList; 30import java.util.HashMap; 31import java.util.List; 32 33/** 34 * MediaRouter allows applications to control the routing of media channels 35 * and streams from the current device to external speakers and destination devices. 36 * 37 * <p>A MediaRouter is retrieved through {@link Context#getSystemService(String) 38 * Context.getSystemService()} of a {@link Context#MEDIA_ROUTER_SERVICE 39 * Context.MEDIA_ROUTER_SERVICE}. 40 * 41 * <p>The media router API is not thread-safe; all interactions with it must be 42 * done from the main thread of the process.</p> 43 */ 44public class MediaRouter { 45 private static final String TAG = "MediaRouter"; 46 47 static class Static { 48 final Resources mResources; 49 final AudioManager mAudioManager; 50 final Handler mHandler; 51 final ArrayList<CallbackInfo> mCallbacks = new ArrayList<CallbackInfo>(); 52 53 final ArrayList<RouteInfo> mRoutes = new ArrayList<RouteInfo>(); 54 final ArrayList<RouteCategory> mCategories = new ArrayList<RouteCategory>(); 55 56 final RouteCategory mSystemCategory; 57 final HeadphoneChangedBroadcastReceiver mHeadphoneChangedReceiver; 58 59 RouteInfo mDefaultAudio; 60 RouteInfo mBluetoothA2dpRoute; 61 62 RouteInfo mSelectedRoute; 63 64 Static(Context appContext) { 65 mResources = Resources.getSystem(); 66 mHandler = new Handler(appContext.getMainLooper()); 67 68 mAudioManager = (AudioManager)appContext.getSystemService(Context.AUDIO_SERVICE); 69 70 // XXX this doesn't deal with locale changes! 71 mSystemCategory = new RouteCategory(mResources.getText( 72 com.android.internal.R.string.default_audio_route_category_name), 73 ROUTE_TYPE_LIVE_AUDIO, false); 74 75 final IntentFilter speakerFilter = new IntentFilter(Intent.ACTION_HEADSET_PLUG); 76 speakerFilter.addAction(Intent.ACTION_ANALOG_AUDIO_DOCK_PLUG); 77 speakerFilter.addAction(Intent.ACTION_DIGITAL_AUDIO_DOCK_PLUG); 78 speakerFilter.addAction(Intent.ACTION_HDMI_AUDIO_PLUG); 79 mHeadphoneChangedReceiver = new HeadphoneChangedBroadcastReceiver(); 80 appContext.registerReceiver(mHeadphoneChangedReceiver, speakerFilter); 81 } 82 83 // Called after sStatic is initialized 84 void initDefaultRoutes() { 85 mDefaultAudio = new RouteInfo(mSystemCategory); 86 mDefaultAudio.mName = mResources.getText( 87 com.android.internal.R.string.default_audio_route_name); 88 mDefaultAudio.mSupportedTypes = ROUTE_TYPE_LIVE_AUDIO; 89 addRoute(mDefaultAudio); 90 } 91 } 92 93 static Static sStatic; 94 95 /** 96 * Route type flag for live audio. 97 * 98 * <p>A device that supports live audio routing will allow the media audio stream 99 * to be routed to supported destinations. This can include internal speakers or 100 * audio jacks on the device itself, A2DP devices, and more.</p> 101 * 102 * <p>Once initiated this routing is transparent to the application. All audio 103 * played on the media stream will be routed to the selected destination.</p> 104 */ 105 public static final int ROUTE_TYPE_LIVE_AUDIO = 0x1; 106 107 /** 108 * Route type flag for application-specific usage. 109 * 110 * <p>Unlike other media route types, user routes are managed by the application. 111 * The MediaRouter will manage and dispatch events for user routes, but the application 112 * is expected to interpret the meaning of these events and perform the requested 113 * routing tasks.</p> 114 */ 115 public static final int ROUTE_TYPE_USER = 0x00800000; 116 117 // Maps application contexts 118 static final HashMap<Context, MediaRouter> sRouters = new HashMap<Context, MediaRouter>(); 119 120 static String typesToString(int types) { 121 final StringBuilder result = new StringBuilder(); 122 if ((types & ROUTE_TYPE_LIVE_AUDIO) != 0) { 123 result.append("ROUTE_TYPE_LIVE_AUDIO "); 124 } 125 if ((types & ROUTE_TYPE_USER) != 0) { 126 result.append("ROUTE_TYPE_USER "); 127 } 128 return result.toString(); 129 } 130 131 /** @hide */ 132 public MediaRouter(Context context) { 133 synchronized (Static.class) { 134 if (sStatic == null) { 135 sStatic = new Static(context.getApplicationContext()); 136 sStatic.initDefaultRoutes(); 137 } 138 } 139 } 140 141 /** 142 * @hide for use by framework routing UI 143 */ 144 public RouteInfo getSystemAudioRoute() { 145 return sStatic.mDefaultAudio; 146 } 147 148 /** 149 * Return the currently selected route for the given types 150 * 151 * @param type route types 152 * @return the selected route 153 */ 154 public RouteInfo getSelectedRoute(int type) { 155 return sStatic.mSelectedRoute; 156 } 157 158 static void onHeadphonesPlugged(boolean headphonesPresent, String headphonesName) { 159 sStatic.mDefaultAudio.mName = headphonesPresent 160 ? headphonesName 161 : sStatic.mResources.getText( 162 com.android.internal.R.string.default_audio_route_name); 163 dispatchRouteChanged(sStatic.mDefaultAudio); 164 } 165 166 /** 167 * Add a callback to listen to events about specific kinds of media routes. 168 * If the specified callback is already registered, its registration will be updated for any 169 * additional route types specified. 170 * 171 * @param types Types of routes this callback is interested in 172 * @param cb Callback to add 173 */ 174 public void addCallback(int types, Callback cb) { 175 final int count = sStatic.mCallbacks.size(); 176 for (int i = 0; i < count; i++) { 177 final CallbackInfo info = sStatic.mCallbacks.get(i); 178 if (info.cb == cb) { 179 info.type &= types; 180 return; 181 } 182 } 183 sStatic.mCallbacks.add(new CallbackInfo(cb, types, this)); 184 } 185 186 /** 187 * Remove the specified callback. It will no longer receive events about media routing. 188 * 189 * @param cb Callback to remove 190 */ 191 public void removeCallback(Callback cb) { 192 final int count = sStatic.mCallbacks.size(); 193 for (int i = 0; i < count; i++) { 194 if (sStatic.mCallbacks.get(i).cb == cb) { 195 sStatic.mCallbacks.remove(i); 196 return; 197 } 198 } 199 Log.w(TAG, "removeCallback(" + cb + "): callback not registered"); 200 } 201 202 /** 203 * Select the specified route to use for output of the given media types. 204 * 205 * @param types type flags indicating which types this route should be used for. 206 * The route must support at least a subset. 207 * @param route Route to select 208 */ 209 public void selectRoute(int types, RouteInfo route) { 210 selectRouteStatic(types, route); 211 } 212 213 static void selectRouteStatic(int types, RouteInfo route) { 214 if (sStatic.mSelectedRoute == route) return; 215 216 if (sStatic.mSelectedRoute != null) { 217 // TODO filter types properly 218 dispatchRouteUnselected(types & sStatic.mSelectedRoute.getSupportedTypes(), 219 sStatic.mSelectedRoute); 220 } 221 sStatic.mSelectedRoute = route; 222 if (route != null) { 223 // TODO filter types properly 224 dispatchRouteSelected(types & route.getSupportedTypes(), route); 225 } 226 } 227 228 /** 229 * Add an app-specified route for media to the MediaRouter. 230 * App-specified route definitions are created using {@link #createUserRoute(RouteCategory)} 231 * 232 * @param info Definition of the route to add 233 * @see #createUserRoute() 234 * @see #removeUserRoute(UserRouteInfo) 235 */ 236 public void addUserRoute(UserRouteInfo info) { 237 addRoute(info); 238 } 239 240 static void addRoute(RouteInfo info) { 241 final RouteCategory cat = info.getCategory(); 242 if (!sStatic.mCategories.contains(cat)) { 243 sStatic.mCategories.add(cat); 244 } 245 final boolean onlyRoute = sStatic.mRoutes.isEmpty(); 246 if (cat.isGroupable() && !(info instanceof RouteGroup)) { 247 // Enforce that any added route in a groupable category must be in a group. 248 final RouteGroup group = new RouteGroup(info.getCategory()); 249 sStatic.mRoutes.add(group); 250 dispatchRouteAdded(group); 251 252 final int at = group.getRouteCount(); 253 group.addRoute(info); 254 dispatchRouteGrouped(info, group, at); 255 256 info = group; 257 } else { 258 sStatic.mRoutes.add(info); 259 dispatchRouteAdded(info); 260 } 261 262 if (onlyRoute) { 263 selectRouteStatic(info.getSupportedTypes(), info); 264 } 265 } 266 267 /** 268 * Remove an app-specified route for media from the MediaRouter. 269 * 270 * @param info Definition of the route to remove 271 * @see #addUserRoute(UserRouteInfo) 272 */ 273 public void removeUserRoute(UserRouteInfo info) { 274 removeRoute(info); 275 } 276 277 /** 278 * Remove all app-specified routes from the MediaRouter. 279 * 280 * @see #removeUserRoute(UserRouteInfo) 281 */ 282 public void clearUserRoutes() { 283 for (int i = 0; i < sStatic.mRoutes.size(); i++) { 284 final RouteInfo info = sStatic.mRoutes.get(i); 285 if (info instanceof UserRouteInfo) { 286 removeRouteAt(i); 287 i--; 288 } 289 } 290 } 291 292 static void removeRoute(RouteInfo info) { 293 if (sStatic.mRoutes.remove(info)) { 294 final RouteCategory removingCat = info.getCategory(); 295 final int count = sStatic.mRoutes.size(); 296 boolean found = false; 297 for (int i = 0; i < count; i++) { 298 final RouteCategory cat = sStatic.mRoutes.get(i).getCategory(); 299 if (removingCat == cat) { 300 found = true; 301 break; 302 } 303 } 304 if (!found) { 305 sStatic.mCategories.remove(removingCat); 306 } 307 dispatchRouteRemoved(info); 308 } 309 } 310 311 void removeRouteAt(int routeIndex) { 312 if (routeIndex >= 0 && routeIndex < sStatic.mRoutes.size()) { 313 final RouteInfo info = sStatic.mRoutes.remove(routeIndex); 314 final RouteCategory removingCat = info.getCategory(); 315 final int count = sStatic.mRoutes.size(); 316 boolean found = false; 317 for (int i = 0; i < count; i++) { 318 final RouteCategory cat = sStatic.mRoutes.get(i).getCategory(); 319 if (removingCat == cat) { 320 found = true; 321 break; 322 } 323 } 324 if (!found) { 325 sStatic.mCategories.remove(removingCat); 326 } 327 dispatchRouteRemoved(info); 328 } 329 } 330 331 /** 332 * Return the number of {@link MediaRouter.RouteCategory categories} currently 333 * represented by routes known to this MediaRouter. 334 * 335 * @return the number of unique categories represented by this MediaRouter's known routes 336 */ 337 public int getCategoryCount() { 338 return sStatic.mCategories.size(); 339 } 340 341 /** 342 * Return the {@link MediaRouter.RouteCategory category} at the given index. 343 * Valid indices are in the range [0-getCategoryCount). 344 * 345 * @param index which category to return 346 * @return the category at index 347 */ 348 public RouteCategory getCategoryAt(int index) { 349 return sStatic.mCategories.get(index); 350 } 351 352 /** 353 * Return the number of {@link MediaRouter.RouteInfo routes} currently known 354 * to this MediaRouter. 355 * 356 * @return the number of routes tracked by this router 357 */ 358 public int getRouteCount() { 359 return sStatic.mRoutes.size(); 360 } 361 362 /** 363 * Return the route at the specified index. 364 * 365 * @param index index of the route to return 366 * @return the route at index 367 */ 368 public RouteInfo getRouteAt(int index) { 369 return sStatic.mRoutes.get(index); 370 } 371 372 static int getRouteCountStatic() { 373 return sStatic.mRoutes.size(); 374 } 375 376 static RouteInfo getRouteAtStatic(int index) { 377 return sStatic.mRoutes.get(index); 378 } 379 380 /** 381 * Create a new user route that may be modified and registered for use by the application. 382 * 383 * @param category The category the new route will belong to 384 * @return A new UserRouteInfo for use by the application 385 * 386 * @see #addUserRoute(UserRouteInfo) 387 * @see #removeUserRoute(UserRouteInfo) 388 * @see #createRouteCategory(CharSequence) 389 */ 390 public UserRouteInfo createUserRoute(RouteCategory category) { 391 return new UserRouteInfo(category); 392 } 393 394 /** 395 * Create a new route category. Each route must belong to a category. 396 * 397 * @param name Name of the new category 398 * @param isGroupable true if routes in this category may be grouped with one another 399 * @return the new RouteCategory 400 */ 401 public RouteCategory createRouteCategory(CharSequence name, boolean isGroupable) { 402 return new RouteCategory(name, ROUTE_TYPE_USER, isGroupable); 403 } 404 405 static void updateRoute(final RouteInfo info) { 406 dispatchRouteChanged(info); 407 } 408 409 static void dispatchRouteSelected(int type, RouteInfo info) { 410 final int count = sStatic.mCallbacks.size(); 411 for (int i = 0; i < count; i++) { 412 final CallbackInfo cbi = sStatic.mCallbacks.get(i); 413 if ((cbi.type & type) != 0) { 414 cbi.cb.onRouteSelected(cbi.router, type, info); 415 } 416 } 417 } 418 419 static void dispatchRouteUnselected(int type, RouteInfo info) { 420 final int count = sStatic.mCallbacks.size(); 421 for (int i = 0; i < count; i++) { 422 final CallbackInfo cbi = sStatic.mCallbacks.get(i); 423 if ((cbi.type & type) != 0) { 424 cbi.cb.onRouteUnselected(cbi.router, type, info); 425 } 426 } 427 } 428 429 static void dispatchRouteChanged(RouteInfo info) { 430 final int count = sStatic.mCallbacks.size(); 431 for (int i = 0; i < count; i++) { 432 final CallbackInfo cbi = sStatic.mCallbacks.get(i); 433 if ((cbi.type & info.mSupportedTypes) != 0) { 434 cbi.cb.onRouteChanged(cbi.router, info); 435 } 436 } 437 } 438 439 static void dispatchRouteAdded(RouteInfo info) { 440 final int count = sStatic.mCallbacks.size(); 441 for (int i = 0; i < count; i++) { 442 final CallbackInfo cbi = sStatic.mCallbacks.get(i); 443 if ((cbi.type & info.mSupportedTypes) != 0) { 444 cbi.cb.onRouteAdded(cbi.router, info); 445 } 446 } 447 } 448 449 static void dispatchRouteRemoved(RouteInfo info) { 450 final int count = sStatic.mCallbacks.size(); 451 for (int i = 0; i < count; i++) { 452 final CallbackInfo cbi = sStatic.mCallbacks.get(i); 453 if ((cbi.type & info.mSupportedTypes) != 0) { 454 cbi.cb.onRouteRemoved(cbi.router, info); 455 } 456 } 457 } 458 459 static void dispatchRouteGrouped(RouteInfo info, RouteGroup group, int index) { 460 final int count = sStatic.mCallbacks.size(); 461 for (int i = 0; i < count; i++) { 462 final CallbackInfo cbi = sStatic.mCallbacks.get(i); 463 if ((cbi.type & group.mSupportedTypes) != 0) { 464 cbi.cb.onRouteGrouped(cbi.router, info, group, index); 465 } 466 } 467 } 468 469 static void dispatchRouteUngrouped(RouteInfo info, RouteGroup group) { 470 final int count = sStatic.mCallbacks.size(); 471 for (int i = 0; i < count; i++) { 472 final CallbackInfo cbi = sStatic.mCallbacks.get(i); 473 if ((cbi.type & group.mSupportedTypes) != 0) { 474 cbi.cb.onRouteUngrouped(cbi.router, info, group); 475 } 476 } 477 } 478 479 static void onA2dpDeviceConnected() { 480 final RouteInfo info = new RouteInfo(sStatic.mSystemCategory); 481 info.mName = "Bluetooth"; // TODO Fetch the real name of the device 482 sStatic.mBluetoothA2dpRoute = info; 483 addRoute(sStatic.mBluetoothA2dpRoute); 484 } 485 486 static void onA2dpDeviceDisconnected() { 487 removeRoute(sStatic.mBluetoothA2dpRoute); 488 sStatic.mBluetoothA2dpRoute = null; 489 } 490 491 /** 492 * Information about a media route. 493 */ 494 public static class RouteInfo { 495 CharSequence mName; 496 private CharSequence mStatus; 497 int mSupportedTypes; 498 RouteGroup mGroup; 499 final RouteCategory mCategory; 500 Drawable mIcon; 501 502 RouteInfo(RouteCategory category) { 503 mCategory = category; 504 } 505 506 /** 507 * @return The user-friendly name of a media route. This is the string presented 508 * to users who may select this as the active route. 509 */ 510 public CharSequence getName() { 511 return mName; 512 } 513 514 /** 515 * @return The user-friendly status for a media route. This may include a description 516 * of the currently playing media, if available. 517 */ 518 public CharSequence getStatus() { 519 return mStatus; 520 } 521 522 /** 523 * @return A media type flag set describing which types this route supports. 524 */ 525 public int getSupportedTypes() { 526 return mSupportedTypes; 527 } 528 529 /** 530 * @return The group that this route belongs to. 531 */ 532 public RouteGroup getGroup() { 533 return mGroup; 534 } 535 536 /** 537 * @return the category this route belongs to. 538 */ 539 public RouteCategory getCategory() { 540 return mCategory; 541 } 542 543 /** 544 * Get the icon representing this route. 545 * This icon will be used in picker UIs if available. 546 * 547 * @return the icon representing this route or null if no icon is available 548 */ 549 public Drawable getIconDrawable() { 550 return mIcon; 551 } 552 553 void setStatusInt(CharSequence status) { 554 if (!status.equals(mStatus)) { 555 mStatus = status; 556 routeUpdated(); 557 if (mGroup != null) { 558 mGroup.memberStatusChanged(this, status); 559 } 560 routeUpdated(); 561 } 562 } 563 564 void routeUpdated() { 565 updateRoute(this); 566 } 567 568 @Override 569 public String toString() { 570 String supportedTypes = typesToString(mSupportedTypes); 571 return "RouteInfo{ name=" + mName + ", status=" + mStatus + 572 " category=" + mCategory + 573 " supportedTypes=" + supportedTypes + "}"; 574 } 575 } 576 577 /** 578 * Information about a route that the application may define and modify. 579 * 580 * @see MediaRouter.RouteInfo 581 */ 582 public static class UserRouteInfo extends RouteInfo { 583 RemoteControlClient mRcc; 584 private Object mTag; 585 586 UserRouteInfo(RouteCategory category) { 587 super(category); 588 mSupportedTypes = ROUTE_TYPE_USER; 589 } 590 591 /** 592 * Set the user-visible name of this route. 593 * @param name Name to display to the user to describe this route 594 */ 595 public void setName(CharSequence name) { 596 mName = name; 597 routeUpdated(); 598 } 599 600 /** 601 * Set the current user-visible status for this route. 602 * @param status Status to display to the user to describe what the endpoint 603 * of this route is currently doing 604 */ 605 public void setStatus(CharSequence status) { 606 setStatusInt(status); 607 } 608 609 /** 610 * Set the RemoteControlClient responsible for reporting playback info for this 611 * user route. 612 * 613 * <p>If this route manages remote playback, the data exposed by this 614 * RemoteControlClient will be used to reflect and update information 615 * such as route volume info in related UIs.</p> 616 * 617 * @param rcc RemoteControlClient associated with this route 618 */ 619 public void setRemoteControlClient(RemoteControlClient rcc) { 620 mRcc = rcc; 621 } 622 623 /** 624 * Set an icon that will be used to represent this route. 625 * The system may use this icon in picker UIs or similar. 626 * 627 * @param icon icon drawable to use to represent this route 628 */ 629 public void setIconDrawable(Drawable icon) { 630 mIcon = icon; 631 } 632 633 /** 634 * Set an icon that will be used to represent this route. 635 * The system may use this icon in picker UIs or similar. 636 * 637 * @param resId Resource ID of an icon drawable to use to represent this route 638 */ 639 public void setIconResource(int resId) { 640 setIconDrawable(sStatic.mResources.getDrawable(resId)); 641 } 642 643 /** 644 * Set an application-specific tag object for this route. 645 * The application may use this to store arbitrary data associated with the 646 * route for internal tracking. 647 * 648 * <p>Note that the lifespan of a route may be well past the lifespan of 649 * an Activity or other Context; take care that objects you store here 650 * will not keep more data in memory alive than you intend.</p> 651 * 652 * @param tag Arbitrary, app-specific data for this route to hold for later use 653 */ 654 public void setTag(Object tag) { 655 mTag = tag; 656 } 657 658 /** 659 * @return The tag object previously set by the application 660 * @see #setTag(Object) 661 */ 662 public Object getTag() { 663 return mTag; 664 } 665 } 666 667 /** 668 * Information about a route that consists of multiple other routes in a group. 669 */ 670 public static class RouteGroup extends RouteInfo { 671 final ArrayList<RouteInfo> mRoutes = new ArrayList<RouteInfo>(); 672 private boolean mUpdateName; 673 674 RouteGroup(RouteCategory category) { 675 super(category); 676 mGroup = this; 677 } 678 679 public CharSequence getName() { 680 if (mUpdateName) updateName(); 681 return super.getName(); 682 } 683 684 /** 685 * Add a route to this group. The route must not currently belong to another group. 686 * 687 * @param route route to add to this group 688 */ 689 public void addRoute(RouteInfo route) { 690 if (route.getGroup() != null) { 691 throw new IllegalStateException("Route " + route + " is already part of a group."); 692 } 693 if (route.getCategory() != mCategory) { 694 throw new IllegalArgumentException( 695 "Route cannot be added to a group with a different category. " + 696 "(Route category=" + route.getCategory() + 697 " group category=" + mCategory + ")"); 698 } 699 final int at = mRoutes.size(); 700 mRoutes.add(route); 701 mUpdateName = true; 702 dispatchRouteGrouped(route, this, at); 703 routeUpdated(); 704 } 705 706 /** 707 * Add a route to this group before the specified index. 708 * 709 * @param route route to add 710 * @param insertAt insert the new route before this index 711 */ 712 public void addRoute(RouteInfo route, int insertAt) { 713 if (route.getGroup() != null) { 714 throw new IllegalStateException("Route " + route + " is already part of a group."); 715 } 716 if (route.getCategory() != mCategory) { 717 throw new IllegalArgumentException( 718 "Route cannot be added to a group with a different category. " + 719 "(Route category=" + route.getCategory() + 720 " group category=" + mCategory + ")"); 721 } 722 mRoutes.add(insertAt, route); 723 mUpdateName = true; 724 dispatchRouteGrouped(route, this, insertAt); 725 routeUpdated(); 726 } 727 728 /** 729 * Remove a route from this group. 730 * 731 * @param route route to remove 732 */ 733 public void removeRoute(RouteInfo route) { 734 if (route.getGroup() != this) { 735 throw new IllegalArgumentException("Route " + route + 736 " is not a member of this group."); 737 } 738 mRoutes.remove(route); 739 mUpdateName = true; 740 dispatchRouteUngrouped(route, this); 741 routeUpdated(); 742 } 743 744 /** 745 * Remove the route at the specified index from this group. 746 * 747 * @param index index of the route to remove 748 */ 749 public void removeRoute(int index) { 750 RouteInfo route = mRoutes.remove(index); 751 mUpdateName = true; 752 dispatchRouteUngrouped(route, this); 753 routeUpdated(); 754 } 755 756 /** 757 * @return The number of routes in this group 758 */ 759 public int getRouteCount() { 760 return mRoutes.size(); 761 } 762 763 /** 764 * Return the route in this group at the specified index 765 * 766 * @param index Index to fetch 767 * @return The route at index 768 */ 769 public RouteInfo getRouteAt(int index) { 770 return mRoutes.get(index); 771 } 772 773 /** 774 * Set an icon that will be used to represent this group. 775 * The system may use this icon in picker UIs or similar. 776 * 777 * @param icon icon drawable to use to represent this group 778 */ 779 public void setIconDrawable(Drawable icon) { 780 mIcon = icon; 781 } 782 783 /** 784 * Set an icon that will be used to represent this group. 785 * The system may use this icon in picker UIs or similar. 786 * 787 * @param resId Resource ID of an icon drawable to use to represent this group 788 */ 789 public void setIconResource(int resId) { 790 setIconDrawable(sStatic.mResources.getDrawable(resId)); 791 } 792 793 void memberNameChanged(RouteInfo info, CharSequence name) { 794 mUpdateName = true; 795 routeUpdated(); 796 } 797 798 void memberStatusChanged(RouteInfo info, CharSequence status) { 799 setStatusInt(status); 800 } 801 802 void updateName() { 803 final StringBuilder sb = new StringBuilder(); 804 final int count = mRoutes.size(); 805 for (int i = 0; i < count; i++) { 806 final RouteInfo info = mRoutes.get(i); 807 if (i > 0) sb.append(", "); 808 sb.append(info.mName); 809 } 810 mName = sb.toString(); 811 mUpdateName = false; 812 } 813 } 814 815 /** 816 * Definition of a category of routes. All routes belong to a category. 817 */ 818 public static class RouteCategory { 819 CharSequence mName; 820 int mTypes; 821 final boolean mGroupable; 822 823 RouteCategory(CharSequence name, int types, boolean groupable) { 824 mName = name; 825 mTypes = types; 826 mGroupable = groupable; 827 } 828 829 /** 830 * @return the name of this route category 831 */ 832 public CharSequence getName() { 833 return mName; 834 } 835 836 /** 837 * Return the current list of routes in this category that have been added 838 * to the MediaRouter. 839 * 840 * <p>This list will not include routes that are nested within RouteGroups. 841 * A RouteGroup is treated as a single route within its category.</p> 842 * 843 * @param out a List to fill with the routes in this category. If this parameter is 844 * non-null, it will be cleared, filled with the current routes with this 845 * category, and returned. If this parameter is null, a new List will be 846 * allocated to report the category's current routes. 847 * @return A list with the routes in this category that have been added to the MediaRouter. 848 */ 849 public List<RouteInfo> getRoutes(List<RouteInfo> out) { 850 if (out == null) { 851 out = new ArrayList<RouteInfo>(); 852 } else { 853 out.clear(); 854 } 855 856 final int count = getRouteCountStatic(); 857 for (int i = 0; i < count; i++) { 858 final RouteInfo route = getRouteAtStatic(i); 859 if (route.mCategory == this) { 860 out.add(route); 861 } 862 } 863 return out; 864 } 865 866 /** 867 * @return Flag set describing the route types supported by this category 868 */ 869 public int getSupportedTypes() { 870 return mTypes; 871 } 872 873 /** 874 * Return whether or not this category supports grouping. 875 * 876 * <p>If this method returns true, all routes obtained from this category 877 * via calls to {@link #getRouteAt(int)} will be {@link MediaRouter.RouteGroup}s.</p> 878 * 879 * @return true if this category supports 880 */ 881 public boolean isGroupable() { 882 return mGroupable; 883 } 884 885 public String toString() { 886 return "RouteCategory{ name=" + mName + " types=" + typesToString(mTypes) + 887 " groupable=" + mGroupable + " routes=" + sStatic.mRoutes.size() + " }"; 888 } 889 } 890 891 static class CallbackInfo { 892 public int type; 893 public final Callback cb; 894 public final MediaRouter router; 895 896 public CallbackInfo(Callback cb, int type, MediaRouter router) { 897 this.cb = cb; 898 this.type = type; 899 this.router = router; 900 } 901 } 902 903 /** 904 * Interface for receiving events about media routing changes. 905 * All methods of this interface will be called from the application's main thread. 906 * 907 * <p>A Callback will only receive events relevant to routes that the callback 908 * was registered for.</p> 909 * 910 * @see MediaRouter#addCallback(int, Callback) 911 * @see MediaRouter#removeCallback(Callback) 912 */ 913 public interface Callback { 914 /** 915 * Called when the supplied route becomes selected as the active route 916 * for the given route type. 917 * 918 * @param router the MediaRouter reporting the event 919 * @param type Type flag set indicating the routes that have been selected 920 * @param info Route that has been selected for the given route types 921 */ 922 public void onRouteSelected(MediaRouter router, int type, RouteInfo info); 923 924 /** 925 * Called when the supplied route becomes unselected as the active route 926 * for the given route type. 927 * 928 * @param router the MediaRouter reporting the event 929 * @param type Type flag set indicating the routes that have been unselected 930 * @param info Route that has been unselected for the given route types 931 */ 932 public void onRouteUnselected(MediaRouter router, int type, RouteInfo info); 933 934 /** 935 * Called when a route for the specified type was added. 936 * 937 * @param router the MediaRouter reporting the event 938 * @param info Route that has become available for use 939 */ 940 public void onRouteAdded(MediaRouter router, RouteInfo info); 941 942 /** 943 * Called when a route for the specified type was removed. 944 * 945 * @param router the MediaRouter reporting the event 946 * @param info Route that has been removed from availability 947 */ 948 public void onRouteRemoved(MediaRouter router, RouteInfo info); 949 950 /** 951 * Called when an aspect of the indicated route has changed. 952 * 953 * <p>This will not indicate that the types supported by this route have 954 * changed, only that cosmetic info such as name or status have been updated.</p> 955 * 956 * @param router the MediaRouter reporting the event 957 * @param info The route that was changed 958 */ 959 public void onRouteChanged(MediaRouter router, RouteInfo info); 960 961 /** 962 * Called when a route is added to a group. 963 * 964 * @param router the MediaRouter reporting the event 965 * @param info The route that was added 966 * @param group The group the route was added to 967 * @param index The route index within group that info was added at 968 */ 969 public void onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group, int index); 970 971 /** 972 * Called when a route is removed from a group. 973 * 974 * @param router the MediaRouter reporting the event 975 * @param info The route that was removed 976 * @param group The group the route was removed from 977 */ 978 public void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group); 979 } 980 981 /** 982 * Stub implementation of the {@link MediaRouter.Callback} interface. 983 * Each interface method is defined as a no-op. Override just the ones 984 * you need. 985 */ 986 public static class SimpleCallback implements Callback { 987 988 @Override 989 public void onRouteSelected(MediaRouter router, int type, RouteInfo info) { 990 } 991 992 @Override 993 public void onRouteUnselected(MediaRouter router, int type, RouteInfo info) { 994 } 995 996 @Override 997 public void onRouteAdded(MediaRouter router, RouteInfo info) { 998 } 999 1000 @Override 1001 public void onRouteRemoved(MediaRouter router, RouteInfo info) { 1002 } 1003 1004 @Override 1005 public void onRouteChanged(MediaRouter router, RouteInfo info) { 1006 } 1007 1008 @Override 1009 public void onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group, 1010 int index) { 1011 } 1012 1013 @Override 1014 public void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group) { 1015 } 1016 1017 } 1018 1019 class BtChangedBroadcastReceiver extends BroadcastReceiver { 1020 @Override 1021 public void onReceive(Context context, Intent intent) { 1022 final String action = intent.getAction(); 1023 if (BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED.equals(action)) { 1024 final int state = intent.getIntExtra(BluetoothA2dp.EXTRA_STATE, -1); 1025 if (state == BluetoothA2dp.STATE_CONNECTED) { 1026 onA2dpDeviceConnected(); 1027 } else if (state == BluetoothA2dp.STATE_DISCONNECTING || 1028 state == BluetoothA2dp.STATE_DISCONNECTED) { 1029 onA2dpDeviceDisconnected(); 1030 } 1031 } 1032 } 1033 } 1034 1035 static class HeadphoneChangedBroadcastReceiver extends BroadcastReceiver { 1036 @Override 1037 public void onReceive(Context context, Intent intent) { 1038 final String action = intent.getAction(); 1039 if (Intent.ACTION_HEADSET_PLUG.equals(action)) { 1040 final boolean plugged = intent.getIntExtra("state", 0) != 0; 1041 final String name = sStatic.mResources.getString( 1042 com.android.internal.R.string.default_audio_route_name_headphones); 1043 onHeadphonesPlugged(plugged, name); 1044 } else if (Intent.ACTION_ANALOG_AUDIO_DOCK_PLUG.equals(action) || 1045 Intent.ACTION_DIGITAL_AUDIO_DOCK_PLUG.equals(action)) { 1046 final boolean plugged = intent.getIntExtra("state", 0) != 0; 1047 final String name = sStatic.mResources.getString( 1048 com.android.internal.R.string.default_audio_route_name_dock_speakers); 1049 onHeadphonesPlugged(plugged, name); 1050 } else if (Intent.ACTION_HDMI_AUDIO_PLUG.equals(action)) { 1051 final boolean plugged = intent.getIntExtra("state", 0) != 0; 1052 final String name = sStatic.mResources.getString( 1053 com.android.internal.R.string.default_audio_route_name_hdmi); 1054 onHeadphonesPlugged(plugged, name); 1055 } 1056 } 1057 } 1058} 1059