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