MediaRouter.java revision 2cd5d253786b115470adc04a3609358f5eb7eb0a
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 // playback information 593 int mPlaybackType = PLAYBACK_TYPE_LOCAL; 594 int mVolumeMax = RemoteControlClient.DEFAULT_PLAYBACK_VOLUME; 595 int mVolume = RemoteControlClient.DEFAULT_PLAYBACK_VOLUME; 596 int mVolumeHandling = RemoteControlClient.DEFAULT_PLAYBACK_VOLUME_HANDLING; 597 int mPlaybackStream = AudioManager.STREAM_MUSIC; 598 VolumeCallbackInfo mVcb; 599 600 private Object mTag; 601 602 /** 603 * The default playback type, "local", indicating the presentation of the media is happening 604 * on the same device (e.g. a phone, a tablet) as where it is controlled from. 605 * @see #setPlaybackType(int) 606 */ 607 public final static int PLAYBACK_TYPE_LOCAL = 0; 608 /** 609 * A playback type indicating the presentation of the media is happening on 610 * a different device (i.e. the remote device) than where it is controlled from. 611 * @see #setPlaybackType(int) 612 */ 613 public final static int PLAYBACK_TYPE_REMOTE = 1; 614 /** 615 * Playback information indicating the playback volume is fixed, i.e. it cannot be 616 * controlled from this object. An example of fixed playback volume is a remote player, 617 * playing over HDMI where the user prefers to control the volume on the HDMI sink, rather 618 * than attenuate at the source. 619 * @see #setVolumeHandling(int) 620 */ 621 public final static int PLAYBACK_VOLUME_FIXED = 0; 622 /** 623 * Playback information indicating the playback volume is variable and can be controlled 624 * from this object. 625 */ 626 public final static int PLAYBACK_VOLUME_VARIABLE = 1; 627 628 RouteInfo(RouteCategory category) { 629 mCategory = category; 630 } 631 632 /** 633 * @return The user-friendly name of a media route. This is the string presented 634 * to users who may select this as the active route. 635 */ 636 public CharSequence getName() { 637 return getName(sStatic.mResources); 638 } 639 640 /** 641 * Return the properly localized/resource selected name of this route. 642 * 643 * @param context Context used to resolve the correct configuration to load 644 * @return The user-friendly name of the media route. This is the string presented 645 * to users who may select this as the active route. 646 */ 647 public CharSequence getName(Context context) { 648 return getName(context.getResources()); 649 } 650 651 CharSequence getName(Resources res) { 652 if (mNameResId != 0) { 653 return mName = res.getText(mNameResId); 654 } 655 return mName; 656 } 657 658 /** 659 * @return The user-friendly status for a media route. This may include a description 660 * of the currently playing media, if available. 661 */ 662 public CharSequence getStatus() { 663 return mStatus; 664 } 665 666 /** 667 * @return A media type flag set describing which types this route supports. 668 */ 669 public int getSupportedTypes() { 670 return mSupportedTypes; 671 } 672 673 /** 674 * @return The group that this route belongs to. 675 */ 676 public RouteGroup getGroup() { 677 return mGroup; 678 } 679 680 /** 681 * @return the category this route belongs to. 682 */ 683 public RouteCategory getCategory() { 684 return mCategory; 685 } 686 687 /** 688 * Get the icon representing this route. 689 * This icon will be used in picker UIs if available. 690 * 691 * @return the icon representing this route or null if no icon is available 692 */ 693 public Drawable getIconDrawable() { 694 return mIcon; 695 } 696 697 /** 698 * Set an application-specific tag object for this route. 699 * The application may use this to store arbitrary data associated with the 700 * route for internal tracking. 701 * 702 * <p>Note that the lifespan of a route may be well past the lifespan of 703 * an Activity or other Context; take care that objects you store here 704 * will not keep more data in memory alive than you intend.</p> 705 * 706 * @param tag Arbitrary, app-specific data for this route to hold for later use 707 */ 708 public void setTag(Object tag) { 709 mTag = tag; 710 routeUpdated(); 711 } 712 713 /** 714 * @return The tag object previously set by the application 715 * @see #setTag(Object) 716 */ 717 public Object getTag() { 718 return mTag; 719 } 720 721 /** 722 * @return the type of playback associated with this route 723 * @see UserRouteInfo#setPlaybackType(int) 724 */ 725 public int getPlaybackType() { 726 return mPlaybackType; 727 } 728 729 /** 730 * @return the stream over which the playback associated with this route is performed 731 * @see UserRouteInfo#setPlaybackStream(int) 732 */ 733 public int getPlaybackStream() { 734 return mPlaybackStream; 735 } 736 737 /** 738 * @return the volume at which the playback associated with this route is performed 739 * @see UserRouteInfo#setVolume(int) 740 */ 741 public int getVolume() { 742 if (mPlaybackType == PLAYBACK_TYPE_LOCAL) { 743 int vol = 0; 744 try { 745 vol = sStatic.mAudioService.getStreamVolume(mPlaybackStream); 746 } catch (RemoteException e) { 747 Log.e(TAG, "Error getting local stream volume", e); 748 } 749 return vol; 750 } else { 751 return mVolume; 752 } 753 } 754 755 /** 756 * @return the maximum volume at which the playback associated with this route is performed 757 * @see UserRouteInfo#setVolumeMax(int) 758 */ 759 public int getVolumeMax() { 760 if (mPlaybackType == PLAYBACK_TYPE_LOCAL) { 761 int volMax = 0; 762 try { 763 volMax = sStatic.mAudioService.getStreamMaxVolume(mPlaybackStream); 764 } catch (RemoteException e) { 765 Log.e(TAG, "Error getting local stream volume", e); 766 } 767 return volMax; 768 } else { 769 return mVolumeMax; 770 } 771 } 772 773 /** 774 * @return how volume is handling on the route 775 * @see UserRouteInfo#setVolumeHandling(int) 776 */ 777 public int getVolumeHandling() { 778 return mVolumeHandling; 779 } 780 781 void setStatusInt(CharSequence status) { 782 if (!status.equals(mStatus)) { 783 mStatus = status; 784 if (mGroup != null) { 785 mGroup.memberStatusChanged(this, status); 786 } 787 routeUpdated(); 788 } 789 } 790 791 final IRemoteVolumeObserver.Stub mRemoteVolObserver = new IRemoteVolumeObserver.Stub() { 792 public void dispatchRemoteVolumeUpdate(final int direction, final int value) { 793 sStatic.mHandler.post(new Runnable() { 794 @Override 795 public void run() { 796 //Log.d(TAG, "dispatchRemoteVolumeUpdate dir=" + direction + " val=" + value); 797 if (mVcb != null) { 798 if (direction != 0) { 799 mVcb.vcb.onVolumeUpdateRequest(mVcb.route, direction); 800 } else { 801 mVcb.vcb.onVolumeSetRequest(mVcb.route, value); 802 } 803 } 804 } 805 }); 806 } 807 }; 808 809 void routeUpdated() { 810 updateRoute(this); 811 } 812 813 @Override 814 public String toString() { 815 String supportedTypes = typesToString(getSupportedTypes()); 816 return getClass().getSimpleName() + "{ name=" + getName() + ", status=" + getStatus() + 817 " category=" + getCategory() + 818 " supportedTypes=" + supportedTypes + "}"; 819 } 820 } 821 822 /** 823 * Information about a route that the application may define and modify. 824 * 825 * @see MediaRouter.RouteInfo 826 */ 827 public static class UserRouteInfo extends RouteInfo { 828 RemoteControlClient mRcc; 829 830 UserRouteInfo(RouteCategory category) { 831 super(category); 832 mSupportedTypes = ROUTE_TYPE_USER; 833 } 834 835 /** 836 * Set the user-visible name of this route. 837 * @param name Name to display to the user to describe this route 838 */ 839 public void setName(CharSequence name) { 840 mName = name; 841 routeUpdated(); 842 } 843 844 /** 845 * Set the user-visible name of this route. 846 * @param resId Resource ID of the name to display to the user to describe this route 847 */ 848 public void setName(int resId) { 849 mNameResId = resId; 850 mName = null; 851 routeUpdated(); 852 } 853 854 /** 855 * Set the current user-visible status for this route. 856 * @param status Status to display to the user to describe what the endpoint 857 * of this route is currently doing 858 */ 859 public void setStatus(CharSequence status) { 860 setStatusInt(status); 861 } 862 863 /** 864 * Set the RemoteControlClient responsible for reporting playback info for this 865 * user route. 866 * 867 * <p>If this route manages remote playback, the data exposed by this 868 * RemoteControlClient will be used to reflect and update information 869 * such as route volume info in related UIs.</p> 870 * 871 * <p>The RemoteControlClient must have been previously registered with 872 * {@link AudioManager#registerRemoteControlClient(RemoteControlClient)}.</p> 873 * 874 * @param rcc RemoteControlClient associated with this route 875 */ 876 public void setRemoteControlClient(RemoteControlClient rcc) { 877 mRcc = rcc; 878 updatePlaybackInfoOnRcc(); 879 } 880 881 /** 882 * Retrieve the RemoteControlClient associated with this route, if one has been set. 883 * 884 * @return the RemoteControlClient associated with this route 885 * @see #setRemoteControlClient(RemoteControlClient) 886 */ 887 public RemoteControlClient getRemoteControlClient() { 888 return mRcc; 889 } 890 891 /** 892 * Set an icon that will be used to represent this route. 893 * The system may use this icon in picker UIs or similar. 894 * 895 * @param icon icon drawable to use to represent this route 896 */ 897 public void setIconDrawable(Drawable icon) { 898 mIcon = icon; 899 } 900 901 /** 902 * Set an icon that will be used to represent this route. 903 * The system may use this icon in picker UIs or similar. 904 * 905 * @param resId Resource ID of an icon drawable to use to represent this route 906 */ 907 public void setIconResource(int resId) { 908 setIconDrawable(sStatic.mResources.getDrawable(resId)); 909 } 910 911 /** 912 * Set a callback to be notified of volume update requests 913 * @param vcb 914 */ 915 public void setVolumeCallback(VolumeCallback vcb) { 916 mVcb = new VolumeCallbackInfo(vcb, this); 917 } 918 919 /** 920 * Defines whether playback associated with this route is "local" 921 * ({@link RouteInfo#PLAYBACK_TYPE_LOCAL}) or "remote" 922 * ({@link RouteInfo#PLAYBACK_TYPE_REMOTE}). 923 * @param type 924 */ 925 public void setPlaybackType(int type) { 926 if (mPlaybackType != type) { 927 mPlaybackType = type; 928 setPlaybackInfoOnRcc(RemoteControlClient.PLAYBACKINFO_PLAYBACK_TYPE, type); 929 } 930 } 931 932 /** 933 * Defines whether volume for the playback associated with this route is fixed 934 * ({@link RouteInfo#PLAYBACK_VOLUME_FIXED}) or can modified 935 * ({@link RouteInfo#PLAYBACK_VOLUME_VARIABLE}). 936 * @param volumeHandling 937 */ 938 public void setVolumeHandling(int volumeHandling) { 939 if (mVolumeHandling != volumeHandling) { 940 mVolumeHandling = volumeHandling; 941 setPlaybackInfoOnRcc( 942 RemoteControlClient.PLAYBACKINFO_VOLUME_HANDLING, volumeHandling); 943 } 944 } 945 946 /** 947 * Defines at what volume the playback associated with this route is performed (for user 948 * feedback purposes). This information is only used when the playback is not local. 949 * @param volume 950 */ 951 public void setVolume(int volume) { 952 if (mVolume != volume) { 953 mVolume = volume; 954 setPlaybackInfoOnRcc(RemoteControlClient.PLAYBACKINFO_VOLUME, volume); 955 } 956 } 957 958 /** 959 * Defines the maximum volume at which the playback associated with this route is performed 960 * (for user feedback purposes). This information is only used when the playback is not 961 * local. 962 * @param volumeMax 963 */ 964 public void setVolumeMax(int volumeMax) { 965 if (mVolumeMax != volumeMax) { 966 mVolumeMax = volumeMax; 967 setPlaybackInfoOnRcc(RemoteControlClient.PLAYBACKINFO_VOLUME_MAX, volumeMax); 968 } 969 } 970 971 /** 972 * Defines over what stream type the media is presented. 973 * @param stream 974 */ 975 public void setPlaybackStream(int stream) { 976 if (mPlaybackStream != stream) { 977 mPlaybackStream = stream; 978 setPlaybackInfoOnRcc(RemoteControlClient.PLAYBACKINFO_USES_STREAM, stream); 979 } 980 } 981 982 private void updatePlaybackInfoOnRcc() { 983 if ((mRcc != null) && (mRcc.getRcseId() != RemoteControlClient.RCSE_ID_UNREGISTERED)) { 984 mRcc.setPlaybackInformation( 985 RemoteControlClient.PLAYBACKINFO_VOLUME_MAX, mVolumeMax); 986 mRcc.setPlaybackInformation( 987 RemoteControlClient.PLAYBACKINFO_VOLUME, mVolume); 988 mRcc.setPlaybackInformation( 989 RemoteControlClient.PLAYBACKINFO_VOLUME_HANDLING, mVolumeHandling); 990 mRcc.setPlaybackInformation( 991 RemoteControlClient.PLAYBACKINFO_USES_STREAM, mPlaybackStream); 992 mRcc.setPlaybackInformation( 993 RemoteControlClient.PLAYBACKINFO_PLAYBACK_TYPE, mPlaybackType); 994 // let AudioService know whom to call when remote volume needs to be updated 995 try { 996 sStatic.mAudioService.registerRemoteVolumeObserverForRcc( 997 mRcc.getRcseId() /* rccId */, mRemoteVolObserver /* rvo */); 998 } catch (RemoteException e) { 999 Log.e(TAG, "Error registering remote volume observer", e); 1000 } 1001 } 1002 } 1003 1004 private void setPlaybackInfoOnRcc(int what, int value) { 1005 if (mRcc != null) { 1006 mRcc.setPlaybackInformation(what, value); 1007 } 1008 } 1009 } 1010 1011 /** 1012 * Information about a route that consists of multiple other routes in a group. 1013 */ 1014 public static class RouteGroup extends RouteInfo { 1015 final ArrayList<RouteInfo> mRoutes = new ArrayList<RouteInfo>(); 1016 private boolean mUpdateName; 1017 1018 RouteGroup(RouteCategory category) { 1019 super(category); 1020 mGroup = this; 1021 } 1022 1023 CharSequence getName(Resources res) { 1024 if (mUpdateName) updateName(); 1025 return super.getName(res); 1026 } 1027 1028 /** 1029 * Add a route to this group. The route must not currently belong to another group. 1030 * 1031 * @param route route to add to this group 1032 */ 1033 public void addRoute(RouteInfo route) { 1034 if (route.getGroup() != null) { 1035 throw new IllegalStateException("Route " + route + " is already part of a group."); 1036 } 1037 if (route.getCategory() != mCategory) { 1038 throw new IllegalArgumentException( 1039 "Route cannot be added to a group with a different category. " + 1040 "(Route category=" + route.getCategory() + 1041 " group category=" + mCategory + ")"); 1042 } 1043 final int at = mRoutes.size(); 1044 mRoutes.add(route); 1045 route.mGroup = this; 1046 mUpdateName = true; 1047 dispatchRouteGrouped(route, this, at); 1048 routeUpdated(); 1049 } 1050 1051 /** 1052 * Add a route to this group before the specified index. 1053 * 1054 * @param route route to add 1055 * @param insertAt insert the new route before this index 1056 */ 1057 public void addRoute(RouteInfo route, int insertAt) { 1058 if (route.getGroup() != null) { 1059 throw new IllegalStateException("Route " + route + " is already part of a group."); 1060 } 1061 if (route.getCategory() != mCategory) { 1062 throw new IllegalArgumentException( 1063 "Route cannot be added to a group with a different category. " + 1064 "(Route category=" + route.getCategory() + 1065 " group category=" + mCategory + ")"); 1066 } 1067 mRoutes.add(insertAt, route); 1068 route.mGroup = this; 1069 mUpdateName = true; 1070 dispatchRouteGrouped(route, this, insertAt); 1071 routeUpdated(); 1072 } 1073 1074 /** 1075 * Remove a route from this group. 1076 * 1077 * @param route route to remove 1078 */ 1079 public void removeRoute(RouteInfo route) { 1080 if (route.getGroup() != this) { 1081 throw new IllegalArgumentException("Route " + route + 1082 " is not a member of this group."); 1083 } 1084 mRoutes.remove(route); 1085 route.mGroup = null; 1086 mUpdateName = true; 1087 dispatchRouteUngrouped(route, this); 1088 routeUpdated(); 1089 } 1090 1091 /** 1092 * Remove the route at the specified index from this group. 1093 * 1094 * @param index index of the route to remove 1095 */ 1096 public void removeRoute(int index) { 1097 RouteInfo route = mRoutes.remove(index); 1098 route.mGroup = null; 1099 mUpdateName = true; 1100 dispatchRouteUngrouped(route, this); 1101 routeUpdated(); 1102 } 1103 1104 /** 1105 * @return The number of routes in this group 1106 */ 1107 public int getRouteCount() { 1108 return mRoutes.size(); 1109 } 1110 1111 /** 1112 * Return the route in this group at the specified index 1113 * 1114 * @param index Index to fetch 1115 * @return The route at index 1116 */ 1117 public RouteInfo getRouteAt(int index) { 1118 return mRoutes.get(index); 1119 } 1120 1121 /** 1122 * Set an icon that will be used to represent this group. 1123 * The system may use this icon in picker UIs or similar. 1124 * 1125 * @param icon icon drawable to use to represent this group 1126 */ 1127 public void setIconDrawable(Drawable icon) { 1128 mIcon = icon; 1129 } 1130 1131 /** 1132 * Set an icon that will be used to represent this group. 1133 * The system may use this icon in picker UIs or similar. 1134 * 1135 * @param resId Resource ID of an icon drawable to use to represent this group 1136 */ 1137 public void setIconResource(int resId) { 1138 setIconDrawable(sStatic.mResources.getDrawable(resId)); 1139 } 1140 1141 void memberNameChanged(RouteInfo info, CharSequence name) { 1142 mUpdateName = true; 1143 routeUpdated(); 1144 } 1145 1146 void memberStatusChanged(RouteInfo info, CharSequence status) { 1147 setStatusInt(status); 1148 } 1149 1150 @Override 1151 void routeUpdated() { 1152 int types = 0; 1153 final int count = mRoutes.size(); 1154 if (count == 0) { 1155 // Don't keep empty groups in the router. 1156 MediaRouter.removeRoute(this); 1157 return; 1158 } 1159 1160 for (int i = 0; i < count; i++) { 1161 types |= mRoutes.get(i).mSupportedTypes; 1162 } 1163 mSupportedTypes = types; 1164 mIcon = count == 1 ? mRoutes.get(0).getIconDrawable() : null; 1165 super.routeUpdated(); 1166 } 1167 1168 void updateName() { 1169 final StringBuilder sb = new StringBuilder(); 1170 final int count = mRoutes.size(); 1171 for (int i = 0; i < count; i++) { 1172 final RouteInfo info = mRoutes.get(i); 1173 // TODO: There's probably a much more correct way to localize this. 1174 if (i > 0) sb.append(", "); 1175 sb.append(info.mName); 1176 } 1177 mName = sb.toString(); 1178 mUpdateName = false; 1179 } 1180 1181 @Override 1182 public String toString() { 1183 StringBuilder sb = new StringBuilder(super.toString()); 1184 sb.append('['); 1185 final int count = mRoutes.size(); 1186 for (int i = 0; i < count; i++) { 1187 if (i > 0) sb.append(", "); 1188 sb.append(mRoutes.get(i)); 1189 } 1190 sb.append(']'); 1191 return sb.toString(); 1192 } 1193 } 1194 1195 /** 1196 * Definition of a category of routes. All routes belong to a category. 1197 */ 1198 public static class RouteCategory { 1199 CharSequence mName; 1200 int mNameResId; 1201 int mTypes; 1202 final boolean mGroupable; 1203 1204 RouteCategory(CharSequence name, int types, boolean groupable) { 1205 mName = name; 1206 mTypes = types; 1207 mGroupable = groupable; 1208 } 1209 1210 RouteCategory(int nameResId, int types, boolean groupable) { 1211 mNameResId = nameResId; 1212 mTypes = types; 1213 mGroupable = groupable; 1214 } 1215 1216 /** 1217 * @return the name of this route category 1218 */ 1219 public CharSequence getName() { 1220 return getName(sStatic.mResources); 1221 } 1222 1223 /** 1224 * Return the properly localized/configuration dependent name of this RouteCategory. 1225 * 1226 * @param context Context to resolve name resources 1227 * @return the name of this route category 1228 */ 1229 public CharSequence getName(Context context) { 1230 return getName(context.getResources()); 1231 } 1232 1233 CharSequence getName(Resources res) { 1234 if (mNameResId != 0) { 1235 return res.getText(mNameResId); 1236 } 1237 return mName; 1238 } 1239 1240 /** 1241 * Return the current list of routes in this category that have been added 1242 * to the MediaRouter. 1243 * 1244 * <p>This list will not include routes that are nested within RouteGroups. 1245 * A RouteGroup is treated as a single route within its category.</p> 1246 * 1247 * @param out a List to fill with the routes in this category. If this parameter is 1248 * non-null, it will be cleared, filled with the current routes with this 1249 * category, and returned. If this parameter is null, a new List will be 1250 * allocated to report the category's current routes. 1251 * @return A list with the routes in this category that have been added to the MediaRouter. 1252 */ 1253 public List<RouteInfo> getRoutes(List<RouteInfo> out) { 1254 if (out == null) { 1255 out = new ArrayList<RouteInfo>(); 1256 } else { 1257 out.clear(); 1258 } 1259 1260 final int count = getRouteCountStatic(); 1261 for (int i = 0; i < count; i++) { 1262 final RouteInfo route = getRouteAtStatic(i); 1263 if (route.mCategory == this) { 1264 out.add(route); 1265 } 1266 } 1267 return out; 1268 } 1269 1270 /** 1271 * @return Flag set describing the route types supported by this category 1272 */ 1273 public int getSupportedTypes() { 1274 return mTypes; 1275 } 1276 1277 /** 1278 * Return whether or not this category supports grouping. 1279 * 1280 * <p>If this method returns true, all routes obtained from this category 1281 * via calls to {@link #getRouteAt(int)} will be {@link MediaRouter.RouteGroup}s.</p> 1282 * 1283 * @return true if this category supports 1284 */ 1285 public boolean isGroupable() { 1286 return mGroupable; 1287 } 1288 1289 public String toString() { 1290 return "RouteCategory{ name=" + mName + " types=" + typesToString(mTypes) + 1291 " groupable=" + mGroupable + " }"; 1292 } 1293 } 1294 1295 static class CallbackInfo { 1296 public int type; 1297 public final Callback cb; 1298 public final MediaRouter router; 1299 1300 public CallbackInfo(Callback cb, int type, MediaRouter router) { 1301 this.cb = cb; 1302 this.type = type; 1303 this.router = router; 1304 } 1305 } 1306 1307 /** 1308 * Interface for receiving events about media routing changes. 1309 * All methods of this interface will be called from the application's main thread. 1310 * 1311 * <p>A Callback will only receive events relevant to routes that the callback 1312 * was registered for.</p> 1313 * 1314 * @see MediaRouter#addCallback(int, Callback) 1315 * @see MediaRouter#removeCallback(Callback) 1316 */ 1317 public static abstract class Callback { 1318 /** 1319 * Called when the supplied route becomes selected as the active route 1320 * for the given route type. 1321 * 1322 * @param router the MediaRouter reporting the event 1323 * @param type Type flag set indicating the routes that have been selected 1324 * @param info Route that has been selected for the given route types 1325 */ 1326 public abstract void onRouteSelected(MediaRouter router, int type, RouteInfo info); 1327 1328 /** 1329 * Called when the supplied route becomes unselected as the active route 1330 * for the given route type. 1331 * 1332 * @param router the MediaRouter reporting the event 1333 * @param type Type flag set indicating the routes that have been unselected 1334 * @param info Route that has been unselected for the given route types 1335 */ 1336 public abstract void onRouteUnselected(MediaRouter router, int type, RouteInfo info); 1337 1338 /** 1339 * Called when a route for the specified type was added. 1340 * 1341 * @param router the MediaRouter reporting the event 1342 * @param info Route that has become available for use 1343 */ 1344 public abstract void onRouteAdded(MediaRouter router, RouteInfo info); 1345 1346 /** 1347 * Called when a route for the specified type was removed. 1348 * 1349 * @param router the MediaRouter reporting the event 1350 * @param info Route that has been removed from availability 1351 */ 1352 public abstract void onRouteRemoved(MediaRouter router, RouteInfo info); 1353 1354 /** 1355 * Called when an aspect of the indicated route has changed. 1356 * 1357 * <p>This will not indicate that the types supported by this route have 1358 * changed, only that cosmetic info such as name or status have been updated.</p> 1359 * 1360 * @param router the MediaRouter reporting the event 1361 * @param info The route that was changed 1362 */ 1363 public abstract void onRouteChanged(MediaRouter router, RouteInfo info); 1364 1365 /** 1366 * Called when a route is added to a group. 1367 * 1368 * @param router the MediaRouter reporting the event 1369 * @param info The route that was added 1370 * @param group The group the route was added to 1371 * @param index The route index within group that info was added at 1372 */ 1373 public abstract void onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group, 1374 int index); 1375 1376 /** 1377 * Called when a route is removed from a group. 1378 * 1379 * @param router the MediaRouter reporting the event 1380 * @param info The route that was removed 1381 * @param group The group the route was removed from 1382 */ 1383 public abstract void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group); 1384 } 1385 1386 /** 1387 * Stub implementation of {@link MediaRouter.Callback}. 1388 * Each abstract method is defined as a no-op. Override just the ones 1389 * you need. 1390 */ 1391 public static class SimpleCallback extends Callback { 1392 1393 @Override 1394 public void onRouteSelected(MediaRouter router, int type, RouteInfo info) { 1395 } 1396 1397 @Override 1398 public void onRouteUnselected(MediaRouter router, int type, RouteInfo info) { 1399 } 1400 1401 @Override 1402 public void onRouteAdded(MediaRouter router, RouteInfo info) { 1403 } 1404 1405 @Override 1406 public void onRouteRemoved(MediaRouter router, RouteInfo info) { 1407 } 1408 1409 @Override 1410 public void onRouteChanged(MediaRouter router, RouteInfo info) { 1411 } 1412 1413 @Override 1414 public void onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group, 1415 int index) { 1416 } 1417 1418 @Override 1419 public void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group) { 1420 } 1421 1422 } 1423 1424 static class VolumeCallbackInfo { 1425 public final VolumeCallback vcb; 1426 public final RouteInfo route; 1427 1428 public VolumeCallbackInfo(VolumeCallback vcb, RouteInfo route) { 1429 this.vcb = vcb; 1430 this.route = route; 1431 } 1432 } 1433 1434 /** 1435 * Interface for receiving events about volume changes. 1436 * All methods of this interface will be called from the application's main thread. 1437 * 1438 * <p>A VolumeCallback will only receive events relevant to routes that the callback 1439 * was registered for.</p> 1440 * 1441 * @see UserRouteInfo#setVolumeCallback(VolumeCallback) 1442 */ 1443 public static abstract class VolumeCallback { 1444 /** 1445 * Called when the volume for the route should be increased or decreased. 1446 * @param info the route affected by this event 1447 * @param direction an integer indicating whether the volume is to be increased 1448 * (positive value) or decreased (negative value). 1449 * For bundled changes, the absolute value indicates the number of changes 1450 * in the same direction, e.g. +3 corresponds to three "volume up" changes. 1451 */ 1452 public abstract void onVolumeUpdateRequest(RouteInfo info, int direction); 1453 /** 1454 * Called when the volume for the route should be set to the given value 1455 * @param info the route affected by this event 1456 * @param volume an integer indicating the new volume value that should be used, always 1457 * between 0 and the value set by {@link UserRouteInfo#setVolumeMax(int)}. 1458 */ 1459 public abstract void onVolumeSetRequest(RouteInfo info, int volume); 1460 } 1461 1462} 1463