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