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