MediaRouter.java revision 615e413a90ff5898d8c458ebc9649ca95fa9fd98
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.hardware.display.DisplayManager; 26import android.hardware.display.WifiDisplay; 27import android.hardware.display.WifiDisplayStatus; 28import android.os.Handler; 29import android.os.IBinder; 30import android.os.RemoteException; 31import android.os.ServiceManager; 32import android.text.TextUtils; 33import android.util.Log; 34import android.view.Display; 35 36import java.util.ArrayList; 37import java.util.HashMap; 38import java.util.List; 39import java.util.concurrent.CopyOnWriteArrayList; 40 41/** 42 * MediaRouter allows applications to control the routing of media channels 43 * and streams from the current device to external speakers and destination devices. 44 * 45 * <p>A MediaRouter is retrieved through {@link Context#getSystemService(String) 46 * Context.getSystemService()} of a {@link Context#MEDIA_ROUTER_SERVICE 47 * Context.MEDIA_ROUTER_SERVICE}. 48 * 49 * <p>The media router API is not thread-safe; all interactions with it must be 50 * done from the main thread of the process.</p> 51 */ 52public class MediaRouter { 53 private static final String TAG = "MediaRouter"; 54 55 static class Static implements DisplayManager.DisplayListener { 56 final Resources mResources; 57 final IAudioService mAudioService; 58 final DisplayManager mDisplayService; 59 final Handler mHandler; 60 final CopyOnWriteArrayList<CallbackInfo> mCallbacks = 61 new CopyOnWriteArrayList<CallbackInfo>(); 62 63 final ArrayList<RouteInfo> mRoutes = new ArrayList<RouteInfo>(); 64 final ArrayList<RouteCategory> mCategories = new ArrayList<RouteCategory>(); 65 66 final RouteCategory mSystemCategory; 67 68 final AudioRoutesInfo mCurAudioRoutesInfo = new AudioRoutesInfo(); 69 70 RouteInfo mDefaultAudioVideo; 71 RouteInfo mBluetoothA2dpRoute; 72 73 RouteInfo mSelectedRoute; 74 75 WifiDisplayStatus mLastKnownWifiDisplayStatus; 76 77 final IAudioRoutesObserver.Stub mAudioRoutesObserver = new IAudioRoutesObserver.Stub() { 78 public void dispatchAudioRoutesChanged(final AudioRoutesInfo newRoutes) { 79 mHandler.post(new Runnable() { 80 @Override public void run() { 81 updateAudioRoutes(newRoutes); 82 } 83 }); 84 } 85 }; 86 87 Static(Context appContext) { 88 mResources = Resources.getSystem(); 89 mHandler = new Handler(appContext.getMainLooper()); 90 91 IBinder b = ServiceManager.getService(Context.AUDIO_SERVICE); 92 mAudioService = IAudioService.Stub.asInterface(b); 93 94 mDisplayService = (DisplayManager) appContext.getSystemService(Context.DISPLAY_SERVICE); 95 96 mSystemCategory = new RouteCategory( 97 com.android.internal.R.string.default_audio_route_category_name, 98 ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_LIVE_VIDEO, false); 99 mSystemCategory.mIsSystem = true; 100 } 101 102 // Called after sStatic is initialized 103 void startMonitoringRoutes(Context appContext) { 104 mDefaultAudioVideo = new RouteInfo(mSystemCategory); 105 mDefaultAudioVideo.mNameResId = com.android.internal.R.string.default_audio_route_name; 106 mDefaultAudioVideo.mSupportedTypes = ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_LIVE_VIDEO; 107 mDefaultAudioVideo.mPresentationDisplay = choosePresentationDisplayForRoute( 108 mDefaultAudioVideo, getAllPresentationDisplays()); 109 addRouteStatic(mDefaultAudioVideo); 110 111 // This will select the active wifi display route if there is one. 112 updateWifiDisplayStatus(mDisplayService.getWifiDisplayStatus()); 113 114 appContext.registerReceiver(new WifiDisplayStatusChangedReceiver(), 115 new IntentFilter(DisplayManager.ACTION_WIFI_DISPLAY_STATUS_CHANGED)); 116 appContext.registerReceiver(new VolumeChangeReceiver(), 117 new IntentFilter(AudioManager.VOLUME_CHANGED_ACTION)); 118 119 mDisplayService.registerDisplayListener(this, mHandler); 120 121 AudioRoutesInfo newAudioRoutes = null; 122 try { 123 newAudioRoutes = mAudioService.startWatchingRoutes(mAudioRoutesObserver); 124 } catch (RemoteException e) { 125 } 126 if (newAudioRoutes != null) { 127 // This will select the active BT route if there is one and the current 128 // selected route is the default system route, or if there is no selected 129 // route yet. 130 updateAudioRoutes(newAudioRoutes); 131 } 132 133 // Select the default route if the above didn't sync us up 134 // appropriately with relevant system state. 135 if (mSelectedRoute == null) { 136 selectRouteStatic(mDefaultAudioVideo.getSupportedTypes(), mDefaultAudioVideo); 137 } 138 } 139 140 void updateAudioRoutes(AudioRoutesInfo newRoutes) { 141 if (newRoutes.mMainType != mCurAudioRoutesInfo.mMainType) { 142 mCurAudioRoutesInfo.mMainType = newRoutes.mMainType; 143 int name; 144 if ((newRoutes.mMainType&AudioRoutesInfo.MAIN_HEADPHONES) != 0 145 || (newRoutes.mMainType&AudioRoutesInfo.MAIN_HEADSET) != 0) { 146 name = com.android.internal.R.string.default_audio_route_name_headphones; 147 } else if ((newRoutes.mMainType&AudioRoutesInfo.MAIN_DOCK_SPEAKERS) != 0) { 148 name = com.android.internal.R.string.default_audio_route_name_dock_speakers; 149 } else if ((newRoutes.mMainType&AudioRoutesInfo.MAIN_HDMI) != 0) { 150 name = com.android.internal.R.string.default_media_route_name_hdmi; 151 } else { 152 name = com.android.internal.R.string.default_audio_route_name; 153 } 154 sStatic.mDefaultAudioVideo.mNameResId = name; 155 dispatchRouteChanged(sStatic.mDefaultAudioVideo); 156 } 157 158 final int mainType = mCurAudioRoutesInfo.mMainType; 159 160 boolean a2dpEnabled; 161 try { 162 a2dpEnabled = mAudioService.isBluetoothA2dpOn(); 163 } catch (RemoteException e) { 164 Log.e(TAG, "Error querying Bluetooth A2DP state", e); 165 a2dpEnabled = false; 166 } 167 168 if (!TextUtils.equals(newRoutes.mBluetoothName, mCurAudioRoutesInfo.mBluetoothName)) { 169 mCurAudioRoutesInfo.mBluetoothName = newRoutes.mBluetoothName; 170 if (mCurAudioRoutesInfo.mBluetoothName != null) { 171 if (sStatic.mBluetoothA2dpRoute == null) { 172 final RouteInfo info = new RouteInfo(sStatic.mSystemCategory); 173 info.mName = mCurAudioRoutesInfo.mBluetoothName; 174 info.mSupportedTypes = ROUTE_TYPE_LIVE_AUDIO; 175 sStatic.mBluetoothA2dpRoute = info; 176 addRouteStatic(sStatic.mBluetoothA2dpRoute); 177 } else { 178 sStatic.mBluetoothA2dpRoute.mName = mCurAudioRoutesInfo.mBluetoothName; 179 dispatchRouteChanged(sStatic.mBluetoothA2dpRoute); 180 } 181 } else if (sStatic.mBluetoothA2dpRoute != null) { 182 removeRoute(sStatic.mBluetoothA2dpRoute); 183 sStatic.mBluetoothA2dpRoute = null; 184 } 185 } 186 187 if (mBluetoothA2dpRoute != null) { 188 if (mainType != AudioRoutesInfo.MAIN_SPEAKER && 189 mSelectedRoute == mBluetoothA2dpRoute && !a2dpEnabled) { 190 selectRouteStatic(ROUTE_TYPE_LIVE_AUDIO, mDefaultAudioVideo); 191 } else if ((mSelectedRoute == mDefaultAudioVideo || mSelectedRoute == null) && 192 a2dpEnabled) { 193 selectRouteStatic(ROUTE_TYPE_LIVE_AUDIO, mBluetoothA2dpRoute); 194 } 195 } 196 } 197 198 @Override 199 public void onDisplayAdded(int displayId) { 200 updatePresentationDisplays(displayId); 201 } 202 203 @Override 204 public void onDisplayChanged(int displayId) { 205 updatePresentationDisplays(displayId); 206 } 207 208 @Override 209 public void onDisplayRemoved(int displayId) { 210 updatePresentationDisplays(displayId); 211 } 212 213 public Display[] getAllPresentationDisplays() { 214 return mDisplayService.getDisplays(DisplayManager.DISPLAY_CATEGORY_PRESENTATION); 215 } 216 217 private void updatePresentationDisplays(int changedDisplayId) { 218 final Display[] displays = getAllPresentationDisplays(); 219 final int count = mRoutes.size(); 220 for (int i = 0; i < count; i++) { 221 final RouteInfo info = mRoutes.get(i); 222 Display display = choosePresentationDisplayForRoute(info, displays); 223 if (display != info.mPresentationDisplay 224 || (display != null && display.getDisplayId() == changedDisplayId)) { 225 info.mPresentationDisplay = display; 226 dispatchRoutePresentationDisplayChanged(info); 227 } 228 } 229 } 230 } 231 232 static Static sStatic; 233 234 /** 235 * Route type flag for live audio. 236 * 237 * <p>A device that supports live audio routing will allow the media audio stream 238 * to be routed to supported destinations. This can include internal speakers or 239 * audio jacks on the device itself, A2DP devices, and more.</p> 240 * 241 * <p>Once initiated this routing is transparent to the application. All audio 242 * played on the media stream will be routed to the selected destination.</p> 243 */ 244 public static final int ROUTE_TYPE_LIVE_AUDIO = 0x1; 245 246 /** 247 * Route type flag for live video. 248 * 249 * <p>A device that supports live video routing will allow a mirrored version 250 * of the device's primary display or a customized 251 * {@link android.app.Presentation Presentation} to be routed to supported destinations.</p> 252 * 253 * <p>Once initiated, display mirroring is transparent to the application. 254 * While remote routing is active the application may use a 255 * {@link android.app.Presentation Presentation} to replace the mirrored view 256 * on the external display with different content.</p> 257 * 258 * @see RouteInfo#getPresentationDisplay() 259 * @see android.app.Presentation 260 */ 261 public static final int ROUTE_TYPE_LIVE_VIDEO = 0x2; 262 263 /** 264 * Route type flag for application-specific usage. 265 * 266 * <p>Unlike other media route types, user routes are managed by the application. 267 * The MediaRouter will manage and dispatch events for user routes, but the application 268 * is expected to interpret the meaning of these events and perform the requested 269 * routing tasks.</p> 270 */ 271 public static final int ROUTE_TYPE_USER = 0x00800000; 272 273 // Maps application contexts 274 static final HashMap<Context, MediaRouter> sRouters = new HashMap<Context, MediaRouter>(); 275 276 static String typesToString(int types) { 277 final StringBuilder result = new StringBuilder(); 278 if ((types & ROUTE_TYPE_LIVE_AUDIO) != 0) { 279 result.append("ROUTE_TYPE_LIVE_AUDIO "); 280 } 281 if ((types & ROUTE_TYPE_LIVE_VIDEO) != 0) { 282 result.append("ROUTE_TYPE_LIVE_VIDEO "); 283 } 284 if ((types & ROUTE_TYPE_USER) != 0) { 285 result.append("ROUTE_TYPE_USER "); 286 } 287 return result.toString(); 288 } 289 290 /** @hide */ 291 public MediaRouter(Context context) { 292 synchronized (Static.class) { 293 if (sStatic == null) { 294 final Context appContext = context.getApplicationContext(); 295 sStatic = new Static(appContext); 296 sStatic.startMonitoringRoutes(appContext); 297 } 298 } 299 } 300 301 /** 302 * Gets the default route for playing media content on the system. 303 * <p> 304 * The system always provides a default route. 305 * </p> 306 * 307 * @return The default route, which is guaranteed to never be null. 308 */ 309 public RouteInfo getDefaultRoute() { 310 return sStatic.mDefaultAudioVideo; 311 } 312 313 /** 314 * @hide for use by framework routing UI 315 */ 316 public RouteCategory getSystemCategory() { 317 return sStatic.mSystemCategory; 318 } 319 320 /** 321 * Return the currently selected route for any of the given types 322 * 323 * @param type route types 324 * @return the selected route 325 */ 326 public RouteInfo getSelectedRoute(int type) { 327 if (sStatic.mSelectedRoute != null && 328 (sStatic.mSelectedRoute.mSupportedTypes & type) != 0) { 329 // If the selected route supports any of the types supplied, it's still considered 330 // 'selected' for that type. 331 return sStatic.mSelectedRoute; 332 } else if (type == ROUTE_TYPE_USER) { 333 // The caller specifically asked for a user route and the currently selected route 334 // doesn't qualify. 335 return null; 336 } 337 // If the above didn't match and we're not specifically asking for a user route, 338 // consider the default selected. 339 return sStatic.mDefaultAudioVideo; 340 } 341 342 /** 343 * Add a callback to listen to events about specific kinds of media routes. 344 * If the specified callback is already registered, its registration will be updated for any 345 * additional route types specified. 346 * 347 * @param types Types of routes this callback is interested in 348 * @param cb Callback to add 349 */ 350 public void addCallback(int types, Callback cb) { 351 final int count = sStatic.mCallbacks.size(); 352 for (int i = 0; i < count; i++) { 353 final CallbackInfo info = sStatic.mCallbacks.get(i); 354 if (info.cb == cb) { 355 info.type |= types; 356 return; 357 } 358 } 359 sStatic.mCallbacks.add(new CallbackInfo(cb, types, this)); 360 } 361 362 /** 363 * Remove the specified callback. It will no longer receive events about media routing. 364 * 365 * @param cb Callback to remove 366 */ 367 public void removeCallback(Callback cb) { 368 final int count = sStatic.mCallbacks.size(); 369 for (int i = 0; i < count; i++) { 370 if (sStatic.mCallbacks.get(i).cb == cb) { 371 sStatic.mCallbacks.remove(i); 372 return; 373 } 374 } 375 Log.w(TAG, "removeCallback(" + cb + "): callback not registered"); 376 } 377 378 /** 379 * Select the specified route to use for output of the given media types. 380 * <p class="note"> 381 * As API version 18, this function may be used to select any route. 382 * In prior versions, this function could only be used to select user 383 * routes and would ignore any attempt to select a system route. 384 * </p> 385 * 386 * @param types type flags indicating which types this route should be used for. 387 * The route must support at least a subset. 388 * @param route Route to select 389 */ 390 public void selectRoute(int types, RouteInfo route) { 391 selectRouteStatic(types, route); 392 } 393 394 /** 395 * @hide internal use 396 */ 397 public void selectRouteInt(int types, RouteInfo route) { 398 selectRouteStatic(types, route); 399 } 400 401 static void selectRouteStatic(int types, RouteInfo route) { 402 final RouteInfo oldRoute = sStatic.mSelectedRoute; 403 if (oldRoute == route) return; 404 if ((route.getSupportedTypes() & types) == 0) { 405 Log.w(TAG, "selectRoute ignored; cannot select route with supported types " + 406 typesToString(route.getSupportedTypes()) + " into route types " + 407 typesToString(types)); 408 return; 409 } 410 411 final RouteInfo btRoute = sStatic.mBluetoothA2dpRoute; 412 if (btRoute != null && (types & ROUTE_TYPE_LIVE_AUDIO) != 0 && 413 (route == btRoute || route == sStatic.mDefaultAudioVideo)) { 414 try { 415 sStatic.mAudioService.setBluetoothA2dpOn(route == btRoute); 416 } catch (RemoteException e) { 417 Log.e(TAG, "Error changing Bluetooth A2DP state", e); 418 } 419 } 420 421 final WifiDisplay activeDisplay = 422 sStatic.mDisplayService.getWifiDisplayStatus().getActiveDisplay(); 423 final boolean oldRouteHasAddress = oldRoute != null && oldRoute.mDeviceAddress != null; 424 final boolean newRouteHasAddress = route != null && route.mDeviceAddress != null; 425 if (activeDisplay != null || oldRouteHasAddress || newRouteHasAddress) { 426 if (newRouteHasAddress && !matchesDeviceAddress(activeDisplay, route)) { 427 sStatic.mDisplayService.connectWifiDisplay(route.mDeviceAddress); 428 } else if (activeDisplay != null && !newRouteHasAddress) { 429 sStatic.mDisplayService.disconnectWifiDisplay(); 430 } 431 } 432 433 if (oldRoute != null) { 434 // TODO filter types properly 435 dispatchRouteUnselected(types & oldRoute.getSupportedTypes(), oldRoute); 436 } 437 sStatic.mSelectedRoute = route; 438 if (route != null) { 439 // TODO filter types properly 440 dispatchRouteSelected(types & route.getSupportedTypes(), route); 441 } 442 } 443 444 /** 445 * Compare the device address of a display and a route. 446 * Nulls/no device address will match another null/no address. 447 */ 448 static boolean matchesDeviceAddress(WifiDisplay display, RouteInfo info) { 449 final boolean routeHasAddress = info != null && info.mDeviceAddress != null; 450 if (display == null && !routeHasAddress) { 451 return true; 452 } 453 454 if (display != null && routeHasAddress) { 455 return display.getDeviceAddress().equals(info.mDeviceAddress); 456 } 457 return false; 458 } 459 460 /** 461 * Add an app-specified route for media to the MediaRouter. 462 * App-specified route definitions are created using {@link #createUserRoute(RouteCategory)} 463 * 464 * @param info Definition of the route to add 465 * @see #createUserRoute(RouteCategory) 466 * @see #removeUserRoute(UserRouteInfo) 467 */ 468 public void addUserRoute(UserRouteInfo info) { 469 addRouteStatic(info); 470 } 471 472 /** 473 * @hide Framework use only 474 */ 475 public void addRouteInt(RouteInfo info) { 476 addRouteStatic(info); 477 } 478 479 static void addRouteStatic(RouteInfo info) { 480 final RouteCategory cat = info.getCategory(); 481 if (!sStatic.mCategories.contains(cat)) { 482 sStatic.mCategories.add(cat); 483 } 484 if (cat.isGroupable() && !(info instanceof RouteGroup)) { 485 // Enforce that any added route in a groupable category must be in a group. 486 final RouteGroup group = new RouteGroup(info.getCategory()); 487 group.mSupportedTypes = info.mSupportedTypes; 488 sStatic.mRoutes.add(group); 489 dispatchRouteAdded(group); 490 group.addRoute(info); 491 492 info = group; 493 } else { 494 sStatic.mRoutes.add(info); 495 dispatchRouteAdded(info); 496 } 497 } 498 499 /** 500 * Remove an app-specified route for media from the MediaRouter. 501 * 502 * @param info Definition of the route to remove 503 * @see #addUserRoute(UserRouteInfo) 504 */ 505 public void removeUserRoute(UserRouteInfo info) { 506 removeRoute(info); 507 } 508 509 /** 510 * Remove all app-specified routes from the MediaRouter. 511 * 512 * @see #removeUserRoute(UserRouteInfo) 513 */ 514 public void clearUserRoutes() { 515 for (int i = 0; i < sStatic.mRoutes.size(); i++) { 516 final RouteInfo info = sStatic.mRoutes.get(i); 517 // TODO Right now, RouteGroups only ever contain user routes. 518 // The code below will need to change if this assumption does. 519 if (info instanceof UserRouteInfo || info instanceof RouteGroup) { 520 removeRouteAt(i); 521 i--; 522 } 523 } 524 } 525 526 /** 527 * @hide internal use only 528 */ 529 public void removeRouteInt(RouteInfo info) { 530 removeRoute(info); 531 } 532 533 static void removeRoute(RouteInfo info) { 534 if (sStatic.mRoutes.remove(info)) { 535 final RouteCategory removingCat = info.getCategory(); 536 final int count = sStatic.mRoutes.size(); 537 boolean found = false; 538 for (int i = 0; i < count; i++) { 539 final RouteCategory cat = sStatic.mRoutes.get(i).getCategory(); 540 if (removingCat == cat) { 541 found = true; 542 break; 543 } 544 } 545 if (info == sStatic.mSelectedRoute) { 546 // Removing the currently selected route? Select the default before we remove it. 547 // TODO: Be smarter about the route types here; this selects for all valid. 548 selectRouteStatic(ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_USER, sStatic.mDefaultAudioVideo); 549 } 550 if (!found) { 551 sStatic.mCategories.remove(removingCat); 552 } 553 dispatchRouteRemoved(info); 554 } 555 } 556 557 void removeRouteAt(int routeIndex) { 558 if (routeIndex >= 0 && routeIndex < sStatic.mRoutes.size()) { 559 final RouteInfo info = sStatic.mRoutes.remove(routeIndex); 560 final RouteCategory removingCat = info.getCategory(); 561 final int count = sStatic.mRoutes.size(); 562 boolean found = false; 563 for (int i = 0; i < count; i++) { 564 final RouteCategory cat = sStatic.mRoutes.get(i).getCategory(); 565 if (removingCat == cat) { 566 found = true; 567 break; 568 } 569 } 570 if (info == sStatic.mSelectedRoute) { 571 // Removing the currently selected route? Select the default before we remove it. 572 // TODO: Be smarter about the route types here; this selects for all valid. 573 selectRouteStatic(ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_LIVE_VIDEO | ROUTE_TYPE_USER, 574 sStatic.mDefaultAudioVideo); 575 } 576 if (!found) { 577 sStatic.mCategories.remove(removingCat); 578 } 579 dispatchRouteRemoved(info); 580 } 581 } 582 583 /** 584 * Return the number of {@link MediaRouter.RouteCategory categories} currently 585 * represented by routes known to this MediaRouter. 586 * 587 * @return the number of unique categories represented by this MediaRouter's known routes 588 */ 589 public int getCategoryCount() { 590 return sStatic.mCategories.size(); 591 } 592 593 /** 594 * Return the {@link MediaRouter.RouteCategory category} at the given index. 595 * Valid indices are in the range [0-getCategoryCount). 596 * 597 * @param index which category to return 598 * @return the category at index 599 */ 600 public RouteCategory getCategoryAt(int index) { 601 return sStatic.mCategories.get(index); 602 } 603 604 /** 605 * Return the number of {@link MediaRouter.RouteInfo routes} currently known 606 * to this MediaRouter. 607 * 608 * @return the number of routes tracked by this router 609 */ 610 public int getRouteCount() { 611 return sStatic.mRoutes.size(); 612 } 613 614 /** 615 * Return the route at the specified index. 616 * 617 * @param index index of the route to return 618 * @return the route at index 619 */ 620 public RouteInfo getRouteAt(int index) { 621 return sStatic.mRoutes.get(index); 622 } 623 624 static int getRouteCountStatic() { 625 return sStatic.mRoutes.size(); 626 } 627 628 static RouteInfo getRouteAtStatic(int index) { 629 return sStatic.mRoutes.get(index); 630 } 631 632 /** 633 * Create a new user route that may be modified and registered for use by the application. 634 * 635 * @param category The category the new route will belong to 636 * @return A new UserRouteInfo for use by the application 637 * 638 * @see #addUserRoute(UserRouteInfo) 639 * @see #removeUserRoute(UserRouteInfo) 640 * @see #createRouteCategory(CharSequence) 641 */ 642 public UserRouteInfo createUserRoute(RouteCategory category) { 643 return new UserRouteInfo(category); 644 } 645 646 /** 647 * Create a new route category. Each route must belong to a category. 648 * 649 * @param name Name of the new category 650 * @param isGroupable true if routes in this category may be grouped with one another 651 * @return the new RouteCategory 652 */ 653 public RouteCategory createRouteCategory(CharSequence name, boolean isGroupable) { 654 return new RouteCategory(name, ROUTE_TYPE_USER, isGroupable); 655 } 656 657 /** 658 * Create a new route category. Each route must belong to a category. 659 * 660 * @param nameResId Resource ID of the name of the new category 661 * @param isGroupable true if routes in this category may be grouped with one another 662 * @return the new RouteCategory 663 */ 664 public RouteCategory createRouteCategory(int nameResId, boolean isGroupable) { 665 return new RouteCategory(nameResId, ROUTE_TYPE_USER, isGroupable); 666 } 667 668 static void updateRoute(final RouteInfo info) { 669 dispatchRouteChanged(info); 670 } 671 672 static void dispatchRouteSelected(int type, RouteInfo info) { 673 for (CallbackInfo cbi : sStatic.mCallbacks) { 674 if ((cbi.type & type) != 0) { 675 cbi.cb.onRouteSelected(cbi.router, type, info); 676 } 677 } 678 } 679 680 static void dispatchRouteUnselected(int type, RouteInfo info) { 681 for (CallbackInfo cbi : sStatic.mCallbacks) { 682 if ((cbi.type & type) != 0) { 683 cbi.cb.onRouteUnselected(cbi.router, type, info); 684 } 685 } 686 } 687 688 static void dispatchRouteChanged(RouteInfo info) { 689 for (CallbackInfo cbi : sStatic.mCallbacks) { 690 if ((cbi.type & info.mSupportedTypes) != 0) { 691 cbi.cb.onRouteChanged(cbi.router, info); 692 } 693 } 694 } 695 696 static void dispatchRouteAdded(RouteInfo info) { 697 for (CallbackInfo cbi : sStatic.mCallbacks) { 698 if ((cbi.type & info.mSupportedTypes) != 0) { 699 cbi.cb.onRouteAdded(cbi.router, info); 700 } 701 } 702 } 703 704 static void dispatchRouteRemoved(RouteInfo info) { 705 for (CallbackInfo cbi : sStatic.mCallbacks) { 706 if ((cbi.type & info.mSupportedTypes) != 0) { 707 cbi.cb.onRouteRemoved(cbi.router, info); 708 } 709 } 710 } 711 712 static void dispatchRouteGrouped(RouteInfo info, RouteGroup group, int index) { 713 for (CallbackInfo cbi : sStatic.mCallbacks) { 714 if ((cbi.type & group.mSupportedTypes) != 0) { 715 cbi.cb.onRouteGrouped(cbi.router, info, group, index); 716 } 717 } 718 } 719 720 static void dispatchRouteUngrouped(RouteInfo info, RouteGroup group) { 721 for (CallbackInfo cbi : sStatic.mCallbacks) { 722 if ((cbi.type & group.mSupportedTypes) != 0) { 723 cbi.cb.onRouteUngrouped(cbi.router, info, group); 724 } 725 } 726 } 727 728 static void dispatchRouteVolumeChanged(RouteInfo info) { 729 for (CallbackInfo cbi : sStatic.mCallbacks) { 730 if ((cbi.type & info.mSupportedTypes) != 0) { 731 cbi.cb.onRouteVolumeChanged(cbi.router, info); 732 } 733 } 734 } 735 736 static void dispatchRoutePresentationDisplayChanged(RouteInfo info) { 737 for (CallbackInfo cbi : sStatic.mCallbacks) { 738 if ((cbi.type & info.mSupportedTypes) != 0) { 739 cbi.cb.onRoutePresentationDisplayChanged(cbi.router, info); 740 } 741 } 742 } 743 744 static void systemVolumeChanged(int newValue) { 745 final RouteInfo selectedRoute = sStatic.mSelectedRoute; 746 if (selectedRoute == null) return; 747 748 if (selectedRoute == sStatic.mBluetoothA2dpRoute || 749 selectedRoute == sStatic.mDefaultAudioVideo) { 750 dispatchRouteVolumeChanged(selectedRoute); 751 } else if (sStatic.mBluetoothA2dpRoute != null) { 752 try { 753 dispatchRouteVolumeChanged(sStatic.mAudioService.isBluetoothA2dpOn() ? 754 sStatic.mBluetoothA2dpRoute : sStatic.mDefaultAudioVideo); 755 } catch (RemoteException e) { 756 Log.e(TAG, "Error checking Bluetooth A2DP state to report volume change", e); 757 } 758 } else { 759 dispatchRouteVolumeChanged(sStatic.mDefaultAudioVideo); 760 } 761 } 762 763 static void updateWifiDisplayStatus(WifiDisplayStatus newStatus) { 764 final WifiDisplayStatus oldStatus = sStatic.mLastKnownWifiDisplayStatus; 765 766 // TODO Naive implementation. Make this smarter later. 767 boolean wantScan = false; 768 boolean blockScan = false; 769 WifiDisplay[] oldDisplays = oldStatus != null ? 770 oldStatus.getRememberedDisplays() : WifiDisplay.EMPTY_ARRAY; 771 WifiDisplay[] newDisplays; 772 WifiDisplay[] availableDisplays; 773 WifiDisplay activeDisplay; 774 775 if (newStatus.getFeatureState() == WifiDisplayStatus.FEATURE_STATE_ON) { 776 newDisplays = newStatus.getRememberedDisplays(); 777 availableDisplays = newStatus.getAvailableDisplays(); 778 activeDisplay = newStatus.getActiveDisplay(); 779 } else { 780 newDisplays = availableDisplays = WifiDisplay.EMPTY_ARRAY; 781 activeDisplay = null; 782 } 783 784 for (int i = 0; i < newDisplays.length; i++) { 785 final WifiDisplay d = newDisplays[i]; 786 final WifiDisplay oldRemembered = findMatchingDisplay(d, oldDisplays); 787 if (oldRemembered == null) { 788 addRouteStatic(makeWifiDisplayRoute(d, 789 findMatchingDisplay(d, availableDisplays) != null)); 790 wantScan = true; 791 } else { 792 final boolean available = findMatchingDisplay(d, availableDisplays) != null; 793 final RouteInfo route = findWifiDisplayRoute(d); 794 updateWifiDisplayRoute(route, d, available, newStatus); 795 } 796 if (d.equals(activeDisplay)) { 797 final RouteInfo activeRoute = findWifiDisplayRoute(d); 798 if (activeRoute != null) { 799 selectRouteStatic(activeRoute.getSupportedTypes(), activeRoute); 800 801 // Don't scan if we're already connected to a wifi display, 802 // the scanning process can cause a hiccup with some configurations. 803 blockScan = true; 804 } 805 } 806 } 807 for (int i = 0; i < oldDisplays.length; i++) { 808 final WifiDisplay d = oldDisplays[i]; 809 final WifiDisplay newDisplay = findMatchingDisplay(d, newDisplays); 810 if (newDisplay == null) { 811 removeRoute(findWifiDisplayRoute(d)); 812 } 813 } 814 815 if (wantScan && !blockScan) { 816 sStatic.mDisplayService.scanWifiDisplays(); 817 } 818 819 sStatic.mLastKnownWifiDisplayStatus = newStatus; 820 } 821 822 static RouteInfo makeWifiDisplayRoute(WifiDisplay display, boolean available) { 823 final RouteInfo newRoute = new RouteInfo(sStatic.mSystemCategory); 824 newRoute.mDeviceAddress = display.getDeviceAddress(); 825 newRoute.mSupportedTypes = ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_LIVE_VIDEO; 826 newRoute.mVolumeHandling = RouteInfo.PLAYBACK_VOLUME_FIXED; 827 newRoute.mPlaybackType = RouteInfo.PLAYBACK_TYPE_REMOTE; 828 829 newRoute.setStatusCode(available ? 830 RouteInfo.STATUS_AVAILABLE : RouteInfo.STATUS_CONNECTING); 831 newRoute.mEnabled = available; 832 833 newRoute.mName = display.getFriendlyDisplayName(); 834 835 newRoute.mPresentationDisplay = choosePresentationDisplayForRoute(newRoute, 836 sStatic.getAllPresentationDisplays()); 837 return newRoute; 838 } 839 840 private static void updateWifiDisplayRoute(RouteInfo route, WifiDisplay display, 841 boolean available, WifiDisplayStatus wifiDisplayStatus) { 842 final boolean isScanning = 843 wifiDisplayStatus.getScanState() == WifiDisplayStatus.SCAN_STATE_SCANNING; 844 845 boolean changed = false; 846 int newStatus = RouteInfo.STATUS_NONE; 847 848 if (available) { 849 newStatus = isScanning ? RouteInfo.STATUS_SCANNING : RouteInfo.STATUS_AVAILABLE; 850 } else { 851 newStatus = RouteInfo.STATUS_NOT_AVAILABLE; 852 } 853 854 if (display.equals(wifiDisplayStatus.getActiveDisplay())) { 855 final int activeState = wifiDisplayStatus.getActiveDisplayState(); 856 switch (activeState) { 857 case WifiDisplayStatus.DISPLAY_STATE_CONNECTED: 858 newStatus = RouteInfo.STATUS_NONE; 859 break; 860 case WifiDisplayStatus.DISPLAY_STATE_CONNECTING: 861 newStatus = RouteInfo.STATUS_CONNECTING; 862 break; 863 case WifiDisplayStatus.DISPLAY_STATE_NOT_CONNECTED: 864 Log.e(TAG, "Active display is not connected!"); 865 break; 866 } 867 } 868 869 final String newName = display.getFriendlyDisplayName(); 870 if (!route.getName().equals(newName)) { 871 route.mName = newName; 872 changed = true; 873 } 874 875 changed |= route.mEnabled != available; 876 route.mEnabled = available; 877 878 changed |= route.setStatusCode(newStatus); 879 880 if (changed) { 881 dispatchRouteChanged(route); 882 } 883 884 if (!available && route == sStatic.mSelectedRoute) { 885 // Oops, no longer available. Reselect the default. 886 final RouteInfo defaultRoute = sStatic.mDefaultAudioVideo; 887 selectRouteStatic(defaultRoute.getSupportedTypes(), defaultRoute); 888 } 889 } 890 891 private static WifiDisplay findMatchingDisplay(WifiDisplay d, WifiDisplay[] displays) { 892 for (int i = 0; i < displays.length; i++) { 893 final WifiDisplay other = displays[i]; 894 if (d.hasSameAddress(other)) { 895 return other; 896 } 897 } 898 return null; 899 } 900 901 private static RouteInfo findWifiDisplayRoute(WifiDisplay d) { 902 final int count = sStatic.mRoutes.size(); 903 for (int i = 0; i < count; i++) { 904 final RouteInfo info = sStatic.mRoutes.get(i); 905 if (d.getDeviceAddress().equals(info.mDeviceAddress)) { 906 return info; 907 } 908 } 909 return null; 910 } 911 912 private static Display choosePresentationDisplayForRoute(RouteInfo route, Display[] displays) { 913 if ((route.mSupportedTypes & ROUTE_TYPE_LIVE_VIDEO) != 0) { 914 if (route.mDeviceAddress != null) { 915 // Find the indicated Wifi display by its address. 916 for (Display display : displays) { 917 if (display.getType() == Display.TYPE_WIFI 918 && route.mDeviceAddress.equals(display.getAddress())) { 919 return display; 920 } 921 } 922 return null; 923 } 924 925 if (route == sStatic.mDefaultAudioVideo && displays.length > 0) { 926 // Choose the first presentation display from the list. 927 return displays[0]; 928 } 929 } 930 return null; 931 } 932 933 /** 934 * Information about a media route. 935 */ 936 public static class RouteInfo { 937 CharSequence mName; 938 int mNameResId; 939 private CharSequence mStatus; 940 int mSupportedTypes; 941 RouteGroup mGroup; 942 final RouteCategory mCategory; 943 Drawable mIcon; 944 // playback information 945 int mPlaybackType = PLAYBACK_TYPE_LOCAL; 946 int mVolumeMax = RemoteControlClient.DEFAULT_PLAYBACK_VOLUME; 947 int mVolume = RemoteControlClient.DEFAULT_PLAYBACK_VOLUME; 948 int mVolumeHandling = RemoteControlClient.DEFAULT_PLAYBACK_VOLUME_HANDLING; 949 int mPlaybackStream = AudioManager.STREAM_MUSIC; 950 VolumeCallbackInfo mVcb; 951 Display mPresentationDisplay; 952 953 String mDeviceAddress; 954 boolean mEnabled = true; 955 956 // A predetermined connection status that can override mStatus 957 private int mStatusCode; 958 959 /** @hide */ public static final int STATUS_NONE = 0; 960 /** @hide */ public static final int STATUS_SCANNING = 1; 961 /** @hide */ public static final int STATUS_CONNECTING = 2; 962 /** @hide */ public static final int STATUS_AVAILABLE = 3; 963 /** @hide */ public static final int STATUS_NOT_AVAILABLE = 4; 964 965 private Object mTag; 966 967 /** 968 * The default playback type, "local", indicating the presentation of the media is happening 969 * on the same device (e.g. a phone, a tablet) as where it is controlled from. 970 * @see #setPlaybackType(int) 971 */ 972 public final static int PLAYBACK_TYPE_LOCAL = 0; 973 /** 974 * A playback type indicating the presentation of the media is happening on 975 * a different device (i.e. the remote device) than where it is controlled from. 976 * @see #setPlaybackType(int) 977 */ 978 public final static int PLAYBACK_TYPE_REMOTE = 1; 979 /** 980 * Playback information indicating the playback volume is fixed, i.e. it cannot be 981 * controlled from this object. An example of fixed playback volume is a remote player, 982 * playing over HDMI where the user prefers to control the volume on the HDMI sink, rather 983 * than attenuate at the source. 984 * @see #setVolumeHandling(int) 985 */ 986 public final static int PLAYBACK_VOLUME_FIXED = 0; 987 /** 988 * Playback information indicating the playback volume is variable and can be controlled 989 * from this object. 990 */ 991 public final static int PLAYBACK_VOLUME_VARIABLE = 1; 992 993 RouteInfo(RouteCategory category) { 994 mCategory = category; 995 } 996 997 /** 998 * @return The user-friendly name of a media route. This is the string presented 999 * to users who may select this as the active route. 1000 */ 1001 public CharSequence getName() { 1002 return getName(sStatic.mResources); 1003 } 1004 1005 /** 1006 * Return the properly localized/resource selected name of this route. 1007 * 1008 * @param context Context used to resolve the correct configuration to load 1009 * @return The user-friendly name of the media route. This is the string presented 1010 * to users who may select this as the active route. 1011 */ 1012 public CharSequence getName(Context context) { 1013 return getName(context.getResources()); 1014 } 1015 1016 CharSequence getName(Resources res) { 1017 if (mNameResId != 0) { 1018 return mName = res.getText(mNameResId); 1019 } 1020 return mName; 1021 } 1022 1023 /** 1024 * @return The user-friendly status for a media route. This may include a description 1025 * of the currently playing media, if available. 1026 */ 1027 public CharSequence getStatus() { 1028 return mStatus; 1029 } 1030 1031 /** 1032 * Set this route's status by predetermined status code. If the caller 1033 * should dispatch a route changed event this call will return true; 1034 */ 1035 boolean setStatusCode(int statusCode) { 1036 if (statusCode != mStatusCode) { 1037 mStatusCode = statusCode; 1038 int resId = 0; 1039 switch (statusCode) { 1040 case STATUS_SCANNING: 1041 resId = com.android.internal.R.string.media_route_status_scanning; 1042 break; 1043 case STATUS_CONNECTING: 1044 resId = com.android.internal.R.string.media_route_status_connecting; 1045 break; 1046 case STATUS_AVAILABLE: 1047 resId = com.android.internal.R.string.media_route_status_available; 1048 break; 1049 case STATUS_NOT_AVAILABLE: 1050 resId = com.android.internal.R.string.media_route_status_not_available; 1051 break; 1052 } 1053 mStatus = resId != 0 ? sStatic.mResources.getText(resId) : null; 1054 return true; 1055 } 1056 return false; 1057 } 1058 1059 /** 1060 * @hide 1061 */ 1062 public int getStatusCode() { 1063 return mStatusCode; 1064 } 1065 1066 /** 1067 * @return A media type flag set describing which types this route supports. 1068 */ 1069 public int getSupportedTypes() { 1070 return mSupportedTypes; 1071 } 1072 1073 /** 1074 * @return The group that this route belongs to. 1075 */ 1076 public RouteGroup getGroup() { 1077 return mGroup; 1078 } 1079 1080 /** 1081 * @return the category this route belongs to. 1082 */ 1083 public RouteCategory getCategory() { 1084 return mCategory; 1085 } 1086 1087 /** 1088 * Get the icon representing this route. 1089 * This icon will be used in picker UIs if available. 1090 * 1091 * @return the icon representing this route or null if no icon is available 1092 */ 1093 public Drawable getIconDrawable() { 1094 return mIcon; 1095 } 1096 1097 /** 1098 * Set an application-specific tag object for this route. 1099 * The application may use this to store arbitrary data associated with the 1100 * route for internal tracking. 1101 * 1102 * <p>Note that the lifespan of a route may be well past the lifespan of 1103 * an Activity or other Context; take care that objects you store here 1104 * will not keep more data in memory alive than you intend.</p> 1105 * 1106 * @param tag Arbitrary, app-specific data for this route to hold for later use 1107 */ 1108 public void setTag(Object tag) { 1109 mTag = tag; 1110 routeUpdated(); 1111 } 1112 1113 /** 1114 * @return The tag object previously set by the application 1115 * @see #setTag(Object) 1116 */ 1117 public Object getTag() { 1118 return mTag; 1119 } 1120 1121 /** 1122 * @return the type of playback associated with this route 1123 * @see UserRouteInfo#setPlaybackType(int) 1124 */ 1125 public int getPlaybackType() { 1126 return mPlaybackType; 1127 } 1128 1129 /** 1130 * @return the stream over which the playback associated with this route is performed 1131 * @see UserRouteInfo#setPlaybackStream(int) 1132 */ 1133 public int getPlaybackStream() { 1134 return mPlaybackStream; 1135 } 1136 1137 /** 1138 * Return the current volume for this route. Depending on the route, this may only 1139 * be valid if the route is currently selected. 1140 * 1141 * @return the volume at which the playback associated with this route is performed 1142 * @see UserRouteInfo#setVolume(int) 1143 */ 1144 public int getVolume() { 1145 if (mPlaybackType == PLAYBACK_TYPE_LOCAL) { 1146 int vol = 0; 1147 try { 1148 vol = sStatic.mAudioService.getStreamVolume(mPlaybackStream); 1149 } catch (RemoteException e) { 1150 Log.e(TAG, "Error getting local stream volume", e); 1151 } 1152 return vol; 1153 } else { 1154 return mVolume; 1155 } 1156 } 1157 1158 /** 1159 * Request a volume change for this route. 1160 * @param volume value between 0 and getVolumeMax 1161 */ 1162 public void requestSetVolume(int volume) { 1163 if (mPlaybackType == PLAYBACK_TYPE_LOCAL) { 1164 try { 1165 sStatic.mAudioService.setStreamVolume(mPlaybackStream, volume, 0); 1166 } catch (RemoteException e) { 1167 Log.e(TAG, "Error setting local stream volume", e); 1168 } 1169 } else { 1170 Log.e(TAG, getClass().getSimpleName() + ".requestSetVolume(): " + 1171 "Non-local volume playback on system route? " + 1172 "Could not request volume change."); 1173 } 1174 } 1175 1176 /** 1177 * Request an incremental volume update for this route. 1178 * @param direction Delta to apply to the current volume 1179 */ 1180 public void requestUpdateVolume(int direction) { 1181 if (mPlaybackType == PLAYBACK_TYPE_LOCAL) { 1182 try { 1183 final int volume = 1184 Math.max(0, Math.min(getVolume() + direction, getVolumeMax())); 1185 sStatic.mAudioService.setStreamVolume(mPlaybackStream, volume, 0); 1186 } catch (RemoteException e) { 1187 Log.e(TAG, "Error setting local stream volume", e); 1188 } 1189 } else { 1190 Log.e(TAG, getClass().getSimpleName() + ".requestChangeVolume(): " + 1191 "Non-local volume playback on system route? " + 1192 "Could not request volume change."); 1193 } 1194 } 1195 1196 /** 1197 * @return the maximum volume at which the playback associated with this route is performed 1198 * @see UserRouteInfo#setVolumeMax(int) 1199 */ 1200 public int getVolumeMax() { 1201 if (mPlaybackType == PLAYBACK_TYPE_LOCAL) { 1202 int volMax = 0; 1203 try { 1204 volMax = sStatic.mAudioService.getStreamMaxVolume(mPlaybackStream); 1205 } catch (RemoteException e) { 1206 Log.e(TAG, "Error getting local stream volume", e); 1207 } 1208 return volMax; 1209 } else { 1210 return mVolumeMax; 1211 } 1212 } 1213 1214 /** 1215 * @return how volume is handling on the route 1216 * @see UserRouteInfo#setVolumeHandling(int) 1217 */ 1218 public int getVolumeHandling() { 1219 return mVolumeHandling; 1220 } 1221 1222 /** 1223 * Gets the {@link Display} that should be used by the application to show 1224 * a {@link android.app.Presentation} on an external display when this route is selected. 1225 * Depending on the route, this may only be valid if the route is currently 1226 * selected. 1227 * <p> 1228 * The preferred presentation display may change independently of the route 1229 * being selected or unselected. For example, the presentation display 1230 * of the default system route may change when an external HDMI display is connected 1231 * or disconnected even though the route itself has not changed. 1232 * </p><p> 1233 * This method may return null if there is no external display associated with 1234 * the route or if the display is not ready to show UI yet. 1235 * </p><p> 1236 * The application should listen for changes to the presentation display 1237 * using the {@link Callback#onRoutePresentationDisplayChanged} callback and 1238 * show or dismiss its {@link android.app.Presentation} accordingly when the display 1239 * becomes available or is removed. 1240 * </p><p> 1241 * This method only makes sense for {@link #ROUTE_TYPE_LIVE_VIDEO live video} routes. 1242 * </p> 1243 * 1244 * @return The preferred presentation display to use when this route is 1245 * selected or null if none. 1246 * 1247 * @see #ROUTE_TYPE_LIVE_VIDEO 1248 * @see android.app.Presentation 1249 */ 1250 public Display getPresentationDisplay() { 1251 return mPresentationDisplay; 1252 } 1253 1254 /** 1255 * @return true if this route is enabled and may be selected 1256 */ 1257 public boolean isEnabled() { 1258 return mEnabled; 1259 } 1260 1261 void setStatusInt(CharSequence status) { 1262 if (!status.equals(mStatus)) { 1263 mStatus = status; 1264 if (mGroup != null) { 1265 mGroup.memberStatusChanged(this, status); 1266 } 1267 routeUpdated(); 1268 } 1269 } 1270 1271 final IRemoteVolumeObserver.Stub mRemoteVolObserver = new IRemoteVolumeObserver.Stub() { 1272 public void dispatchRemoteVolumeUpdate(final int direction, final int value) { 1273 sStatic.mHandler.post(new Runnable() { 1274 @Override 1275 public void run() { 1276 if (mVcb != null) { 1277 if (direction != 0) { 1278 mVcb.vcb.onVolumeUpdateRequest(mVcb.route, direction); 1279 } else { 1280 mVcb.vcb.onVolumeSetRequest(mVcb.route, value); 1281 } 1282 } 1283 } 1284 }); 1285 } 1286 }; 1287 1288 void routeUpdated() { 1289 updateRoute(this); 1290 } 1291 1292 @Override 1293 public String toString() { 1294 String supportedTypes = typesToString(getSupportedTypes()); 1295 return getClass().getSimpleName() + "{ name=" + getName() + 1296 ", status=" + getStatus() + 1297 ", category=" + getCategory() + 1298 ", supportedTypes=" + supportedTypes + 1299 ", presentationDisplay=" + mPresentationDisplay + "}"; 1300 } 1301 } 1302 1303 /** 1304 * Information about a route that the application may define and modify. 1305 * A user route defaults to {@link RouteInfo#PLAYBACK_TYPE_REMOTE} and 1306 * {@link RouteInfo#PLAYBACK_VOLUME_FIXED}. 1307 * 1308 * @see MediaRouter.RouteInfo 1309 */ 1310 public static class UserRouteInfo extends RouteInfo { 1311 RemoteControlClient mRcc; 1312 1313 UserRouteInfo(RouteCategory category) { 1314 super(category); 1315 mSupportedTypes = ROUTE_TYPE_USER; 1316 mPlaybackType = PLAYBACK_TYPE_REMOTE; 1317 mVolumeHandling = PLAYBACK_VOLUME_FIXED; 1318 } 1319 1320 /** 1321 * Set the user-visible name of this route. 1322 * @param name Name to display to the user to describe this route 1323 */ 1324 public void setName(CharSequence name) { 1325 mName = name; 1326 routeUpdated(); 1327 } 1328 1329 /** 1330 * Set the user-visible name of this route. 1331 * @param resId Resource ID of the name to display to the user to describe this route 1332 */ 1333 public void setName(int resId) { 1334 mNameResId = resId; 1335 mName = null; 1336 routeUpdated(); 1337 } 1338 1339 /** 1340 * Set the current user-visible status for this route. 1341 * @param status Status to display to the user to describe what the endpoint 1342 * of this route is currently doing 1343 */ 1344 public void setStatus(CharSequence status) { 1345 setStatusInt(status); 1346 } 1347 1348 /** 1349 * Set the RemoteControlClient responsible for reporting playback info for this 1350 * user route. 1351 * 1352 * <p>If this route manages remote playback, the data exposed by this 1353 * RemoteControlClient will be used to reflect and update information 1354 * such as route volume info in related UIs.</p> 1355 * 1356 * <p>The RemoteControlClient must have been previously registered with 1357 * {@link AudioManager#registerRemoteControlClient(RemoteControlClient)}.</p> 1358 * 1359 * @param rcc RemoteControlClient associated with this route 1360 */ 1361 public void setRemoteControlClient(RemoteControlClient rcc) { 1362 mRcc = rcc; 1363 updatePlaybackInfoOnRcc(); 1364 } 1365 1366 /** 1367 * Retrieve the RemoteControlClient associated with this route, if one has been set. 1368 * 1369 * @return the RemoteControlClient associated with this route 1370 * @see #setRemoteControlClient(RemoteControlClient) 1371 */ 1372 public RemoteControlClient getRemoteControlClient() { 1373 return mRcc; 1374 } 1375 1376 /** 1377 * Set an icon that will be used to represent this route. 1378 * The system may use this icon in picker UIs or similar. 1379 * 1380 * @param icon icon drawable to use to represent this route 1381 */ 1382 public void setIconDrawable(Drawable icon) { 1383 mIcon = icon; 1384 } 1385 1386 /** 1387 * Set an icon that will be used to represent this route. 1388 * The system may use this icon in picker UIs or similar. 1389 * 1390 * @param resId Resource ID of an icon drawable to use to represent this route 1391 */ 1392 public void setIconResource(int resId) { 1393 setIconDrawable(sStatic.mResources.getDrawable(resId)); 1394 } 1395 1396 /** 1397 * Set a callback to be notified of volume update requests 1398 * @param vcb 1399 */ 1400 public void setVolumeCallback(VolumeCallback vcb) { 1401 mVcb = new VolumeCallbackInfo(vcb, this); 1402 } 1403 1404 /** 1405 * Defines whether playback associated with this route is "local" 1406 * ({@link RouteInfo#PLAYBACK_TYPE_LOCAL}) or "remote" 1407 * ({@link RouteInfo#PLAYBACK_TYPE_REMOTE}). 1408 * @param type 1409 */ 1410 public void setPlaybackType(int type) { 1411 if (mPlaybackType != type) { 1412 mPlaybackType = type; 1413 setPlaybackInfoOnRcc(RemoteControlClient.PLAYBACKINFO_PLAYBACK_TYPE, type); 1414 } 1415 } 1416 1417 /** 1418 * Defines whether volume for the playback associated with this route is fixed 1419 * ({@link RouteInfo#PLAYBACK_VOLUME_FIXED}) or can modified 1420 * ({@link RouteInfo#PLAYBACK_VOLUME_VARIABLE}). 1421 * @param volumeHandling 1422 */ 1423 public void setVolumeHandling(int volumeHandling) { 1424 if (mVolumeHandling != volumeHandling) { 1425 mVolumeHandling = volumeHandling; 1426 setPlaybackInfoOnRcc( 1427 RemoteControlClient.PLAYBACKINFO_VOLUME_HANDLING, volumeHandling); 1428 } 1429 } 1430 1431 /** 1432 * Defines at what volume the playback associated with this route is performed (for user 1433 * feedback purposes). This information is only used when the playback is not local. 1434 * @param volume 1435 */ 1436 public void setVolume(int volume) { 1437 volume = Math.max(0, Math.min(volume, getVolumeMax())); 1438 if (mVolume != volume) { 1439 mVolume = volume; 1440 setPlaybackInfoOnRcc(RemoteControlClient.PLAYBACKINFO_VOLUME, volume); 1441 dispatchRouteVolumeChanged(this); 1442 if (mGroup != null) { 1443 mGroup.memberVolumeChanged(this); 1444 } 1445 } 1446 } 1447 1448 @Override 1449 public void requestSetVolume(int volume) { 1450 if (mVolumeHandling == PLAYBACK_VOLUME_VARIABLE) { 1451 if (mVcb == null) { 1452 Log.e(TAG, "Cannot requestSetVolume on user route - no volume callback set"); 1453 return; 1454 } 1455 mVcb.vcb.onVolumeSetRequest(this, volume); 1456 } 1457 } 1458 1459 @Override 1460 public void requestUpdateVolume(int direction) { 1461 if (mVolumeHandling == PLAYBACK_VOLUME_VARIABLE) { 1462 if (mVcb == null) { 1463 Log.e(TAG, "Cannot requestChangeVolume on user route - no volumec callback set"); 1464 return; 1465 } 1466 mVcb.vcb.onVolumeUpdateRequest(this, direction); 1467 } 1468 } 1469 1470 /** 1471 * Defines the maximum volume at which the playback associated with this route is performed 1472 * (for user feedback purposes). This information is only used when the playback is not 1473 * local. 1474 * @param volumeMax 1475 */ 1476 public void setVolumeMax(int volumeMax) { 1477 if (mVolumeMax != volumeMax) { 1478 mVolumeMax = volumeMax; 1479 setPlaybackInfoOnRcc(RemoteControlClient.PLAYBACKINFO_VOLUME_MAX, volumeMax); 1480 } 1481 } 1482 1483 /** 1484 * Defines over what stream type the media is presented. 1485 * @param stream 1486 */ 1487 public void setPlaybackStream(int stream) { 1488 if (mPlaybackStream != stream) { 1489 mPlaybackStream = stream; 1490 setPlaybackInfoOnRcc(RemoteControlClient.PLAYBACKINFO_USES_STREAM, stream); 1491 } 1492 } 1493 1494 private void updatePlaybackInfoOnRcc() { 1495 if ((mRcc != null) && (mRcc.getRcseId() != RemoteControlClient.RCSE_ID_UNREGISTERED)) { 1496 mRcc.setPlaybackInformation( 1497 RemoteControlClient.PLAYBACKINFO_VOLUME_MAX, mVolumeMax); 1498 mRcc.setPlaybackInformation( 1499 RemoteControlClient.PLAYBACKINFO_VOLUME, mVolume); 1500 mRcc.setPlaybackInformation( 1501 RemoteControlClient.PLAYBACKINFO_VOLUME_HANDLING, mVolumeHandling); 1502 mRcc.setPlaybackInformation( 1503 RemoteControlClient.PLAYBACKINFO_USES_STREAM, mPlaybackStream); 1504 mRcc.setPlaybackInformation( 1505 RemoteControlClient.PLAYBACKINFO_PLAYBACK_TYPE, mPlaybackType); 1506 // let AudioService know whom to call when remote volume needs to be updated 1507 try { 1508 sStatic.mAudioService.registerRemoteVolumeObserverForRcc( 1509 mRcc.getRcseId() /* rccId */, mRemoteVolObserver /* rvo */); 1510 } catch (RemoteException e) { 1511 Log.e(TAG, "Error registering remote volume observer", e); 1512 } 1513 } 1514 } 1515 1516 private void setPlaybackInfoOnRcc(int what, int value) { 1517 if (mRcc != null) { 1518 mRcc.setPlaybackInformation(what, value); 1519 } 1520 } 1521 } 1522 1523 /** 1524 * Information about a route that consists of multiple other routes in a group. 1525 */ 1526 public static class RouteGroup extends RouteInfo { 1527 final ArrayList<RouteInfo> mRoutes = new ArrayList<RouteInfo>(); 1528 private boolean mUpdateName; 1529 1530 RouteGroup(RouteCategory category) { 1531 super(category); 1532 mGroup = this; 1533 mVolumeHandling = PLAYBACK_VOLUME_FIXED; 1534 } 1535 1536 CharSequence getName(Resources res) { 1537 if (mUpdateName) updateName(); 1538 return super.getName(res); 1539 } 1540 1541 /** 1542 * Add a route to this group. The route must not currently belong to another group. 1543 * 1544 * @param route route to add to this group 1545 */ 1546 public void addRoute(RouteInfo route) { 1547 if (route.getGroup() != null) { 1548 throw new IllegalStateException("Route " + route + " is already part of a group."); 1549 } 1550 if (route.getCategory() != mCategory) { 1551 throw new IllegalArgumentException( 1552 "Route cannot be added to a group with a different category. " + 1553 "(Route category=" + route.getCategory() + 1554 " group category=" + mCategory + ")"); 1555 } 1556 final int at = mRoutes.size(); 1557 mRoutes.add(route); 1558 route.mGroup = this; 1559 mUpdateName = true; 1560 updateVolume(); 1561 routeUpdated(); 1562 dispatchRouteGrouped(route, this, at); 1563 } 1564 1565 /** 1566 * Add a route to this group before the specified index. 1567 * 1568 * @param route route to add 1569 * @param insertAt insert the new route before this index 1570 */ 1571 public void addRoute(RouteInfo route, int insertAt) { 1572 if (route.getGroup() != null) { 1573 throw new IllegalStateException("Route " + route + " is already part of a group."); 1574 } 1575 if (route.getCategory() != mCategory) { 1576 throw new IllegalArgumentException( 1577 "Route cannot be added to a group with a different category. " + 1578 "(Route category=" + route.getCategory() + 1579 " group category=" + mCategory + ")"); 1580 } 1581 mRoutes.add(insertAt, route); 1582 route.mGroup = this; 1583 mUpdateName = true; 1584 updateVolume(); 1585 routeUpdated(); 1586 dispatchRouteGrouped(route, this, insertAt); 1587 } 1588 1589 /** 1590 * Remove a route from this group. 1591 * 1592 * @param route route to remove 1593 */ 1594 public void removeRoute(RouteInfo route) { 1595 if (route.getGroup() != this) { 1596 throw new IllegalArgumentException("Route " + route + 1597 " is not a member of this group."); 1598 } 1599 mRoutes.remove(route); 1600 route.mGroup = null; 1601 mUpdateName = true; 1602 updateVolume(); 1603 dispatchRouteUngrouped(route, this); 1604 routeUpdated(); 1605 } 1606 1607 /** 1608 * Remove the route at the specified index from this group. 1609 * 1610 * @param index index of the route to remove 1611 */ 1612 public void removeRoute(int index) { 1613 RouteInfo route = mRoutes.remove(index); 1614 route.mGroup = null; 1615 mUpdateName = true; 1616 updateVolume(); 1617 dispatchRouteUngrouped(route, this); 1618 routeUpdated(); 1619 } 1620 1621 /** 1622 * @return The number of routes in this group 1623 */ 1624 public int getRouteCount() { 1625 return mRoutes.size(); 1626 } 1627 1628 /** 1629 * Return the route in this group at the specified index 1630 * 1631 * @param index Index to fetch 1632 * @return The route at index 1633 */ 1634 public RouteInfo getRouteAt(int index) { 1635 return mRoutes.get(index); 1636 } 1637 1638 /** 1639 * Set an icon that will be used to represent this group. 1640 * The system may use this icon in picker UIs or similar. 1641 * 1642 * @param icon icon drawable to use to represent this group 1643 */ 1644 public void setIconDrawable(Drawable icon) { 1645 mIcon = icon; 1646 } 1647 1648 /** 1649 * Set an icon that will be used to represent this group. 1650 * The system may use this icon in picker UIs or similar. 1651 * 1652 * @param resId Resource ID of an icon drawable to use to represent this group 1653 */ 1654 public void setIconResource(int resId) { 1655 setIconDrawable(sStatic.mResources.getDrawable(resId)); 1656 } 1657 1658 @Override 1659 public void requestSetVolume(int volume) { 1660 final int maxVol = getVolumeMax(); 1661 if (maxVol == 0) { 1662 return; 1663 } 1664 1665 final float scaledVolume = (float) volume / maxVol; 1666 final int routeCount = getRouteCount(); 1667 for (int i = 0; i < routeCount; i++) { 1668 final RouteInfo route = getRouteAt(i); 1669 final int routeVol = (int) (scaledVolume * route.getVolumeMax()); 1670 route.requestSetVolume(routeVol); 1671 } 1672 if (volume != mVolume) { 1673 mVolume = volume; 1674 dispatchRouteVolumeChanged(this); 1675 } 1676 } 1677 1678 @Override 1679 public void requestUpdateVolume(int direction) { 1680 final int maxVol = getVolumeMax(); 1681 if (maxVol == 0) { 1682 return; 1683 } 1684 1685 final int routeCount = getRouteCount(); 1686 int volume = 0; 1687 for (int i = 0; i < routeCount; i++) { 1688 final RouteInfo route = getRouteAt(i); 1689 route.requestUpdateVolume(direction); 1690 final int routeVol = route.getVolume(); 1691 if (routeVol > volume) { 1692 volume = routeVol; 1693 } 1694 } 1695 if (volume != mVolume) { 1696 mVolume = volume; 1697 dispatchRouteVolumeChanged(this); 1698 } 1699 } 1700 1701 void memberNameChanged(RouteInfo info, CharSequence name) { 1702 mUpdateName = true; 1703 routeUpdated(); 1704 } 1705 1706 void memberStatusChanged(RouteInfo info, CharSequence status) { 1707 setStatusInt(status); 1708 } 1709 1710 void memberVolumeChanged(RouteInfo info) { 1711 updateVolume(); 1712 } 1713 1714 void updateVolume() { 1715 // A group always represents the highest component volume value. 1716 final int routeCount = getRouteCount(); 1717 int volume = 0; 1718 for (int i = 0; i < routeCount; i++) { 1719 final int routeVol = getRouteAt(i).getVolume(); 1720 if (routeVol > volume) { 1721 volume = routeVol; 1722 } 1723 } 1724 if (volume != mVolume) { 1725 mVolume = volume; 1726 dispatchRouteVolumeChanged(this); 1727 } 1728 } 1729 1730 @Override 1731 void routeUpdated() { 1732 int types = 0; 1733 final int count = mRoutes.size(); 1734 if (count == 0) { 1735 // Don't keep empty groups in the router. 1736 MediaRouter.removeRoute(this); 1737 return; 1738 } 1739 1740 int maxVolume = 0; 1741 boolean isLocal = true; 1742 boolean isFixedVolume = true; 1743 for (int i = 0; i < count; i++) { 1744 final RouteInfo route = mRoutes.get(i); 1745 types |= route.mSupportedTypes; 1746 final int routeMaxVolume = route.getVolumeMax(); 1747 if (routeMaxVolume > maxVolume) { 1748 maxVolume = routeMaxVolume; 1749 } 1750 isLocal &= route.getPlaybackType() == PLAYBACK_TYPE_LOCAL; 1751 isFixedVolume &= route.getVolumeHandling() == PLAYBACK_VOLUME_FIXED; 1752 } 1753 mPlaybackType = isLocal ? PLAYBACK_TYPE_LOCAL : PLAYBACK_TYPE_REMOTE; 1754 mVolumeHandling = isFixedVolume ? PLAYBACK_VOLUME_FIXED : PLAYBACK_VOLUME_VARIABLE; 1755 mSupportedTypes = types; 1756 mVolumeMax = maxVolume; 1757 mIcon = count == 1 ? mRoutes.get(0).getIconDrawable() : null; 1758 super.routeUpdated(); 1759 } 1760 1761 void updateName() { 1762 final StringBuilder sb = new StringBuilder(); 1763 final int count = mRoutes.size(); 1764 for (int i = 0; i < count; i++) { 1765 final RouteInfo info = mRoutes.get(i); 1766 // TODO: There's probably a much more correct way to localize this. 1767 if (i > 0) sb.append(", "); 1768 sb.append(info.mName); 1769 } 1770 mName = sb.toString(); 1771 mUpdateName = false; 1772 } 1773 1774 @Override 1775 public String toString() { 1776 StringBuilder sb = new StringBuilder(super.toString()); 1777 sb.append('['); 1778 final int count = mRoutes.size(); 1779 for (int i = 0; i < count; i++) { 1780 if (i > 0) sb.append(", "); 1781 sb.append(mRoutes.get(i)); 1782 } 1783 sb.append(']'); 1784 return sb.toString(); 1785 } 1786 } 1787 1788 /** 1789 * Definition of a category of routes. All routes belong to a category. 1790 */ 1791 public static class RouteCategory { 1792 CharSequence mName; 1793 int mNameResId; 1794 int mTypes; 1795 final boolean mGroupable; 1796 boolean mIsSystem; 1797 1798 RouteCategory(CharSequence name, int types, boolean groupable) { 1799 mName = name; 1800 mTypes = types; 1801 mGroupable = groupable; 1802 } 1803 1804 RouteCategory(int nameResId, int types, boolean groupable) { 1805 mNameResId = nameResId; 1806 mTypes = types; 1807 mGroupable = groupable; 1808 } 1809 1810 /** 1811 * @return the name of this route category 1812 */ 1813 public CharSequence getName() { 1814 return getName(sStatic.mResources); 1815 } 1816 1817 /** 1818 * Return the properly localized/configuration dependent name of this RouteCategory. 1819 * 1820 * @param context Context to resolve name resources 1821 * @return the name of this route category 1822 */ 1823 public CharSequence getName(Context context) { 1824 return getName(context.getResources()); 1825 } 1826 1827 CharSequence getName(Resources res) { 1828 if (mNameResId != 0) { 1829 return res.getText(mNameResId); 1830 } 1831 return mName; 1832 } 1833 1834 /** 1835 * Return the current list of routes in this category that have been added 1836 * to the MediaRouter. 1837 * 1838 * <p>This list will not include routes that are nested within RouteGroups. 1839 * A RouteGroup is treated as a single route within its category.</p> 1840 * 1841 * @param out a List to fill with the routes in this category. If this parameter is 1842 * non-null, it will be cleared, filled with the current routes with this 1843 * category, and returned. If this parameter is null, a new List will be 1844 * allocated to report the category's current routes. 1845 * @return A list with the routes in this category that have been added to the MediaRouter. 1846 */ 1847 public List<RouteInfo> getRoutes(List<RouteInfo> out) { 1848 if (out == null) { 1849 out = new ArrayList<RouteInfo>(); 1850 } else { 1851 out.clear(); 1852 } 1853 1854 final int count = getRouteCountStatic(); 1855 for (int i = 0; i < count; i++) { 1856 final RouteInfo route = getRouteAtStatic(i); 1857 if (route.mCategory == this) { 1858 out.add(route); 1859 } 1860 } 1861 return out; 1862 } 1863 1864 /** 1865 * @return Flag set describing the route types supported by this category 1866 */ 1867 public int getSupportedTypes() { 1868 return mTypes; 1869 } 1870 1871 /** 1872 * Return whether or not this category supports grouping. 1873 * 1874 * <p>If this method returns true, all routes obtained from this category 1875 * via calls to {@link #getRouteAt(int)} will be {@link MediaRouter.RouteGroup}s.</p> 1876 * 1877 * @return true if this category supports 1878 */ 1879 public boolean isGroupable() { 1880 return mGroupable; 1881 } 1882 1883 /** 1884 * @return true if this is the category reserved for system routes. 1885 * @hide 1886 */ 1887 public boolean isSystem() { 1888 return mIsSystem; 1889 } 1890 1891 public String toString() { 1892 return "RouteCategory{ name=" + mName + " types=" + typesToString(mTypes) + 1893 " groupable=" + mGroupable + " }"; 1894 } 1895 } 1896 1897 static class CallbackInfo { 1898 public int type; 1899 public final Callback cb; 1900 public final MediaRouter router; 1901 1902 public CallbackInfo(Callback cb, int type, MediaRouter router) { 1903 this.cb = cb; 1904 this.type = type; 1905 this.router = router; 1906 } 1907 } 1908 1909 /** 1910 * Interface for receiving events about media routing changes. 1911 * All methods of this interface will be called from the application's main thread. 1912 * 1913 * <p>A Callback will only receive events relevant to routes that the callback 1914 * was registered for.</p> 1915 * 1916 * @see MediaRouter#addCallback(int, Callback) 1917 * @see MediaRouter#removeCallback(Callback) 1918 */ 1919 public static abstract class Callback { 1920 /** 1921 * Called when the supplied route becomes selected as the active route 1922 * for the given route type. 1923 * 1924 * @param router the MediaRouter reporting the event 1925 * @param type Type flag set indicating the routes that have been selected 1926 * @param info Route that has been selected for the given route types 1927 */ 1928 public abstract void onRouteSelected(MediaRouter router, int type, RouteInfo info); 1929 1930 /** 1931 * Called when the supplied route becomes unselected as the active route 1932 * for the given route type. 1933 * 1934 * @param router the MediaRouter reporting the event 1935 * @param type Type flag set indicating the routes that have been unselected 1936 * @param info Route that has been unselected for the given route types 1937 */ 1938 public abstract void onRouteUnselected(MediaRouter router, int type, RouteInfo info); 1939 1940 /** 1941 * Called when a route for the specified type was added. 1942 * 1943 * @param router the MediaRouter reporting the event 1944 * @param info Route that has become available for use 1945 */ 1946 public abstract void onRouteAdded(MediaRouter router, RouteInfo info); 1947 1948 /** 1949 * Called when a route for the specified type was removed. 1950 * 1951 * @param router the MediaRouter reporting the event 1952 * @param info Route that has been removed from availability 1953 */ 1954 public abstract void onRouteRemoved(MediaRouter router, RouteInfo info); 1955 1956 /** 1957 * Called when an aspect of the indicated route has changed. 1958 * 1959 * <p>This will not indicate that the types supported by this route have 1960 * changed, only that cosmetic info such as name or status have been updated.</p> 1961 * 1962 * @param router the MediaRouter reporting the event 1963 * @param info The route that was changed 1964 */ 1965 public abstract void onRouteChanged(MediaRouter router, RouteInfo info); 1966 1967 /** 1968 * Called when a route is added to a group. 1969 * 1970 * @param router the MediaRouter reporting the event 1971 * @param info The route that was added 1972 * @param group The group the route was added to 1973 * @param index The route index within group that info was added at 1974 */ 1975 public abstract void onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group, 1976 int index); 1977 1978 /** 1979 * Called when a route is removed from a group. 1980 * 1981 * @param router the MediaRouter reporting the event 1982 * @param info The route that was removed 1983 * @param group The group the route was removed from 1984 */ 1985 public abstract void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group); 1986 1987 /** 1988 * Called when a route's volume changes. 1989 * 1990 * @param router the MediaRouter reporting the event 1991 * @param info The route with altered volume 1992 */ 1993 public abstract void onRouteVolumeChanged(MediaRouter router, RouteInfo info); 1994 1995 /** 1996 * Called when a route's presentation display changes. 1997 * <p> 1998 * This method is called whenever the route's presentation display becomes 1999 * available, is removes or has changes to some of its properties (such as its size). 2000 * </p> 2001 * 2002 * @param router the MediaRouter reporting the event 2003 * @param info The route whose presentation display changed 2004 * 2005 * @see RouteInfo#getPresentationDisplay() 2006 */ 2007 public void onRoutePresentationDisplayChanged(MediaRouter router, RouteInfo info) { 2008 } 2009 } 2010 2011 /** 2012 * Stub implementation of {@link MediaRouter.Callback}. 2013 * Each abstract method is defined as a no-op. Override just the ones 2014 * you need. 2015 */ 2016 public static class SimpleCallback extends Callback { 2017 2018 @Override 2019 public void onRouteSelected(MediaRouter router, int type, RouteInfo info) { 2020 } 2021 2022 @Override 2023 public void onRouteUnselected(MediaRouter router, int type, RouteInfo info) { 2024 } 2025 2026 @Override 2027 public void onRouteAdded(MediaRouter router, RouteInfo info) { 2028 } 2029 2030 @Override 2031 public void onRouteRemoved(MediaRouter router, RouteInfo info) { 2032 } 2033 2034 @Override 2035 public void onRouteChanged(MediaRouter router, RouteInfo info) { 2036 } 2037 2038 @Override 2039 public void onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group, 2040 int index) { 2041 } 2042 2043 @Override 2044 public void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group) { 2045 } 2046 2047 @Override 2048 public void onRouteVolumeChanged(MediaRouter router, RouteInfo info) { 2049 } 2050 } 2051 2052 static class VolumeCallbackInfo { 2053 public final VolumeCallback vcb; 2054 public final RouteInfo route; 2055 2056 public VolumeCallbackInfo(VolumeCallback vcb, RouteInfo route) { 2057 this.vcb = vcb; 2058 this.route = route; 2059 } 2060 } 2061 2062 /** 2063 * Interface for receiving events about volume changes. 2064 * All methods of this interface will be called from the application's main thread. 2065 * 2066 * <p>A VolumeCallback will only receive events relevant to routes that the callback 2067 * was registered for.</p> 2068 * 2069 * @see UserRouteInfo#setVolumeCallback(VolumeCallback) 2070 */ 2071 public static abstract class VolumeCallback { 2072 /** 2073 * Called when the volume for the route should be increased or decreased. 2074 * @param info the route affected by this event 2075 * @param direction an integer indicating whether the volume is to be increased 2076 * (positive value) or decreased (negative value). 2077 * For bundled changes, the absolute value indicates the number of changes 2078 * in the same direction, e.g. +3 corresponds to three "volume up" changes. 2079 */ 2080 public abstract void onVolumeUpdateRequest(RouteInfo info, int direction); 2081 /** 2082 * Called when the volume for the route should be set to the given value 2083 * @param info the route affected by this event 2084 * @param volume an integer indicating the new volume value that should be used, always 2085 * between 0 and the value set by {@link UserRouteInfo#setVolumeMax(int)}. 2086 */ 2087 public abstract void onVolumeSetRequest(RouteInfo info, int volume); 2088 } 2089 2090 static class VolumeChangeReceiver extends BroadcastReceiver { 2091 @Override 2092 public void onReceive(Context context, Intent intent) { 2093 if (intent.getAction().equals(AudioManager.VOLUME_CHANGED_ACTION)) { 2094 final int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, 2095 -1); 2096 if (streamType != AudioManager.STREAM_MUSIC) { 2097 return; 2098 } 2099 2100 final int newVolume = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, 0); 2101 final int oldVolume = intent.getIntExtra( 2102 AudioManager.EXTRA_PREV_VOLUME_STREAM_VALUE, 0); 2103 if (newVolume != oldVolume) { 2104 systemVolumeChanged(newVolume); 2105 } 2106 } 2107 } 2108 } 2109 2110 static class WifiDisplayStatusChangedReceiver extends BroadcastReceiver { 2111 @Override 2112 public void onReceive(Context context, Intent intent) { 2113 if (intent.getAction().equals(DisplayManager.ACTION_WIFI_DISPLAY_STATUS_CHANGED)) { 2114 updateWifiDisplayStatus((WifiDisplayStatus) intent.getParcelableExtra( 2115 DisplayManager.EXTRA_WIFI_DISPLAY_STATUS)); 2116 } 2117 } 2118 } 2119} 2120