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