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