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