MediaRouter.java revision f8ac14a7f5a59b4ec8e89283a2da40b626e42065
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.BroadcastReceiver; 20import android.content.Context; 21import android.content.Intent; 22import android.content.IntentFilter; 23import android.content.res.Resources; 24import android.graphics.drawable.Drawable; 25import android.os.Handler; 26import android.os.IBinder; 27import android.os.RemoteException; 28import android.os.ServiceManager; 29import android.text.TextUtils; 30import android.util.Log; 31 32import java.util.ArrayList; 33import java.util.HashMap; 34import java.util.List; 35import java.util.concurrent.CopyOnWriteArrayList; 36 37/** 38 * MediaRouter allows applications to control the routing of media channels 39 * and streams from the current device to external speakers and destination devices. 40 * 41 * <p>A MediaRouter is retrieved through {@link Context#getSystemService(String) 42 * Context.getSystemService()} of a {@link Context#MEDIA_ROUTER_SERVICE 43 * Context.MEDIA_ROUTER_SERVICE}. 44 * 45 * <p>The media router API is not thread-safe; all interactions with it must be 46 * done from the main thread of the process.</p> 47 */ 48public class MediaRouter { 49 private static final String TAG = "MediaRouter"; 50 51 static class Static { 52 final Resources mResources; 53 final IAudioService mAudioService; 54 final Handler mHandler; 55 final CopyOnWriteArrayList<CallbackInfo> mCallbacks = 56 new CopyOnWriteArrayList<CallbackInfo>(); 57 58 final ArrayList<RouteInfo> mRoutes = new ArrayList<RouteInfo>(); 59 final ArrayList<RouteCategory> mCategories = new ArrayList<RouteCategory>(); 60 61 final RouteCategory mSystemCategory; 62 63 final AudioRoutesInfo mCurRoutesInfo = new AudioRoutesInfo(); 64 65 RouteInfo mDefaultAudio; 66 RouteInfo mBluetoothA2dpRoute; 67 68 RouteInfo mSelectedRoute; 69 70 final IAudioRoutesObserver.Stub mRoutesObserver = new IAudioRoutesObserver.Stub() { 71 public void dispatchAudioRoutesChanged(final AudioRoutesInfo newRoutes) { 72 mHandler.post(new Runnable() { 73 @Override public void run() { 74 updateRoutes(newRoutes); 75 } 76 }); 77 } 78 }; 79 80 Static(Context appContext) { 81 mResources = Resources.getSystem(); 82 mHandler = new Handler(appContext.getMainLooper()); 83 84 IBinder b = ServiceManager.getService(Context.AUDIO_SERVICE); 85 mAudioService = IAudioService.Stub.asInterface(b); 86 87 mSystemCategory = new RouteCategory( 88 com.android.internal.R.string.default_audio_route_category_name, 89 ROUTE_TYPE_LIVE_AUDIO, false); 90 } 91 92 // Called after sStatic is initialized 93 void startMonitoringRoutes(Context appContext) { 94 mDefaultAudio = new RouteInfo(mSystemCategory); 95 mDefaultAudio.mNameResId = com.android.internal.R.string.default_audio_route_name; 96 mDefaultAudio.mSupportedTypes = ROUTE_TYPE_LIVE_AUDIO; 97 addRoute(mDefaultAudio); 98 99 appContext.registerReceiver(new VolumeChangeReceiver(), 100 new IntentFilter(AudioManager.VOLUME_CHANGED_ACTION)); 101 102 AudioRoutesInfo newRoutes = null; 103 try { 104 newRoutes = mAudioService.startWatchingRoutes(mRoutesObserver); 105 } catch (RemoteException e) { 106 } 107 if (newRoutes != null) { 108 updateRoutes(newRoutes); 109 } 110 } 111 112 void updateRoutes(AudioRoutesInfo newRoutes) { 113 if (newRoutes.mMainType != mCurRoutesInfo.mMainType) { 114 mCurRoutesInfo.mMainType = newRoutes.mMainType; 115 int name; 116 if ((newRoutes.mMainType&AudioRoutesInfo.MAIN_HEADPHONES) != 0 117 || (newRoutes.mMainType&AudioRoutesInfo.MAIN_HEADSET) != 0) { 118 name = com.android.internal.R.string.default_audio_route_name_headphones; 119 } else if ((newRoutes.mMainType&AudioRoutesInfo.MAIN_DOCK_SPEAKERS) != 0) { 120 name = com.android.internal.R.string.default_audio_route_name_dock_speakers; 121 } else if ((newRoutes.mMainType&AudioRoutesInfo.MAIN_HDMI) != 0) { 122 name = com.android.internal.R.string.default_audio_route_name_hdmi; 123 } else { 124 name = com.android.internal.R.string.default_audio_route_name; 125 } 126 sStatic.mDefaultAudio.mNameResId = name; 127 dispatchRouteChanged(sStatic.mDefaultAudio); 128 } 129 if (!TextUtils.equals(newRoutes.mBluetoothName, mCurRoutesInfo.mBluetoothName)) { 130 mCurRoutesInfo.mBluetoothName = newRoutes.mBluetoothName; 131 if (mCurRoutesInfo.mBluetoothName != null) { 132 if (sStatic.mBluetoothA2dpRoute == null) { 133 final RouteInfo info = new RouteInfo(sStatic.mSystemCategory); 134 info.mName = mCurRoutesInfo.mBluetoothName; 135 info.mSupportedTypes = ROUTE_TYPE_LIVE_AUDIO; 136 sStatic.mBluetoothA2dpRoute = info; 137 addRoute(sStatic.mBluetoothA2dpRoute); 138 try { 139 if (mAudioService.isBluetoothA2dpOn()) { 140 selectRouteStatic(ROUTE_TYPE_LIVE_AUDIO, mBluetoothA2dpRoute); 141 } 142 } catch (RemoteException e) { 143 Log.e(TAG, "Error selecting Bluetooth A2DP route", e); 144 } 145 } else { 146 sStatic.mBluetoothA2dpRoute.mName = mCurRoutesInfo.mBluetoothName; 147 dispatchRouteChanged(sStatic.mBluetoothA2dpRoute); 148 } 149 } else if (sStatic.mBluetoothA2dpRoute != null) { 150 removeRoute(sStatic.mBluetoothA2dpRoute); 151 sStatic.mBluetoothA2dpRoute = null; 152 } 153 } 154 } 155 } 156 157 static Static sStatic; 158 159 /** 160 * Route type flag for live audio. 161 * 162 * <p>A device that supports live audio routing will allow the media audio stream 163 * to be routed to supported destinations. This can include internal speakers or 164 * audio jacks on the device itself, A2DP devices, and more.</p> 165 * 166 * <p>Once initiated this routing is transparent to the application. All audio 167 * played on the media stream will be routed to the selected destination.</p> 168 */ 169 public static final int ROUTE_TYPE_LIVE_AUDIO = 0x1; 170 171 /** 172 * Route type flag for application-specific usage. 173 * 174 * <p>Unlike other media route types, user routes are managed by the application. 175 * The MediaRouter will manage and dispatch events for user routes, but the application 176 * is expected to interpret the meaning of these events and perform the requested 177 * routing tasks.</p> 178 */ 179 public static final int ROUTE_TYPE_USER = 0x00800000; 180 181 // Maps application contexts 182 static final HashMap<Context, MediaRouter> sRouters = new HashMap<Context, MediaRouter>(); 183 184 static String typesToString(int types) { 185 final StringBuilder result = new StringBuilder(); 186 if ((types & ROUTE_TYPE_LIVE_AUDIO) != 0) { 187 result.append("ROUTE_TYPE_LIVE_AUDIO "); 188 } 189 if ((types & ROUTE_TYPE_USER) != 0) { 190 result.append("ROUTE_TYPE_USER "); 191 } 192 return result.toString(); 193 } 194 195 /** @hide */ 196 public MediaRouter(Context context) { 197 synchronized (Static.class) { 198 if (sStatic == null) { 199 final Context appContext = context.getApplicationContext(); 200 sStatic = new Static(appContext); 201 sStatic.startMonitoringRoutes(appContext); 202 } 203 } 204 } 205 206 /** 207 * @hide for use by framework routing UI 208 */ 209 public RouteInfo getSystemAudioRoute() { 210 return sStatic.mDefaultAudio; 211 } 212 213 /** 214 * @hide for use by framework routing UI 215 */ 216 public RouteCategory getSystemAudioCategory() { 217 return sStatic.mSystemCategory; 218 } 219 220 /** 221 * Return the currently selected route for the given types 222 * 223 * @param type route types 224 * @return the selected route 225 */ 226 public RouteInfo getSelectedRoute(int type) { 227 return sStatic.mSelectedRoute; 228 } 229 230 /** 231 * Add a callback to listen to events about specific kinds of media routes. 232 * If the specified callback is already registered, its registration will be updated for any 233 * additional route types specified. 234 * 235 * @param types Types of routes this callback is interested in 236 * @param cb Callback to add 237 */ 238 public void addCallback(int types, Callback cb) { 239 final int count = sStatic.mCallbacks.size(); 240 for (int i = 0; i < count; i++) { 241 final CallbackInfo info = sStatic.mCallbacks.get(i); 242 if (info.cb == cb) { 243 info.type &= types; 244 return; 245 } 246 } 247 sStatic.mCallbacks.add(new CallbackInfo(cb, types, this)); 248 } 249 250 /** 251 * Remove the specified callback. It will no longer receive events about media routing. 252 * 253 * @param cb Callback to remove 254 */ 255 public void removeCallback(Callback cb) { 256 final int count = sStatic.mCallbacks.size(); 257 for (int i = 0; i < count; i++) { 258 if (sStatic.mCallbacks.get(i).cb == cb) { 259 sStatic.mCallbacks.remove(i); 260 return; 261 } 262 } 263 Log.w(TAG, "removeCallback(" + cb + "): callback not registered"); 264 } 265 266 /** 267 * Select the specified route to use for output of the given media types. 268 * 269 * @param types type flags indicating which types this route should be used for. 270 * The route must support at least a subset. 271 * @param route Route to select 272 */ 273 public void selectRoute(int types, RouteInfo route) { 274 // Applications shouldn't programmatically change anything but user routes. 275 types &= ROUTE_TYPE_USER; 276 selectRouteStatic(types, route); 277 } 278 279 /** 280 * @hide internal use 281 */ 282 public void selectRouteInt(int types, RouteInfo route) { 283 selectRouteStatic(types, route); 284 } 285 286 static void selectRouteStatic(int types, RouteInfo route) { 287 if (sStatic.mSelectedRoute == route) return; 288 if ((route.getSupportedTypes() & types) == 0) { 289 Log.w(TAG, "selectRoute ignored; cannot select route with supported types " + 290 typesToString(route.getSupportedTypes()) + " into route types " + 291 typesToString(types)); 292 return; 293 } 294 295 final RouteInfo btRoute = sStatic.mBluetoothA2dpRoute; 296 if (btRoute != null && (types & ROUTE_TYPE_LIVE_AUDIO) != 0 && 297 (route == btRoute || route == sStatic.mDefaultAudio)) { 298 try { 299 sStatic.mAudioService.setBluetoothA2dpOn(route == btRoute); 300 } catch (RemoteException e) { 301 Log.e(TAG, "Error changing Bluetooth A2DP state", e); 302 } 303 } 304 305 if (sStatic.mSelectedRoute != null) { 306 // TODO filter types properly 307 dispatchRouteUnselected(types & sStatic.mSelectedRoute.getSupportedTypes(), 308 sStatic.mSelectedRoute); 309 } 310 sStatic.mSelectedRoute = route; 311 if (route != null) { 312 // TODO filter types properly 313 dispatchRouteSelected(types & route.getSupportedTypes(), route); 314 } 315 } 316 317 /** 318 * Add an app-specified route for media to the MediaRouter. 319 * App-specified route definitions are created using {@link #createUserRoute(RouteCategory)} 320 * 321 * @param info Definition of the route to add 322 * @see #createUserRoute() 323 * @see #removeUserRoute(UserRouteInfo) 324 */ 325 public void addUserRoute(UserRouteInfo info) { 326 addRoute(info); 327 } 328 329 /** 330 * @hide Framework use only 331 */ 332 public void addRouteInt(RouteInfo info) { 333 addRoute(info); 334 } 335 336 static void addRoute(RouteInfo info) { 337 final RouteCategory cat = info.getCategory(); 338 if (!sStatic.mCategories.contains(cat)) { 339 sStatic.mCategories.add(cat); 340 } 341 final boolean onlyRoute = sStatic.mRoutes.isEmpty(); 342 if (cat.isGroupable() && !(info instanceof RouteGroup)) { 343 // Enforce that any added route in a groupable category must be in a group. 344 final RouteGroup group = new RouteGroup(info.getCategory()); 345 sStatic.mRoutes.add(group); 346 dispatchRouteAdded(group); 347 group.addRoute(info); 348 349 info = group; 350 } else { 351 sStatic.mRoutes.add(info); 352 dispatchRouteAdded(info); 353 } 354 355 if (onlyRoute) { 356 selectRouteStatic(info.getSupportedTypes(), info); 357 } 358 } 359 360 /** 361 * Remove an app-specified route for media from the MediaRouter. 362 * 363 * @param info Definition of the route to remove 364 * @see #addUserRoute(UserRouteInfo) 365 */ 366 public void removeUserRoute(UserRouteInfo info) { 367 removeRoute(info); 368 } 369 370 /** 371 * Remove all app-specified routes from the MediaRouter. 372 * 373 * @see #removeUserRoute(UserRouteInfo) 374 */ 375 public void clearUserRoutes() { 376 for (int i = 0; i < sStatic.mRoutes.size(); i++) { 377 final RouteInfo info = sStatic.mRoutes.get(i); 378 // TODO Right now, RouteGroups only ever contain user routes. 379 // The code below will need to change if this assumption does. 380 if (info instanceof UserRouteInfo || info instanceof RouteGroup) { 381 removeRouteAt(i); 382 i--; 383 } 384 } 385 } 386 387 /** 388 * @hide internal use only 389 */ 390 public void removeRouteInt(RouteInfo info) { 391 removeRoute(info); 392 } 393 394 static void removeRoute(RouteInfo info) { 395 if (sStatic.mRoutes.remove(info)) { 396 final RouteCategory removingCat = info.getCategory(); 397 final int count = sStatic.mRoutes.size(); 398 boolean found = false; 399 for (int i = 0; i < count; i++) { 400 final RouteCategory cat = sStatic.mRoutes.get(i).getCategory(); 401 if (removingCat == cat) { 402 found = true; 403 break; 404 } 405 } 406 if (info == sStatic.mSelectedRoute) { 407 // Removing the currently selected route? Select the default before we remove it. 408 // TODO: Be smarter about the route types here; this selects for all valid. 409 selectRouteStatic(ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_USER, sStatic.mDefaultAudio); 410 } 411 if (!found) { 412 sStatic.mCategories.remove(removingCat); 413 } 414 dispatchRouteRemoved(info); 415 } 416 } 417 418 void removeRouteAt(int routeIndex) { 419 if (routeIndex >= 0 && routeIndex < sStatic.mRoutes.size()) { 420 final RouteInfo info = sStatic.mRoutes.remove(routeIndex); 421 final RouteCategory removingCat = info.getCategory(); 422 final int count = sStatic.mRoutes.size(); 423 boolean found = false; 424 for (int i = 0; i < count; i++) { 425 final RouteCategory cat = sStatic.mRoutes.get(i).getCategory(); 426 if (removingCat == cat) { 427 found = true; 428 break; 429 } 430 } 431 if (info == sStatic.mSelectedRoute) { 432 // Removing the currently selected route? Select the default before we remove it. 433 // TODO: Be smarter about the route types here; this selects for all valid. 434 selectRouteStatic(ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_USER, sStatic.mDefaultAudio); 435 } 436 if (!found) { 437 sStatic.mCategories.remove(removingCat); 438 } 439 dispatchRouteRemoved(info); 440 } 441 } 442 443 /** 444 * Return the number of {@link MediaRouter.RouteCategory categories} currently 445 * represented by routes known to this MediaRouter. 446 * 447 * @return the number of unique categories represented by this MediaRouter's known routes 448 */ 449 public int getCategoryCount() { 450 return sStatic.mCategories.size(); 451 } 452 453 /** 454 * Return the {@link MediaRouter.RouteCategory category} at the given index. 455 * Valid indices are in the range [0-getCategoryCount). 456 * 457 * @param index which category to return 458 * @return the category at index 459 */ 460 public RouteCategory getCategoryAt(int index) { 461 return sStatic.mCategories.get(index); 462 } 463 464 /** 465 * Return the number of {@link MediaRouter.RouteInfo routes} currently known 466 * to this MediaRouter. 467 * 468 * @return the number of routes tracked by this router 469 */ 470 public int getRouteCount() { 471 return sStatic.mRoutes.size(); 472 } 473 474 /** 475 * Return the route at the specified index. 476 * 477 * @param index index of the route to return 478 * @return the route at index 479 */ 480 public RouteInfo getRouteAt(int index) { 481 return sStatic.mRoutes.get(index); 482 } 483 484 static int getRouteCountStatic() { 485 return sStatic.mRoutes.size(); 486 } 487 488 static RouteInfo getRouteAtStatic(int index) { 489 return sStatic.mRoutes.get(index); 490 } 491 492 /** 493 * Create a new user route that may be modified and registered for use by the application. 494 * 495 * @param category The category the new route will belong to 496 * @return A new UserRouteInfo for use by the application 497 * 498 * @see #addUserRoute(UserRouteInfo) 499 * @see #removeUserRoute(UserRouteInfo) 500 * @see #createRouteCategory(CharSequence) 501 */ 502 public UserRouteInfo createUserRoute(RouteCategory category) { 503 return new UserRouteInfo(category); 504 } 505 506 /** 507 * Create a new route category. Each route must belong to a category. 508 * 509 * @param name Name of the new category 510 * @param isGroupable true if routes in this category may be grouped with one another 511 * @return the new RouteCategory 512 */ 513 public RouteCategory createRouteCategory(CharSequence name, boolean isGroupable) { 514 return new RouteCategory(name, ROUTE_TYPE_USER, isGroupable); 515 } 516 517 /** 518 * Create a new route category. Each route must belong to a category. 519 * 520 * @param nameResId Resource ID of the name of the new category 521 * @param isGroupable true if routes in this category may be grouped with one another 522 * @return the new RouteCategory 523 */ 524 public RouteCategory createRouteCategory(int nameResId, boolean isGroupable) { 525 return new RouteCategory(nameResId, ROUTE_TYPE_USER, isGroupable); 526 } 527 528 static void updateRoute(final RouteInfo info) { 529 dispatchRouteChanged(info); 530 } 531 532 static void dispatchRouteSelected(int type, RouteInfo info) { 533 for (CallbackInfo cbi : sStatic.mCallbacks) { 534 if ((cbi.type & type) != 0) { 535 cbi.cb.onRouteSelected(cbi.router, type, info); 536 } 537 } 538 } 539 540 static void dispatchRouteUnselected(int type, RouteInfo info) { 541 for (CallbackInfo cbi : sStatic.mCallbacks) { 542 if ((cbi.type & type) != 0) { 543 cbi.cb.onRouteUnselected(cbi.router, type, info); 544 } 545 } 546 } 547 548 static void dispatchRouteChanged(RouteInfo info) { 549 for (CallbackInfo cbi : sStatic.mCallbacks) { 550 if ((cbi.type & info.mSupportedTypes) != 0) { 551 cbi.cb.onRouteChanged(cbi.router, info); 552 } 553 } 554 } 555 556 static void dispatchRouteAdded(RouteInfo info) { 557 for (CallbackInfo cbi : sStatic.mCallbacks) { 558 if ((cbi.type & info.mSupportedTypes) != 0) { 559 cbi.cb.onRouteAdded(cbi.router, info); 560 } 561 } 562 } 563 564 static void dispatchRouteRemoved(RouteInfo info) { 565 for (CallbackInfo cbi : sStatic.mCallbacks) { 566 if ((cbi.type & info.mSupportedTypes) != 0) { 567 cbi.cb.onRouteRemoved(cbi.router, info); 568 } 569 } 570 } 571 572 static void dispatchRouteGrouped(RouteInfo info, RouteGroup group, int index) { 573 for (CallbackInfo cbi : sStatic.mCallbacks) { 574 if ((cbi.type & group.mSupportedTypes) != 0) { 575 cbi.cb.onRouteGrouped(cbi.router, info, group, index); 576 } 577 } 578 } 579 580 static void dispatchRouteUngrouped(RouteInfo info, RouteGroup group) { 581 for (CallbackInfo cbi : sStatic.mCallbacks) { 582 if ((cbi.type & group.mSupportedTypes) != 0) { 583 cbi.cb.onRouteUngrouped(cbi.router, info, group); 584 } 585 } 586 } 587 588 static void dispatchRouteVolumeChanged(RouteInfo info) { 589 for (CallbackInfo cbi : sStatic.mCallbacks) { 590 if ((cbi.type & info.mSupportedTypes) != 0) { 591 cbi.cb.onRouteVolumeChanged(cbi.router, info); 592 } 593 } 594 } 595 596 static void systemVolumeChanged(int newValue) { 597 final RouteInfo selectedRoute = sStatic.mSelectedRoute; 598 if (selectedRoute == null) return; 599 600 if (selectedRoute == sStatic.mBluetoothA2dpRoute || 601 selectedRoute == sStatic.mDefaultAudio) { 602 dispatchRouteVolumeChanged(selectedRoute); 603 } else if (sStatic.mBluetoothA2dpRoute != null) { 604 try { 605 dispatchRouteVolumeChanged(sStatic.mAudioService.isBluetoothA2dpOn() ? 606 sStatic.mBluetoothA2dpRoute : sStatic.mDefaultAudio); 607 } catch (RemoteException e) { 608 Log.e(TAG, "Error checking Bluetooth A2DP state to report volume change", e); 609 } 610 } else { 611 dispatchRouteVolumeChanged(sStatic.mDefaultAudio); 612 } 613 } 614 615 /** 616 * Information about a media route. 617 */ 618 public static class RouteInfo { 619 CharSequence mName; 620 int mNameResId; 621 private CharSequence mStatus; 622 int mSupportedTypes; 623 RouteGroup mGroup; 624 final RouteCategory mCategory; 625 Drawable mIcon; 626 // playback information 627 int mPlaybackType = PLAYBACK_TYPE_LOCAL; 628 int mVolumeMax = RemoteControlClient.DEFAULT_PLAYBACK_VOLUME; 629 int mVolume = RemoteControlClient.DEFAULT_PLAYBACK_VOLUME; 630 int mVolumeHandling = RemoteControlClient.DEFAULT_PLAYBACK_VOLUME_HANDLING; 631 int mPlaybackStream = AudioManager.STREAM_MUSIC; 632 VolumeCallbackInfo mVcb; 633 634 private Object mTag; 635 636 /** 637 * The default playback type, "local", indicating the presentation of the media is happening 638 * on the same device (e.g. a phone, a tablet) as where it is controlled from. 639 * @see #setPlaybackType(int) 640 */ 641 public final static int PLAYBACK_TYPE_LOCAL = 0; 642 /** 643 * A playback type indicating the presentation of the media is happening on 644 * a different device (i.e. the remote device) than where it is controlled from. 645 * @see #setPlaybackType(int) 646 */ 647 public final static int PLAYBACK_TYPE_REMOTE = 1; 648 /** 649 * Playback information indicating the playback volume is fixed, i.e. it cannot be 650 * controlled from this object. An example of fixed playback volume is a remote player, 651 * playing over HDMI where the user prefers to control the volume on the HDMI sink, rather 652 * than attenuate at the source. 653 * @see #setVolumeHandling(int) 654 */ 655 public final static int PLAYBACK_VOLUME_FIXED = 0; 656 /** 657 * Playback information indicating the playback volume is variable and can be controlled 658 * from this object. 659 */ 660 public final static int PLAYBACK_VOLUME_VARIABLE = 1; 661 662 RouteInfo(RouteCategory category) { 663 mCategory = category; 664 } 665 666 /** 667 * @return The user-friendly name of a media route. This is the string presented 668 * to users who may select this as the active route. 669 */ 670 public CharSequence getName() { 671 return getName(sStatic.mResources); 672 } 673 674 /** 675 * Return the properly localized/resource selected name of this route. 676 * 677 * @param context Context used to resolve the correct configuration to load 678 * @return The user-friendly name of the media route. This is the string presented 679 * to users who may select this as the active route. 680 */ 681 public CharSequence getName(Context context) { 682 return getName(context.getResources()); 683 } 684 685 CharSequence getName(Resources res) { 686 if (mNameResId != 0) { 687 return mName = res.getText(mNameResId); 688 } 689 return mName; 690 } 691 692 /** 693 * @return The user-friendly status for a media route. This may include a description 694 * of the currently playing media, if available. 695 */ 696 public CharSequence getStatus() { 697 return mStatus; 698 } 699 700 /** 701 * @return A media type flag set describing which types this route supports. 702 */ 703 public int getSupportedTypes() { 704 return mSupportedTypes; 705 } 706 707 /** 708 * @return The group that this route belongs to. 709 */ 710 public RouteGroup getGroup() { 711 return mGroup; 712 } 713 714 /** 715 * @return the category this route belongs to. 716 */ 717 public RouteCategory getCategory() { 718 return mCategory; 719 } 720 721 /** 722 * Get the icon representing this route. 723 * This icon will be used in picker UIs if available. 724 * 725 * @return the icon representing this route or null if no icon is available 726 */ 727 public Drawable getIconDrawable() { 728 return mIcon; 729 } 730 731 /** 732 * Set an application-specific tag object for this route. 733 * The application may use this to store arbitrary data associated with the 734 * route for internal tracking. 735 * 736 * <p>Note that the lifespan of a route may be well past the lifespan of 737 * an Activity or other Context; take care that objects you store here 738 * will not keep more data in memory alive than you intend.</p> 739 * 740 * @param tag Arbitrary, app-specific data for this route to hold for later use 741 */ 742 public void setTag(Object tag) { 743 mTag = tag; 744 routeUpdated(); 745 } 746 747 /** 748 * @return The tag object previously set by the application 749 * @see #setTag(Object) 750 */ 751 public Object getTag() { 752 return mTag; 753 } 754 755 /** 756 * @return the type of playback associated with this route 757 * @see UserRouteInfo#setPlaybackType(int) 758 */ 759 public int getPlaybackType() { 760 return mPlaybackType; 761 } 762 763 /** 764 * @return the stream over which the playback associated with this route is performed 765 * @see UserRouteInfo#setPlaybackStream(int) 766 */ 767 public int getPlaybackStream() { 768 return mPlaybackStream; 769 } 770 771 /** 772 * Return the current volume for this route. Depending on the route, this may only 773 * be valid if the route is currently selected. 774 * 775 * @return the volume at which the playback associated with this route is performed 776 * @see UserRouteInfo#setVolume(int) 777 */ 778 public int getVolume() { 779 if (mPlaybackType == PLAYBACK_TYPE_LOCAL) { 780 int vol = 0; 781 try { 782 vol = sStatic.mAudioService.getStreamVolume(mPlaybackStream); 783 } catch (RemoteException e) { 784 Log.e(TAG, "Error getting local stream volume", e); 785 } 786 return vol; 787 } else { 788 return mVolume; 789 } 790 } 791 792 /** 793 * Request a volume change for this route. 794 * @param volume value between 0 and getVolumeMax 795 */ 796 public void requestSetVolume(int volume) { 797 if (mPlaybackType == PLAYBACK_TYPE_LOCAL) { 798 try { 799 sStatic.mAudioService.setStreamVolume(mPlaybackStream, volume, 0); 800 } catch (RemoteException e) { 801 Log.e(TAG, "Error setting local stream volume", e); 802 } 803 } else { 804 Log.e(TAG, getClass().getSimpleName() + ".requestSetVolume(): " + 805 "Non-local volume playback on system route? " + 806 "Could not request volume change."); 807 } 808 } 809 810 /** 811 * Request an incremental volume update for this route. 812 * @param direction Delta to apply to the current volume 813 */ 814 public void requestUpdateVolume(int direction) { 815 if (mPlaybackType == PLAYBACK_TYPE_LOCAL) { 816 try { 817 final int volume = 818 Math.max(0, Math.min(getVolume() + direction, getVolumeMax())); 819 sStatic.mAudioService.setStreamVolume(mPlaybackStream, volume, 0); 820 } catch (RemoteException e) { 821 Log.e(TAG, "Error setting local stream volume", e); 822 } 823 } else { 824 Log.e(TAG, getClass().getSimpleName() + ".requestChangeVolume(): " + 825 "Non-local volume playback on system route? " + 826 "Could not request volume change."); 827 } 828 } 829 830 /** 831 * @return the maximum volume at which the playback associated with this route is performed 832 * @see UserRouteInfo#setVolumeMax(int) 833 */ 834 public int getVolumeMax() { 835 if (mPlaybackType == PLAYBACK_TYPE_LOCAL) { 836 int volMax = 0; 837 try { 838 volMax = sStatic.mAudioService.getStreamMaxVolume(mPlaybackStream); 839 } catch (RemoteException e) { 840 Log.e(TAG, "Error getting local stream volume", e); 841 } 842 return volMax; 843 } else { 844 return mVolumeMax; 845 } 846 } 847 848 /** 849 * @return how volume is handling on the route 850 * @see UserRouteInfo#setVolumeHandling(int) 851 */ 852 public int getVolumeHandling() { 853 return mVolumeHandling; 854 } 855 856 void setStatusInt(CharSequence status) { 857 if (!status.equals(mStatus)) { 858 mStatus = status; 859 if (mGroup != null) { 860 mGroup.memberStatusChanged(this, status); 861 } 862 routeUpdated(); 863 } 864 } 865 866 final IRemoteVolumeObserver.Stub mRemoteVolObserver = new IRemoteVolumeObserver.Stub() { 867 public void dispatchRemoteVolumeUpdate(final int direction, final int value) { 868 sStatic.mHandler.post(new Runnable() { 869 @Override 870 public void run() { 871 //Log.d(TAG, "dispatchRemoteVolumeUpdate dir=" + direction + " val=" + value); 872 if (mVcb != null) { 873 if (direction != 0) { 874 mVcb.vcb.onVolumeUpdateRequest(mVcb.route, direction); 875 } else { 876 mVcb.vcb.onVolumeSetRequest(mVcb.route, value); 877 } 878 } 879 } 880 }); 881 } 882 }; 883 884 void routeUpdated() { 885 updateRoute(this); 886 } 887 888 @Override 889 public String toString() { 890 String supportedTypes = typesToString(getSupportedTypes()); 891 return getClass().getSimpleName() + "{ name=" + getName() + ", status=" + getStatus() + 892 " category=" + getCategory() + 893 " supportedTypes=" + supportedTypes + "}"; 894 } 895 } 896 897 /** 898 * Information about a route that the application may define and modify. 899 * A user route defaults to {@link RouteInfo#PLAYBACK_TYPE_REMOTE} and 900 * {@link RouteInfo#PLAYBACK_VOLUME_FIXED}. 901 * 902 * @see MediaRouter.RouteInfo 903 */ 904 public static class UserRouteInfo extends RouteInfo { 905 RemoteControlClient mRcc; 906 907 UserRouteInfo(RouteCategory category) { 908 super(category); 909 mSupportedTypes = ROUTE_TYPE_USER; 910 mPlaybackType = PLAYBACK_TYPE_REMOTE; 911 mVolumeHandling = PLAYBACK_VOLUME_FIXED; 912 } 913 914 /** 915 * Set the user-visible name of this route. 916 * @param name Name to display to the user to describe this route 917 */ 918 public void setName(CharSequence name) { 919 mName = name; 920 routeUpdated(); 921 } 922 923 /** 924 * Set the user-visible name of this route. 925 * @param resId Resource ID of the name to display to the user to describe this route 926 */ 927 public void setName(int resId) { 928 mNameResId = resId; 929 mName = null; 930 routeUpdated(); 931 } 932 933 /** 934 * Set the current user-visible status for this route. 935 * @param status Status to display to the user to describe what the endpoint 936 * of this route is currently doing 937 */ 938 public void setStatus(CharSequence status) { 939 setStatusInt(status); 940 } 941 942 /** 943 * Set the RemoteControlClient responsible for reporting playback info for this 944 * user route. 945 * 946 * <p>If this route manages remote playback, the data exposed by this 947 * RemoteControlClient will be used to reflect and update information 948 * such as route volume info in related UIs.</p> 949 * 950 * <p>The RemoteControlClient must have been previously registered with 951 * {@link AudioManager#registerRemoteControlClient(RemoteControlClient)}.</p> 952 * 953 * @param rcc RemoteControlClient associated with this route 954 */ 955 public void setRemoteControlClient(RemoteControlClient rcc) { 956 mRcc = rcc; 957 updatePlaybackInfoOnRcc(); 958 } 959 960 /** 961 * Retrieve the RemoteControlClient associated with this route, if one has been set. 962 * 963 * @return the RemoteControlClient associated with this route 964 * @see #setRemoteControlClient(RemoteControlClient) 965 */ 966 public RemoteControlClient getRemoteControlClient() { 967 return mRcc; 968 } 969 970 /** 971 * Set an icon that will be used to represent this route. 972 * The system may use this icon in picker UIs or similar. 973 * 974 * @param icon icon drawable to use to represent this route 975 */ 976 public void setIconDrawable(Drawable icon) { 977 mIcon = icon; 978 } 979 980 /** 981 * Set an icon that will be used to represent this route. 982 * The system may use this icon in picker UIs or similar. 983 * 984 * @param resId Resource ID of an icon drawable to use to represent this route 985 */ 986 public void setIconResource(int resId) { 987 setIconDrawable(sStatic.mResources.getDrawable(resId)); 988 } 989 990 /** 991 * Set a callback to be notified of volume update requests 992 * @param vcb 993 */ 994 public void setVolumeCallback(VolumeCallback vcb) { 995 mVcb = new VolumeCallbackInfo(vcb, this); 996 } 997 998 /** 999 * Defines whether playback associated with this route is "local" 1000 * ({@link RouteInfo#PLAYBACK_TYPE_LOCAL}) or "remote" 1001 * ({@link RouteInfo#PLAYBACK_TYPE_REMOTE}). 1002 * @param type 1003 */ 1004 public void setPlaybackType(int type) { 1005 if (mPlaybackType != type) { 1006 mPlaybackType = type; 1007 setPlaybackInfoOnRcc(RemoteControlClient.PLAYBACKINFO_PLAYBACK_TYPE, type); 1008 } 1009 } 1010 1011 /** 1012 * Defines whether volume for the playback associated with this route is fixed 1013 * ({@link RouteInfo#PLAYBACK_VOLUME_FIXED}) or can modified 1014 * ({@link RouteInfo#PLAYBACK_VOLUME_VARIABLE}). 1015 * @param volumeHandling 1016 */ 1017 public void setVolumeHandling(int volumeHandling) { 1018 if (mVolumeHandling != volumeHandling) { 1019 mVolumeHandling = volumeHandling; 1020 setPlaybackInfoOnRcc( 1021 RemoteControlClient.PLAYBACKINFO_VOLUME_HANDLING, volumeHandling); 1022 } 1023 } 1024 1025 /** 1026 * Defines at what volume the playback associated with this route is performed (for user 1027 * feedback purposes). This information is only used when the playback is not local. 1028 * @param volume 1029 */ 1030 public void setVolume(int volume) { 1031 volume = Math.max(0, Math.min(volume, getVolumeMax())); 1032 if (mVolume != volume) { 1033 mVolume = volume; 1034 setPlaybackInfoOnRcc(RemoteControlClient.PLAYBACKINFO_VOLUME, volume); 1035 dispatchRouteVolumeChanged(this); 1036 if (mGroup != null) { 1037 mGroup.memberVolumeChanged(this); 1038 } 1039 } 1040 } 1041 1042 @Override 1043 public void requestSetVolume(int volume) { 1044 if (mVolumeHandling == PLAYBACK_VOLUME_VARIABLE) { 1045 if (mVcb == null) { 1046 Log.e(TAG, "Cannot requestSetVolume on user route - no volume callback set"); 1047 return; 1048 } 1049 mVcb.vcb.onVolumeSetRequest(this, volume); 1050 } 1051 } 1052 1053 @Override 1054 public void requestUpdateVolume(int direction) { 1055 if (mVolumeHandling == PLAYBACK_VOLUME_VARIABLE) { 1056 if (mVcb == null) { 1057 Log.e(TAG, "Cannot requestChangeVolume on user route - no volumec callback set"); 1058 return; 1059 } 1060 mVcb.vcb.onVolumeUpdateRequest(this, direction); 1061 } 1062 } 1063 1064 /** 1065 * Defines the maximum volume at which the playback associated with this route is performed 1066 * (for user feedback purposes). This information is only used when the playback is not 1067 * local. 1068 * @param volumeMax 1069 */ 1070 public void setVolumeMax(int volumeMax) { 1071 if (mVolumeMax != volumeMax) { 1072 mVolumeMax = volumeMax; 1073 setPlaybackInfoOnRcc(RemoteControlClient.PLAYBACKINFO_VOLUME_MAX, volumeMax); 1074 } 1075 } 1076 1077 /** 1078 * Defines over what stream type the media is presented. 1079 * @param stream 1080 */ 1081 public void setPlaybackStream(int stream) { 1082 if (mPlaybackStream != stream) { 1083 mPlaybackStream = stream; 1084 setPlaybackInfoOnRcc(RemoteControlClient.PLAYBACKINFO_USES_STREAM, stream); 1085 } 1086 } 1087 1088 private void updatePlaybackInfoOnRcc() { 1089 if ((mRcc != null) && (mRcc.getRcseId() != RemoteControlClient.RCSE_ID_UNREGISTERED)) { 1090 mRcc.setPlaybackInformation( 1091 RemoteControlClient.PLAYBACKINFO_VOLUME_MAX, mVolumeMax); 1092 mRcc.setPlaybackInformation( 1093 RemoteControlClient.PLAYBACKINFO_VOLUME, mVolume); 1094 mRcc.setPlaybackInformation( 1095 RemoteControlClient.PLAYBACKINFO_VOLUME_HANDLING, mVolumeHandling); 1096 mRcc.setPlaybackInformation( 1097 RemoteControlClient.PLAYBACKINFO_USES_STREAM, mPlaybackStream); 1098 mRcc.setPlaybackInformation( 1099 RemoteControlClient.PLAYBACKINFO_PLAYBACK_TYPE, mPlaybackType); 1100 // let AudioService know whom to call when remote volume needs to be updated 1101 try { 1102 sStatic.mAudioService.registerRemoteVolumeObserverForRcc( 1103 mRcc.getRcseId() /* rccId */, mRemoteVolObserver /* rvo */); 1104 } catch (RemoteException e) { 1105 Log.e(TAG, "Error registering remote volume observer", e); 1106 } 1107 } 1108 } 1109 1110 private void setPlaybackInfoOnRcc(int what, int value) { 1111 if (mRcc != null) { 1112 mRcc.setPlaybackInformation(what, value); 1113 } 1114 } 1115 } 1116 1117 /** 1118 * Information about a route that consists of multiple other routes in a group. 1119 */ 1120 public static class RouteGroup extends RouteInfo { 1121 final ArrayList<RouteInfo> mRoutes = new ArrayList<RouteInfo>(); 1122 private boolean mUpdateName; 1123 1124 RouteGroup(RouteCategory category) { 1125 super(category); 1126 mGroup = this; 1127 mVolumeHandling = PLAYBACK_VOLUME_FIXED; 1128 } 1129 1130 CharSequence getName(Resources res) { 1131 if (mUpdateName) updateName(); 1132 return super.getName(res); 1133 } 1134 1135 /** 1136 * Add a route to this group. The route must not currently belong to another group. 1137 * 1138 * @param route route to add to this group 1139 */ 1140 public void addRoute(RouteInfo route) { 1141 if (route.getGroup() != null) { 1142 throw new IllegalStateException("Route " + route + " is already part of a group."); 1143 } 1144 if (route.getCategory() != mCategory) { 1145 throw new IllegalArgumentException( 1146 "Route cannot be added to a group with a different category. " + 1147 "(Route category=" + route.getCategory() + 1148 " group category=" + mCategory + ")"); 1149 } 1150 final int at = mRoutes.size(); 1151 mRoutes.add(route); 1152 route.mGroup = this; 1153 mUpdateName = true; 1154 updateVolume(); 1155 dispatchRouteGrouped(route, this, at); 1156 routeUpdated(); 1157 } 1158 1159 /** 1160 * Add a route to this group before the specified index. 1161 * 1162 * @param route route to add 1163 * @param insertAt insert the new route before this index 1164 */ 1165 public void addRoute(RouteInfo route, int insertAt) { 1166 if (route.getGroup() != null) { 1167 throw new IllegalStateException("Route " + route + " is already part of a group."); 1168 } 1169 if (route.getCategory() != mCategory) { 1170 throw new IllegalArgumentException( 1171 "Route cannot be added to a group with a different category. " + 1172 "(Route category=" + route.getCategory() + 1173 " group category=" + mCategory + ")"); 1174 } 1175 mRoutes.add(insertAt, route); 1176 route.mGroup = this; 1177 mUpdateName = true; 1178 updateVolume(); 1179 dispatchRouteGrouped(route, this, insertAt); 1180 routeUpdated(); 1181 } 1182 1183 /** 1184 * Remove a route from this group. 1185 * 1186 * @param route route to remove 1187 */ 1188 public void removeRoute(RouteInfo route) { 1189 if (route.getGroup() != this) { 1190 throw new IllegalArgumentException("Route " + route + 1191 " is not a member of this group."); 1192 } 1193 mRoutes.remove(route); 1194 route.mGroup = null; 1195 mUpdateName = true; 1196 updateVolume(); 1197 dispatchRouteUngrouped(route, this); 1198 routeUpdated(); 1199 } 1200 1201 /** 1202 * Remove the route at the specified index from this group. 1203 * 1204 * @param index index of the route to remove 1205 */ 1206 public void removeRoute(int index) { 1207 RouteInfo route = mRoutes.remove(index); 1208 route.mGroup = null; 1209 mUpdateName = true; 1210 updateVolume(); 1211 dispatchRouteUngrouped(route, this); 1212 routeUpdated(); 1213 } 1214 1215 /** 1216 * @return The number of routes in this group 1217 */ 1218 public int getRouteCount() { 1219 return mRoutes.size(); 1220 } 1221 1222 /** 1223 * Return the route in this group at the specified index 1224 * 1225 * @param index Index to fetch 1226 * @return The route at index 1227 */ 1228 public RouteInfo getRouteAt(int index) { 1229 return mRoutes.get(index); 1230 } 1231 1232 /** 1233 * Set an icon that will be used to represent this group. 1234 * The system may use this icon in picker UIs or similar. 1235 * 1236 * @param icon icon drawable to use to represent this group 1237 */ 1238 public void setIconDrawable(Drawable icon) { 1239 mIcon = icon; 1240 } 1241 1242 /** 1243 * Set an icon that will be used to represent this group. 1244 * The system may use this icon in picker UIs or similar. 1245 * 1246 * @param resId Resource ID of an icon drawable to use to represent this group 1247 */ 1248 public void setIconResource(int resId) { 1249 setIconDrawable(sStatic.mResources.getDrawable(resId)); 1250 } 1251 1252 @Override 1253 public void requestSetVolume(int volume) { 1254 final int maxVol = getVolumeMax(); 1255 if (maxVol == 0) { 1256 return; 1257 } 1258 1259 final float scaledVolume = (float) volume / maxVol; 1260 final int routeCount = getRouteCount(); 1261 for (int i = 0; i < routeCount; i++) { 1262 final RouteInfo route = getRouteAt(i); 1263 final int routeVol = (int) (scaledVolume * route.getVolumeMax()); 1264 route.requestSetVolume(routeVol); 1265 } 1266 if (volume != mVolume) { 1267 mVolume = volume; 1268 dispatchRouteVolumeChanged(this); 1269 } 1270 } 1271 1272 @Override 1273 public void requestUpdateVolume(int direction) { 1274 final int maxVol = getVolumeMax(); 1275 if (maxVol == 0) { 1276 return; 1277 } 1278 1279 final int routeCount = getRouteCount(); 1280 int volume = 0; 1281 for (int i = 0; i < routeCount; i++) { 1282 final RouteInfo route = getRouteAt(i); 1283 route.requestUpdateVolume(direction); 1284 final int routeVol = route.getVolume(); 1285 if (routeVol > volume) { 1286 volume = routeVol; 1287 } 1288 } 1289 if (volume != mVolume) { 1290 mVolume = volume; 1291 dispatchRouteVolumeChanged(this); 1292 } 1293 } 1294 1295 void memberNameChanged(RouteInfo info, CharSequence name) { 1296 mUpdateName = true; 1297 routeUpdated(); 1298 } 1299 1300 void memberStatusChanged(RouteInfo info, CharSequence status) { 1301 setStatusInt(status); 1302 } 1303 1304 void memberVolumeChanged(RouteInfo info) { 1305 updateVolume(); 1306 } 1307 1308 void updateVolume() { 1309 // A group always represents the highest component volume value. 1310 final int routeCount = getRouteCount(); 1311 int volume = 0; 1312 for (int i = 0; i < routeCount; i++) { 1313 final int routeVol = getRouteAt(i).getVolume(); 1314 if (routeVol > volume) { 1315 volume = routeVol; 1316 } 1317 } 1318 if (volume != mVolume) { 1319 mVolume = volume; 1320 dispatchRouteVolumeChanged(this); 1321 } 1322 } 1323 1324 @Override 1325 void routeUpdated() { 1326 int types = 0; 1327 final int count = mRoutes.size(); 1328 if (count == 0) { 1329 // Don't keep empty groups in the router. 1330 MediaRouter.removeRoute(this); 1331 return; 1332 } 1333 1334 int maxVolume = 0; 1335 boolean isLocal = true; 1336 boolean isFixedVolume = true; 1337 for (int i = 0; i < count; i++) { 1338 final RouteInfo route = mRoutes.get(i); 1339 types |= route.mSupportedTypes; 1340 final int routeMaxVolume = route.getVolumeMax(); 1341 if (routeMaxVolume > maxVolume) { 1342 maxVolume = routeMaxVolume; 1343 } 1344 isLocal &= route.getPlaybackType() == PLAYBACK_TYPE_LOCAL; 1345 isFixedVolume &= route.getVolumeHandling() == PLAYBACK_VOLUME_FIXED; 1346 } 1347 mPlaybackType = isLocal ? PLAYBACK_TYPE_LOCAL : PLAYBACK_TYPE_REMOTE; 1348 mVolumeHandling = isFixedVolume ? PLAYBACK_VOLUME_FIXED : PLAYBACK_VOLUME_VARIABLE; 1349 mSupportedTypes = types; 1350 mVolumeMax = maxVolume; 1351 mIcon = count == 1 ? mRoutes.get(0).getIconDrawable() : null; 1352 super.routeUpdated(); 1353 } 1354 1355 void updateName() { 1356 final StringBuilder sb = new StringBuilder(); 1357 final int count = mRoutes.size(); 1358 for (int i = 0; i < count; i++) { 1359 final RouteInfo info = mRoutes.get(i); 1360 // TODO: There's probably a much more correct way to localize this. 1361 if (i > 0) sb.append(", "); 1362 sb.append(info.mName); 1363 } 1364 mName = sb.toString(); 1365 mUpdateName = false; 1366 } 1367 1368 @Override 1369 public String toString() { 1370 StringBuilder sb = new StringBuilder(super.toString()); 1371 sb.append('['); 1372 final int count = mRoutes.size(); 1373 for (int i = 0; i < count; i++) { 1374 if (i > 0) sb.append(", "); 1375 sb.append(mRoutes.get(i)); 1376 } 1377 sb.append(']'); 1378 return sb.toString(); 1379 } 1380 } 1381 1382 /** 1383 * Definition of a category of routes. All routes belong to a category. 1384 */ 1385 public static class RouteCategory { 1386 CharSequence mName; 1387 int mNameResId; 1388 int mTypes; 1389 final boolean mGroupable; 1390 1391 RouteCategory(CharSequence name, int types, boolean groupable) { 1392 mName = name; 1393 mTypes = types; 1394 mGroupable = groupable; 1395 } 1396 1397 RouteCategory(int nameResId, int types, boolean groupable) { 1398 mNameResId = nameResId; 1399 mTypes = types; 1400 mGroupable = groupable; 1401 } 1402 1403 /** 1404 * @return the name of this route category 1405 */ 1406 public CharSequence getName() { 1407 return getName(sStatic.mResources); 1408 } 1409 1410 /** 1411 * Return the properly localized/configuration dependent name of this RouteCategory. 1412 * 1413 * @param context Context to resolve name resources 1414 * @return the name of this route category 1415 */ 1416 public CharSequence getName(Context context) { 1417 return getName(context.getResources()); 1418 } 1419 1420 CharSequence getName(Resources res) { 1421 if (mNameResId != 0) { 1422 return res.getText(mNameResId); 1423 } 1424 return mName; 1425 } 1426 1427 /** 1428 * Return the current list of routes in this category that have been added 1429 * to the MediaRouter. 1430 * 1431 * <p>This list will not include routes that are nested within RouteGroups. 1432 * A RouteGroup is treated as a single route within its category.</p> 1433 * 1434 * @param out a List to fill with the routes in this category. If this parameter is 1435 * non-null, it will be cleared, filled with the current routes with this 1436 * category, and returned. If this parameter is null, a new List will be 1437 * allocated to report the category's current routes. 1438 * @return A list with the routes in this category that have been added to the MediaRouter. 1439 */ 1440 public List<RouteInfo> getRoutes(List<RouteInfo> out) { 1441 if (out == null) { 1442 out = new ArrayList<RouteInfo>(); 1443 } else { 1444 out.clear(); 1445 } 1446 1447 final int count = getRouteCountStatic(); 1448 for (int i = 0; i < count; i++) { 1449 final RouteInfo route = getRouteAtStatic(i); 1450 if (route.mCategory == this) { 1451 out.add(route); 1452 } 1453 } 1454 return out; 1455 } 1456 1457 /** 1458 * @return Flag set describing the route types supported by this category 1459 */ 1460 public int getSupportedTypes() { 1461 return mTypes; 1462 } 1463 1464 /** 1465 * Return whether or not this category supports grouping. 1466 * 1467 * <p>If this method returns true, all routes obtained from this category 1468 * via calls to {@link #getRouteAt(int)} will be {@link MediaRouter.RouteGroup}s.</p> 1469 * 1470 * @return true if this category supports 1471 */ 1472 public boolean isGroupable() { 1473 return mGroupable; 1474 } 1475 1476 public String toString() { 1477 return "RouteCategory{ name=" + mName + " types=" + typesToString(mTypes) + 1478 " groupable=" + mGroupable + " }"; 1479 } 1480 } 1481 1482 static class CallbackInfo { 1483 public int type; 1484 public final Callback cb; 1485 public final MediaRouter router; 1486 1487 public CallbackInfo(Callback cb, int type, MediaRouter router) { 1488 this.cb = cb; 1489 this.type = type; 1490 this.router = router; 1491 } 1492 } 1493 1494 /** 1495 * Interface for receiving events about media routing changes. 1496 * All methods of this interface will be called from the application's main thread. 1497 * 1498 * <p>A Callback will only receive events relevant to routes that the callback 1499 * was registered for.</p> 1500 * 1501 * @see MediaRouter#addCallback(int, Callback) 1502 * @see MediaRouter#removeCallback(Callback) 1503 */ 1504 public static abstract class Callback { 1505 /** 1506 * Called when the supplied route becomes selected as the active route 1507 * for the given route type. 1508 * 1509 * @param router the MediaRouter reporting the event 1510 * @param type Type flag set indicating the routes that have been selected 1511 * @param info Route that has been selected for the given route types 1512 */ 1513 public abstract void onRouteSelected(MediaRouter router, int type, RouteInfo info); 1514 1515 /** 1516 * Called when the supplied route becomes unselected as the active route 1517 * for the given route type. 1518 * 1519 * @param router the MediaRouter reporting the event 1520 * @param type Type flag set indicating the routes that have been unselected 1521 * @param info Route that has been unselected for the given route types 1522 */ 1523 public abstract void onRouteUnselected(MediaRouter router, int type, RouteInfo info); 1524 1525 /** 1526 * Called when a route for the specified type was added. 1527 * 1528 * @param router the MediaRouter reporting the event 1529 * @param info Route that has become available for use 1530 */ 1531 public abstract void onRouteAdded(MediaRouter router, RouteInfo info); 1532 1533 /** 1534 * Called when a route for the specified type was removed. 1535 * 1536 * @param router the MediaRouter reporting the event 1537 * @param info Route that has been removed from availability 1538 */ 1539 public abstract void onRouteRemoved(MediaRouter router, RouteInfo info); 1540 1541 /** 1542 * Called when an aspect of the indicated route has changed. 1543 * 1544 * <p>This will not indicate that the types supported by this route have 1545 * changed, only that cosmetic info such as name or status have been updated.</p> 1546 * 1547 * @param router the MediaRouter reporting the event 1548 * @param info The route that was changed 1549 */ 1550 public abstract void onRouteChanged(MediaRouter router, RouteInfo info); 1551 1552 /** 1553 * Called when a route is added to a group. 1554 * 1555 * @param router the MediaRouter reporting the event 1556 * @param info The route that was added 1557 * @param group The group the route was added to 1558 * @param index The route index within group that info was added at 1559 */ 1560 public abstract void onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group, 1561 int index); 1562 1563 /** 1564 * Called when a route is removed from a group. 1565 * 1566 * @param router the MediaRouter reporting the event 1567 * @param info The route that was removed 1568 * @param group The group the route was removed from 1569 */ 1570 public abstract void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group); 1571 1572 /** 1573 * Called when a route's volume changes. 1574 * 1575 * @param router the MediaRouter reporting the event 1576 * @param info The route with altered volume 1577 */ 1578 public abstract void onRouteVolumeChanged(MediaRouter router, RouteInfo info); 1579 } 1580 1581 /** 1582 * Stub implementation of {@link MediaRouter.Callback}. 1583 * Each abstract method is defined as a no-op. Override just the ones 1584 * you need. 1585 */ 1586 public static class SimpleCallback extends Callback { 1587 1588 @Override 1589 public void onRouteSelected(MediaRouter router, int type, RouteInfo info) { 1590 } 1591 1592 @Override 1593 public void onRouteUnselected(MediaRouter router, int type, RouteInfo info) { 1594 } 1595 1596 @Override 1597 public void onRouteAdded(MediaRouter router, RouteInfo info) { 1598 } 1599 1600 @Override 1601 public void onRouteRemoved(MediaRouter router, RouteInfo info) { 1602 } 1603 1604 @Override 1605 public void onRouteChanged(MediaRouter router, RouteInfo info) { 1606 } 1607 1608 @Override 1609 public void onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group, 1610 int index) { 1611 } 1612 1613 @Override 1614 public void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group) { 1615 } 1616 1617 @Override 1618 public void onRouteVolumeChanged(MediaRouter router, RouteInfo info) { 1619 } 1620 } 1621 1622 static class VolumeCallbackInfo { 1623 public final VolumeCallback vcb; 1624 public final RouteInfo route; 1625 1626 public VolumeCallbackInfo(VolumeCallback vcb, RouteInfo route) { 1627 this.vcb = vcb; 1628 this.route = route; 1629 } 1630 } 1631 1632 /** 1633 * Interface for receiving events about volume changes. 1634 * All methods of this interface will be called from the application's main thread. 1635 * 1636 * <p>A VolumeCallback will only receive events relevant to routes that the callback 1637 * was registered for.</p> 1638 * 1639 * @see UserRouteInfo#setVolumeCallback(VolumeCallback) 1640 */ 1641 public static abstract class VolumeCallback { 1642 /** 1643 * Called when the volume for the route should be increased or decreased. 1644 * @param info the route affected by this event 1645 * @param direction an integer indicating whether the volume is to be increased 1646 * (positive value) or decreased (negative value). 1647 * For bundled changes, the absolute value indicates the number of changes 1648 * in the same direction, e.g. +3 corresponds to three "volume up" changes. 1649 */ 1650 public abstract void onVolumeUpdateRequest(RouteInfo info, int direction); 1651 /** 1652 * Called when the volume for the route should be set to the given value 1653 * @param info the route affected by this event 1654 * @param volume an integer indicating the new volume value that should be used, always 1655 * between 0 and the value set by {@link UserRouteInfo#setVolumeMax(int)}. 1656 */ 1657 public abstract void onVolumeSetRequest(RouteInfo info, int volume); 1658 } 1659 1660 static class VolumeChangeReceiver extends BroadcastReceiver { 1661 1662 @Override 1663 public void onReceive(Context context, Intent intent) { 1664 if (intent.getAction().equals(AudioManager.VOLUME_CHANGED_ACTION)) { 1665 final int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, 1666 -1); 1667 if (streamType != AudioManager.STREAM_MUSIC) { 1668 return; 1669 } 1670 1671 final int newVolume = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, 0); 1672 final int oldVolume = intent.getIntExtra( 1673 AudioManager.EXTRA_PREV_VOLUME_STREAM_VALUE, 0); 1674 if (newVolume != oldVolume) { 1675 systemVolumeChanged(newVolume); 1676 } 1677 } 1678 } 1679 1680 } 1681} 1682