MediaRouter.java revision 77e59fef24c02a109bb409afe38dbdf28e1fd914
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 boolean available = findMatchingDisplay(d, availableDisplays) != null; 787 RouteInfo route = findWifiDisplayRoute(d); 788 if (route == null) { 789 route = makeWifiDisplayRoute(d, available); 790 addRouteStatic(route); 791 wantScan = true; 792 } else { 793 updateWifiDisplayRoute(route, d, available, newStatus); 794 } 795 if (d.equals(activeDisplay)) { 796 selectRouteStatic(route.getSupportedTypes(), route); 797 798 // Don't scan if we're already connected to a wifi display, 799 // the scanning process can cause a hiccup with some configurations. 800 blockScan = true; 801 } 802 } 803 for (int i = 0; i < oldDisplays.length; i++) { 804 final WifiDisplay d = oldDisplays[i]; 805 final WifiDisplay newDisplay = findMatchingDisplay(d, newDisplays); 806 if (newDisplay == null) { 807 removeRoute(findWifiDisplayRoute(d)); 808 } 809 } 810 811 if (wantScan && !blockScan) { 812 sStatic.mDisplayService.scanWifiDisplays(); 813 } 814 815 sStatic.mLastKnownWifiDisplayStatus = newStatus; 816 } 817 818 static RouteInfo makeWifiDisplayRoute(WifiDisplay display, boolean available) { 819 final RouteInfo newRoute = new RouteInfo(sStatic.mSystemCategory); 820 newRoute.mDeviceAddress = display.getDeviceAddress(); 821 newRoute.mSupportedTypes = ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_LIVE_VIDEO; 822 newRoute.mVolumeHandling = RouteInfo.PLAYBACK_VOLUME_FIXED; 823 newRoute.mPlaybackType = RouteInfo.PLAYBACK_TYPE_REMOTE; 824 825 newRoute.setStatusCode(available ? 826 RouteInfo.STATUS_AVAILABLE : RouteInfo.STATUS_CONNECTING); 827 newRoute.mEnabled = available; 828 829 newRoute.mName = display.getFriendlyDisplayName(); 830 831 newRoute.mPresentationDisplay = choosePresentationDisplayForRoute(newRoute, 832 sStatic.getAllPresentationDisplays()); 833 return newRoute; 834 } 835 836 private static void updateWifiDisplayRoute(RouteInfo route, WifiDisplay display, 837 boolean available, WifiDisplayStatus wifiDisplayStatus) { 838 final boolean isScanning = 839 wifiDisplayStatus.getScanState() == WifiDisplayStatus.SCAN_STATE_SCANNING; 840 841 boolean changed = false; 842 int newStatus = RouteInfo.STATUS_NONE; 843 844 if (available) { 845 newStatus = isScanning ? RouteInfo.STATUS_SCANNING : RouteInfo.STATUS_AVAILABLE; 846 } else { 847 newStatus = RouteInfo.STATUS_NOT_AVAILABLE; 848 } 849 850 if (display.equals(wifiDisplayStatus.getActiveDisplay())) { 851 final int activeState = wifiDisplayStatus.getActiveDisplayState(); 852 switch (activeState) { 853 case WifiDisplayStatus.DISPLAY_STATE_CONNECTED: 854 newStatus = RouteInfo.STATUS_NONE; 855 break; 856 case WifiDisplayStatus.DISPLAY_STATE_CONNECTING: 857 newStatus = RouteInfo.STATUS_CONNECTING; 858 break; 859 case WifiDisplayStatus.DISPLAY_STATE_NOT_CONNECTED: 860 Log.e(TAG, "Active display is not connected!"); 861 break; 862 } 863 } 864 865 final String newName = display.getFriendlyDisplayName(); 866 if (!route.getName().equals(newName)) { 867 route.mName = newName; 868 changed = true; 869 } 870 871 changed |= route.mEnabled != available; 872 route.mEnabled = available; 873 874 changed |= route.setStatusCode(newStatus); 875 876 if (changed) { 877 dispatchRouteChanged(route); 878 } 879 880 if (!available && route == sStatic.mSelectedRoute) { 881 // Oops, no longer available. Reselect the default. 882 final RouteInfo defaultRoute = sStatic.mDefaultAudioVideo; 883 selectRouteStatic(defaultRoute.getSupportedTypes(), defaultRoute); 884 } 885 } 886 887 private static WifiDisplay findMatchingDisplay(WifiDisplay d, WifiDisplay[] displays) { 888 for (int i = 0; i < displays.length; i++) { 889 final WifiDisplay other = displays[i]; 890 if (d.hasSameAddress(other)) { 891 return other; 892 } 893 } 894 return null; 895 } 896 897 private static RouteInfo findWifiDisplayRoute(WifiDisplay d) { 898 final int count = sStatic.mRoutes.size(); 899 for (int i = 0; i < count; i++) { 900 final RouteInfo info = sStatic.mRoutes.get(i); 901 if (d.getDeviceAddress().equals(info.mDeviceAddress)) { 902 return info; 903 } 904 } 905 return null; 906 } 907 908 private static Display choosePresentationDisplayForRoute(RouteInfo route, Display[] displays) { 909 if ((route.mSupportedTypes & ROUTE_TYPE_LIVE_VIDEO) != 0) { 910 if (route.mDeviceAddress != null) { 911 // Find the indicated Wifi display by its address. 912 for (Display display : displays) { 913 if (display.getType() == Display.TYPE_WIFI 914 && route.mDeviceAddress.equals(display.getAddress())) { 915 return display; 916 } 917 } 918 return null; 919 } 920 921 if (route == sStatic.mDefaultAudioVideo && displays.length > 0) { 922 // Choose the first presentation display from the list. 923 return displays[0]; 924 } 925 } 926 return null; 927 } 928 929 /** 930 * Information about a media route. 931 */ 932 public static class RouteInfo { 933 CharSequence mName; 934 int mNameResId; 935 private CharSequence mStatus; 936 int mSupportedTypes; 937 RouteGroup mGroup; 938 final RouteCategory mCategory; 939 Drawable mIcon; 940 // playback information 941 int mPlaybackType = PLAYBACK_TYPE_LOCAL; 942 int mVolumeMax = RemoteControlClient.DEFAULT_PLAYBACK_VOLUME; 943 int mVolume = RemoteControlClient.DEFAULT_PLAYBACK_VOLUME; 944 int mVolumeHandling = RemoteControlClient.DEFAULT_PLAYBACK_VOLUME_HANDLING; 945 int mPlaybackStream = AudioManager.STREAM_MUSIC; 946 VolumeCallbackInfo mVcb; 947 Display mPresentationDisplay; 948 949 String mDeviceAddress; 950 boolean mEnabled = true; 951 952 // A predetermined connection status that can override mStatus 953 private int mStatusCode; 954 955 /** @hide */ public static final int STATUS_NONE = 0; 956 /** @hide */ public static final int STATUS_SCANNING = 1; 957 /** @hide */ public static final int STATUS_CONNECTING = 2; 958 /** @hide */ public static final int STATUS_AVAILABLE = 3; 959 /** @hide */ public static final int STATUS_NOT_AVAILABLE = 4; 960 961 private Object mTag; 962 963 /** 964 * The default playback type, "local", indicating the presentation of the media is happening 965 * on the same device (e.g. a phone, a tablet) as where it is controlled from. 966 * @see #setPlaybackType(int) 967 */ 968 public final static int PLAYBACK_TYPE_LOCAL = 0; 969 /** 970 * A playback type indicating the presentation of the media is happening on 971 * a different device (i.e. the remote device) than where it is controlled from. 972 * @see #setPlaybackType(int) 973 */ 974 public final static int PLAYBACK_TYPE_REMOTE = 1; 975 /** 976 * Playback information indicating the playback volume is fixed, i.e. it cannot be 977 * controlled from this object. An example of fixed playback volume is a remote player, 978 * playing over HDMI where the user prefers to control the volume on the HDMI sink, rather 979 * than attenuate at the source. 980 * @see #setVolumeHandling(int) 981 */ 982 public final static int PLAYBACK_VOLUME_FIXED = 0; 983 /** 984 * Playback information indicating the playback volume is variable and can be controlled 985 * from this object. 986 */ 987 public final static int PLAYBACK_VOLUME_VARIABLE = 1; 988 989 RouteInfo(RouteCategory category) { 990 mCategory = category; 991 } 992 993 /** 994 * @return The user-friendly name of a media route. This is the string presented 995 * to users who may select this as the active route. 996 */ 997 public CharSequence getName() { 998 return getName(sStatic.mResources); 999 } 1000 1001 /** 1002 * Return the properly localized/resource selected name of this route. 1003 * 1004 * @param context Context used to resolve the correct configuration to load 1005 * @return The user-friendly name of the media route. This is the string presented 1006 * to users who may select this as the active route. 1007 */ 1008 public CharSequence getName(Context context) { 1009 return getName(context.getResources()); 1010 } 1011 1012 CharSequence getName(Resources res) { 1013 if (mNameResId != 0) { 1014 return mName = res.getText(mNameResId); 1015 } 1016 return mName; 1017 } 1018 1019 /** 1020 * @return The user-friendly status for a media route. This may include a description 1021 * of the currently playing media, if available. 1022 */ 1023 public CharSequence getStatus() { 1024 return mStatus; 1025 } 1026 1027 /** 1028 * Set this route's status by predetermined status code. If the caller 1029 * should dispatch a route changed event this call will return true; 1030 */ 1031 boolean setStatusCode(int statusCode) { 1032 if (statusCode != mStatusCode) { 1033 mStatusCode = statusCode; 1034 int resId = 0; 1035 switch (statusCode) { 1036 case STATUS_SCANNING: 1037 resId = com.android.internal.R.string.media_route_status_scanning; 1038 break; 1039 case STATUS_CONNECTING: 1040 resId = com.android.internal.R.string.media_route_status_connecting; 1041 break; 1042 case STATUS_AVAILABLE: 1043 resId = com.android.internal.R.string.media_route_status_available; 1044 break; 1045 case STATUS_NOT_AVAILABLE: 1046 resId = com.android.internal.R.string.media_route_status_not_available; 1047 break; 1048 } 1049 mStatus = resId != 0 ? sStatic.mResources.getText(resId) : null; 1050 return true; 1051 } 1052 return false; 1053 } 1054 1055 /** 1056 * @hide 1057 */ 1058 public int getStatusCode() { 1059 return mStatusCode; 1060 } 1061 1062 /** 1063 * @return A media type flag set describing which types this route supports. 1064 */ 1065 public int getSupportedTypes() { 1066 return mSupportedTypes; 1067 } 1068 1069 /** 1070 * @return The group that this route belongs to. 1071 */ 1072 public RouteGroup getGroup() { 1073 return mGroup; 1074 } 1075 1076 /** 1077 * @return the category this route belongs to. 1078 */ 1079 public RouteCategory getCategory() { 1080 return mCategory; 1081 } 1082 1083 /** 1084 * Get the icon representing this route. 1085 * This icon will be used in picker UIs if available. 1086 * 1087 * @return the icon representing this route or null if no icon is available 1088 */ 1089 public Drawable getIconDrawable() { 1090 return mIcon; 1091 } 1092 1093 /** 1094 * Set an application-specific tag object for this route. 1095 * The application may use this to store arbitrary data associated with the 1096 * route for internal tracking. 1097 * 1098 * <p>Note that the lifespan of a route may be well past the lifespan of 1099 * an Activity or other Context; take care that objects you store here 1100 * will not keep more data in memory alive than you intend.</p> 1101 * 1102 * @param tag Arbitrary, app-specific data for this route to hold for later use 1103 */ 1104 public void setTag(Object tag) { 1105 mTag = tag; 1106 routeUpdated(); 1107 } 1108 1109 /** 1110 * @return The tag object previously set by the application 1111 * @see #setTag(Object) 1112 */ 1113 public Object getTag() { 1114 return mTag; 1115 } 1116 1117 /** 1118 * @return the type of playback associated with this route 1119 * @see UserRouteInfo#setPlaybackType(int) 1120 */ 1121 public int getPlaybackType() { 1122 return mPlaybackType; 1123 } 1124 1125 /** 1126 * @return the stream over which the playback associated with this route is performed 1127 * @see UserRouteInfo#setPlaybackStream(int) 1128 */ 1129 public int getPlaybackStream() { 1130 return mPlaybackStream; 1131 } 1132 1133 /** 1134 * Return the current volume for this route. Depending on the route, this may only 1135 * be valid if the route is currently selected. 1136 * 1137 * @return the volume at which the playback associated with this route is performed 1138 * @see UserRouteInfo#setVolume(int) 1139 */ 1140 public int getVolume() { 1141 if (mPlaybackType == PLAYBACK_TYPE_LOCAL) { 1142 int vol = 0; 1143 try { 1144 vol = sStatic.mAudioService.getStreamVolume(mPlaybackStream); 1145 } catch (RemoteException e) { 1146 Log.e(TAG, "Error getting local stream volume", e); 1147 } 1148 return vol; 1149 } else { 1150 return mVolume; 1151 } 1152 } 1153 1154 /** 1155 * Request a volume change for this route. 1156 * @param volume value between 0 and getVolumeMax 1157 */ 1158 public void requestSetVolume(int volume) { 1159 if (mPlaybackType == PLAYBACK_TYPE_LOCAL) { 1160 try { 1161 sStatic.mAudioService.setStreamVolume(mPlaybackStream, volume, 0); 1162 } catch (RemoteException e) { 1163 Log.e(TAG, "Error setting local stream volume", e); 1164 } 1165 } else { 1166 Log.e(TAG, getClass().getSimpleName() + ".requestSetVolume(): " + 1167 "Non-local volume playback on system route? " + 1168 "Could not request volume change."); 1169 } 1170 } 1171 1172 /** 1173 * Request an incremental volume update for this route. 1174 * @param direction Delta to apply to the current volume 1175 */ 1176 public void requestUpdateVolume(int direction) { 1177 if (mPlaybackType == PLAYBACK_TYPE_LOCAL) { 1178 try { 1179 final int volume = 1180 Math.max(0, Math.min(getVolume() + direction, getVolumeMax())); 1181 sStatic.mAudioService.setStreamVolume(mPlaybackStream, volume, 0); 1182 } catch (RemoteException e) { 1183 Log.e(TAG, "Error setting local stream volume", e); 1184 } 1185 } else { 1186 Log.e(TAG, getClass().getSimpleName() + ".requestChangeVolume(): " + 1187 "Non-local volume playback on system route? " + 1188 "Could not request volume change."); 1189 } 1190 } 1191 1192 /** 1193 * @return the maximum volume at which the playback associated with this route is performed 1194 * @see UserRouteInfo#setVolumeMax(int) 1195 */ 1196 public int getVolumeMax() { 1197 if (mPlaybackType == PLAYBACK_TYPE_LOCAL) { 1198 int volMax = 0; 1199 try { 1200 volMax = sStatic.mAudioService.getStreamMaxVolume(mPlaybackStream); 1201 } catch (RemoteException e) { 1202 Log.e(TAG, "Error getting local stream volume", e); 1203 } 1204 return volMax; 1205 } else { 1206 return mVolumeMax; 1207 } 1208 } 1209 1210 /** 1211 * @return how volume is handling on the route 1212 * @see UserRouteInfo#setVolumeHandling(int) 1213 */ 1214 public int getVolumeHandling() { 1215 return mVolumeHandling; 1216 } 1217 1218 /** 1219 * Gets the {@link Display} that should be used by the application to show 1220 * a {@link android.app.Presentation} on an external display when this route is selected. 1221 * Depending on the route, this may only be valid if the route is currently 1222 * selected. 1223 * <p> 1224 * The preferred presentation display may change independently of the route 1225 * being selected or unselected. For example, the presentation display 1226 * of the default system route may change when an external HDMI display is connected 1227 * or disconnected even though the route itself has not changed. 1228 * </p><p> 1229 * This method may return null if there is no external display associated with 1230 * the route or if the display is not ready to show UI yet. 1231 * </p><p> 1232 * The application should listen for changes to the presentation display 1233 * using the {@link Callback#onRoutePresentationDisplayChanged} callback and 1234 * show or dismiss its {@link android.app.Presentation} accordingly when the display 1235 * becomes available or is removed. 1236 * </p><p> 1237 * This method only makes sense for {@link #ROUTE_TYPE_LIVE_VIDEO live video} routes. 1238 * </p> 1239 * 1240 * @return The preferred presentation display to use when this route is 1241 * selected or null if none. 1242 * 1243 * @see #ROUTE_TYPE_LIVE_VIDEO 1244 * @see android.app.Presentation 1245 */ 1246 public Display getPresentationDisplay() { 1247 return mPresentationDisplay; 1248 } 1249 1250 /** 1251 * @return true if this route is enabled and may be selected 1252 */ 1253 public boolean isEnabled() { 1254 return mEnabled; 1255 } 1256 1257 void setStatusInt(CharSequence status) { 1258 if (!status.equals(mStatus)) { 1259 mStatus = status; 1260 if (mGroup != null) { 1261 mGroup.memberStatusChanged(this, status); 1262 } 1263 routeUpdated(); 1264 } 1265 } 1266 1267 final IRemoteVolumeObserver.Stub mRemoteVolObserver = new IRemoteVolumeObserver.Stub() { 1268 public void dispatchRemoteVolumeUpdate(final int direction, final int value) { 1269 sStatic.mHandler.post(new Runnable() { 1270 @Override 1271 public void run() { 1272 if (mVcb != null) { 1273 if (direction != 0) { 1274 mVcb.vcb.onVolumeUpdateRequest(mVcb.route, direction); 1275 } else { 1276 mVcb.vcb.onVolumeSetRequest(mVcb.route, value); 1277 } 1278 } 1279 } 1280 }); 1281 } 1282 }; 1283 1284 void routeUpdated() { 1285 updateRoute(this); 1286 } 1287 1288 @Override 1289 public String toString() { 1290 String supportedTypes = typesToString(getSupportedTypes()); 1291 return getClass().getSimpleName() + "{ name=" + getName() + 1292 ", status=" + getStatus() + 1293 ", category=" + getCategory() + 1294 ", supportedTypes=" + supportedTypes + 1295 ", presentationDisplay=" + mPresentationDisplay + "}"; 1296 } 1297 } 1298 1299 /** 1300 * Information about a route that the application may define and modify. 1301 * A user route defaults to {@link RouteInfo#PLAYBACK_TYPE_REMOTE} and 1302 * {@link RouteInfo#PLAYBACK_VOLUME_FIXED}. 1303 * 1304 * @see MediaRouter.RouteInfo 1305 */ 1306 public static class UserRouteInfo extends RouteInfo { 1307 RemoteControlClient mRcc; 1308 1309 UserRouteInfo(RouteCategory category) { 1310 super(category); 1311 mSupportedTypes = ROUTE_TYPE_USER; 1312 mPlaybackType = PLAYBACK_TYPE_REMOTE; 1313 mVolumeHandling = PLAYBACK_VOLUME_FIXED; 1314 } 1315 1316 /** 1317 * Set the user-visible name of this route. 1318 * @param name Name to display to the user to describe this route 1319 */ 1320 public void setName(CharSequence name) { 1321 mName = name; 1322 routeUpdated(); 1323 } 1324 1325 /** 1326 * Set the user-visible name of this route. 1327 * @param resId Resource ID of the name to display to the user to describe this route 1328 */ 1329 public void setName(int resId) { 1330 mNameResId = resId; 1331 mName = null; 1332 routeUpdated(); 1333 } 1334 1335 /** 1336 * Set the current user-visible status for this route. 1337 * @param status Status to display to the user to describe what the endpoint 1338 * of this route is currently doing 1339 */ 1340 public void setStatus(CharSequence status) { 1341 setStatusInt(status); 1342 } 1343 1344 /** 1345 * Set the RemoteControlClient responsible for reporting playback info for this 1346 * user route. 1347 * 1348 * <p>If this route manages remote playback, the data exposed by this 1349 * RemoteControlClient will be used to reflect and update information 1350 * such as route volume info in related UIs.</p> 1351 * 1352 * <p>The RemoteControlClient must have been previously registered with 1353 * {@link AudioManager#registerRemoteControlClient(RemoteControlClient)}.</p> 1354 * 1355 * @param rcc RemoteControlClient associated with this route 1356 */ 1357 public void setRemoteControlClient(RemoteControlClient rcc) { 1358 mRcc = rcc; 1359 updatePlaybackInfoOnRcc(); 1360 } 1361 1362 /** 1363 * Retrieve the RemoteControlClient associated with this route, if one has been set. 1364 * 1365 * @return the RemoteControlClient associated with this route 1366 * @see #setRemoteControlClient(RemoteControlClient) 1367 */ 1368 public RemoteControlClient getRemoteControlClient() { 1369 return mRcc; 1370 } 1371 1372 /** 1373 * Set an icon that will be used to represent this route. 1374 * The system may use this icon in picker UIs or similar. 1375 * 1376 * @param icon icon drawable to use to represent this route 1377 */ 1378 public void setIconDrawable(Drawable icon) { 1379 mIcon = icon; 1380 } 1381 1382 /** 1383 * Set an icon that will be used to represent this route. 1384 * The system may use this icon in picker UIs or similar. 1385 * 1386 * @param resId Resource ID of an icon drawable to use to represent this route 1387 */ 1388 public void setIconResource(int resId) { 1389 setIconDrawable(sStatic.mResources.getDrawable(resId)); 1390 } 1391 1392 /** 1393 * Set a callback to be notified of volume update requests 1394 * @param vcb 1395 */ 1396 public void setVolumeCallback(VolumeCallback vcb) { 1397 mVcb = new VolumeCallbackInfo(vcb, this); 1398 } 1399 1400 /** 1401 * Defines whether playback associated with this route is "local" 1402 * ({@link RouteInfo#PLAYBACK_TYPE_LOCAL}) or "remote" 1403 * ({@link RouteInfo#PLAYBACK_TYPE_REMOTE}). 1404 * @param type 1405 */ 1406 public void setPlaybackType(int type) { 1407 if (mPlaybackType != type) { 1408 mPlaybackType = type; 1409 setPlaybackInfoOnRcc(RemoteControlClient.PLAYBACKINFO_PLAYBACK_TYPE, type); 1410 } 1411 } 1412 1413 /** 1414 * Defines whether volume for the playback associated with this route is fixed 1415 * ({@link RouteInfo#PLAYBACK_VOLUME_FIXED}) or can modified 1416 * ({@link RouteInfo#PLAYBACK_VOLUME_VARIABLE}). 1417 * @param volumeHandling 1418 */ 1419 public void setVolumeHandling(int volumeHandling) { 1420 if (mVolumeHandling != volumeHandling) { 1421 mVolumeHandling = volumeHandling; 1422 setPlaybackInfoOnRcc( 1423 RemoteControlClient.PLAYBACKINFO_VOLUME_HANDLING, volumeHandling); 1424 } 1425 } 1426 1427 /** 1428 * Defines at what volume the playback associated with this route is performed (for user 1429 * feedback purposes). This information is only used when the playback is not local. 1430 * @param volume 1431 */ 1432 public void setVolume(int volume) { 1433 volume = Math.max(0, Math.min(volume, getVolumeMax())); 1434 if (mVolume != volume) { 1435 mVolume = volume; 1436 setPlaybackInfoOnRcc(RemoteControlClient.PLAYBACKINFO_VOLUME, volume); 1437 dispatchRouteVolumeChanged(this); 1438 if (mGroup != null) { 1439 mGroup.memberVolumeChanged(this); 1440 } 1441 } 1442 } 1443 1444 @Override 1445 public void requestSetVolume(int volume) { 1446 if (mVolumeHandling == PLAYBACK_VOLUME_VARIABLE) { 1447 if (mVcb == null) { 1448 Log.e(TAG, "Cannot requestSetVolume on user route - no volume callback set"); 1449 return; 1450 } 1451 mVcb.vcb.onVolumeSetRequest(this, volume); 1452 } 1453 } 1454 1455 @Override 1456 public void requestUpdateVolume(int direction) { 1457 if (mVolumeHandling == PLAYBACK_VOLUME_VARIABLE) { 1458 if (mVcb == null) { 1459 Log.e(TAG, "Cannot requestChangeVolume on user route - no volumec callback set"); 1460 return; 1461 } 1462 mVcb.vcb.onVolumeUpdateRequest(this, direction); 1463 } 1464 } 1465 1466 /** 1467 * Defines the maximum volume at which the playback associated with this route is performed 1468 * (for user feedback purposes). This information is only used when the playback is not 1469 * local. 1470 * @param volumeMax 1471 */ 1472 public void setVolumeMax(int volumeMax) { 1473 if (mVolumeMax != volumeMax) { 1474 mVolumeMax = volumeMax; 1475 setPlaybackInfoOnRcc(RemoteControlClient.PLAYBACKINFO_VOLUME_MAX, volumeMax); 1476 } 1477 } 1478 1479 /** 1480 * Defines over what stream type the media is presented. 1481 * @param stream 1482 */ 1483 public void setPlaybackStream(int stream) { 1484 if (mPlaybackStream != stream) { 1485 mPlaybackStream = stream; 1486 setPlaybackInfoOnRcc(RemoteControlClient.PLAYBACKINFO_USES_STREAM, stream); 1487 } 1488 } 1489 1490 private void updatePlaybackInfoOnRcc() { 1491 if ((mRcc != null) && (mRcc.getRcseId() != RemoteControlClient.RCSE_ID_UNREGISTERED)) { 1492 mRcc.setPlaybackInformation( 1493 RemoteControlClient.PLAYBACKINFO_VOLUME_MAX, mVolumeMax); 1494 mRcc.setPlaybackInformation( 1495 RemoteControlClient.PLAYBACKINFO_VOLUME, mVolume); 1496 mRcc.setPlaybackInformation( 1497 RemoteControlClient.PLAYBACKINFO_VOLUME_HANDLING, mVolumeHandling); 1498 mRcc.setPlaybackInformation( 1499 RemoteControlClient.PLAYBACKINFO_USES_STREAM, mPlaybackStream); 1500 mRcc.setPlaybackInformation( 1501 RemoteControlClient.PLAYBACKINFO_PLAYBACK_TYPE, mPlaybackType); 1502 // let AudioService know whom to call when remote volume needs to be updated 1503 try { 1504 sStatic.mAudioService.registerRemoteVolumeObserverForRcc( 1505 mRcc.getRcseId() /* rccId */, mRemoteVolObserver /* rvo */); 1506 } catch (RemoteException e) { 1507 Log.e(TAG, "Error registering remote volume observer", e); 1508 } 1509 } 1510 } 1511 1512 private void setPlaybackInfoOnRcc(int what, int value) { 1513 if (mRcc != null) { 1514 mRcc.setPlaybackInformation(what, value); 1515 } 1516 } 1517 } 1518 1519 /** 1520 * Information about a route that consists of multiple other routes in a group. 1521 */ 1522 public static class RouteGroup extends RouteInfo { 1523 final ArrayList<RouteInfo> mRoutes = new ArrayList<RouteInfo>(); 1524 private boolean mUpdateName; 1525 1526 RouteGroup(RouteCategory category) { 1527 super(category); 1528 mGroup = this; 1529 mVolumeHandling = PLAYBACK_VOLUME_FIXED; 1530 } 1531 1532 CharSequence getName(Resources res) { 1533 if (mUpdateName) updateName(); 1534 return super.getName(res); 1535 } 1536 1537 /** 1538 * Add a route to this group. The route must not currently belong to another group. 1539 * 1540 * @param route route to add to this group 1541 */ 1542 public void addRoute(RouteInfo route) { 1543 if (route.getGroup() != null) { 1544 throw new IllegalStateException("Route " + route + " is already part of a group."); 1545 } 1546 if (route.getCategory() != mCategory) { 1547 throw new IllegalArgumentException( 1548 "Route cannot be added to a group with a different category. " + 1549 "(Route category=" + route.getCategory() + 1550 " group category=" + mCategory + ")"); 1551 } 1552 final int at = mRoutes.size(); 1553 mRoutes.add(route); 1554 route.mGroup = this; 1555 mUpdateName = true; 1556 updateVolume(); 1557 routeUpdated(); 1558 dispatchRouteGrouped(route, this, at); 1559 } 1560 1561 /** 1562 * Add a route to this group before the specified index. 1563 * 1564 * @param route route to add 1565 * @param insertAt insert the new route before this index 1566 */ 1567 public void addRoute(RouteInfo route, int insertAt) { 1568 if (route.getGroup() != null) { 1569 throw new IllegalStateException("Route " + route + " is already part of a group."); 1570 } 1571 if (route.getCategory() != mCategory) { 1572 throw new IllegalArgumentException( 1573 "Route cannot be added to a group with a different category. " + 1574 "(Route category=" + route.getCategory() + 1575 " group category=" + mCategory + ")"); 1576 } 1577 mRoutes.add(insertAt, route); 1578 route.mGroup = this; 1579 mUpdateName = true; 1580 updateVolume(); 1581 routeUpdated(); 1582 dispatchRouteGrouped(route, this, insertAt); 1583 } 1584 1585 /** 1586 * Remove a route from this group. 1587 * 1588 * @param route route to remove 1589 */ 1590 public void removeRoute(RouteInfo route) { 1591 if (route.getGroup() != this) { 1592 throw new IllegalArgumentException("Route " + route + 1593 " is not a member of this group."); 1594 } 1595 mRoutes.remove(route); 1596 route.mGroup = null; 1597 mUpdateName = true; 1598 updateVolume(); 1599 dispatchRouteUngrouped(route, this); 1600 routeUpdated(); 1601 } 1602 1603 /** 1604 * Remove the route at the specified index from this group. 1605 * 1606 * @param index index of the route to remove 1607 */ 1608 public void removeRoute(int index) { 1609 RouteInfo route = mRoutes.remove(index); 1610 route.mGroup = null; 1611 mUpdateName = true; 1612 updateVolume(); 1613 dispatchRouteUngrouped(route, this); 1614 routeUpdated(); 1615 } 1616 1617 /** 1618 * @return The number of routes in this group 1619 */ 1620 public int getRouteCount() { 1621 return mRoutes.size(); 1622 } 1623 1624 /** 1625 * Return the route in this group at the specified index 1626 * 1627 * @param index Index to fetch 1628 * @return The route at index 1629 */ 1630 public RouteInfo getRouteAt(int index) { 1631 return mRoutes.get(index); 1632 } 1633 1634 /** 1635 * Set an icon that will be used to represent this group. 1636 * The system may use this icon in picker UIs or similar. 1637 * 1638 * @param icon icon drawable to use to represent this group 1639 */ 1640 public void setIconDrawable(Drawable icon) { 1641 mIcon = icon; 1642 } 1643 1644 /** 1645 * Set an icon that will be used to represent this group. 1646 * The system may use this icon in picker UIs or similar. 1647 * 1648 * @param resId Resource ID of an icon drawable to use to represent this group 1649 */ 1650 public void setIconResource(int resId) { 1651 setIconDrawable(sStatic.mResources.getDrawable(resId)); 1652 } 1653 1654 @Override 1655 public void requestSetVolume(int volume) { 1656 final int maxVol = getVolumeMax(); 1657 if (maxVol == 0) { 1658 return; 1659 } 1660 1661 final float scaledVolume = (float) volume / maxVol; 1662 final int routeCount = getRouteCount(); 1663 for (int i = 0; i < routeCount; i++) { 1664 final RouteInfo route = getRouteAt(i); 1665 final int routeVol = (int) (scaledVolume * route.getVolumeMax()); 1666 route.requestSetVolume(routeVol); 1667 } 1668 if (volume != mVolume) { 1669 mVolume = volume; 1670 dispatchRouteVolumeChanged(this); 1671 } 1672 } 1673 1674 @Override 1675 public void requestUpdateVolume(int direction) { 1676 final int maxVol = getVolumeMax(); 1677 if (maxVol == 0) { 1678 return; 1679 } 1680 1681 final int routeCount = getRouteCount(); 1682 int volume = 0; 1683 for (int i = 0; i < routeCount; i++) { 1684 final RouteInfo route = getRouteAt(i); 1685 route.requestUpdateVolume(direction); 1686 final int routeVol = route.getVolume(); 1687 if (routeVol > volume) { 1688 volume = routeVol; 1689 } 1690 } 1691 if (volume != mVolume) { 1692 mVolume = volume; 1693 dispatchRouteVolumeChanged(this); 1694 } 1695 } 1696 1697 void memberNameChanged(RouteInfo info, CharSequence name) { 1698 mUpdateName = true; 1699 routeUpdated(); 1700 } 1701 1702 void memberStatusChanged(RouteInfo info, CharSequence status) { 1703 setStatusInt(status); 1704 } 1705 1706 void memberVolumeChanged(RouteInfo info) { 1707 updateVolume(); 1708 } 1709 1710 void updateVolume() { 1711 // A group always represents the highest component volume value. 1712 final int routeCount = getRouteCount(); 1713 int volume = 0; 1714 for (int i = 0; i < routeCount; i++) { 1715 final int routeVol = getRouteAt(i).getVolume(); 1716 if (routeVol > volume) { 1717 volume = routeVol; 1718 } 1719 } 1720 if (volume != mVolume) { 1721 mVolume = volume; 1722 dispatchRouteVolumeChanged(this); 1723 } 1724 } 1725 1726 @Override 1727 void routeUpdated() { 1728 int types = 0; 1729 final int count = mRoutes.size(); 1730 if (count == 0) { 1731 // Don't keep empty groups in the router. 1732 MediaRouter.removeRoute(this); 1733 return; 1734 } 1735 1736 int maxVolume = 0; 1737 boolean isLocal = true; 1738 boolean isFixedVolume = true; 1739 for (int i = 0; i < count; i++) { 1740 final RouteInfo route = mRoutes.get(i); 1741 types |= route.mSupportedTypes; 1742 final int routeMaxVolume = route.getVolumeMax(); 1743 if (routeMaxVolume > maxVolume) { 1744 maxVolume = routeMaxVolume; 1745 } 1746 isLocal &= route.getPlaybackType() == PLAYBACK_TYPE_LOCAL; 1747 isFixedVolume &= route.getVolumeHandling() == PLAYBACK_VOLUME_FIXED; 1748 } 1749 mPlaybackType = isLocal ? PLAYBACK_TYPE_LOCAL : PLAYBACK_TYPE_REMOTE; 1750 mVolumeHandling = isFixedVolume ? PLAYBACK_VOLUME_FIXED : PLAYBACK_VOLUME_VARIABLE; 1751 mSupportedTypes = types; 1752 mVolumeMax = maxVolume; 1753 mIcon = count == 1 ? mRoutes.get(0).getIconDrawable() : null; 1754 super.routeUpdated(); 1755 } 1756 1757 void updateName() { 1758 final StringBuilder sb = new StringBuilder(); 1759 final int count = mRoutes.size(); 1760 for (int i = 0; i < count; i++) { 1761 final RouteInfo info = mRoutes.get(i); 1762 // TODO: There's probably a much more correct way to localize this. 1763 if (i > 0) sb.append(", "); 1764 sb.append(info.mName); 1765 } 1766 mName = sb.toString(); 1767 mUpdateName = false; 1768 } 1769 1770 @Override 1771 public String toString() { 1772 StringBuilder sb = new StringBuilder(super.toString()); 1773 sb.append('['); 1774 final int count = mRoutes.size(); 1775 for (int i = 0; i < count; i++) { 1776 if (i > 0) sb.append(", "); 1777 sb.append(mRoutes.get(i)); 1778 } 1779 sb.append(']'); 1780 return sb.toString(); 1781 } 1782 } 1783 1784 /** 1785 * Definition of a category of routes. All routes belong to a category. 1786 */ 1787 public static class RouteCategory { 1788 CharSequence mName; 1789 int mNameResId; 1790 int mTypes; 1791 final boolean mGroupable; 1792 boolean mIsSystem; 1793 1794 RouteCategory(CharSequence name, int types, boolean groupable) { 1795 mName = name; 1796 mTypes = types; 1797 mGroupable = groupable; 1798 } 1799 1800 RouteCategory(int nameResId, int types, boolean groupable) { 1801 mNameResId = nameResId; 1802 mTypes = types; 1803 mGroupable = groupable; 1804 } 1805 1806 /** 1807 * @return the name of this route category 1808 */ 1809 public CharSequence getName() { 1810 return getName(sStatic.mResources); 1811 } 1812 1813 /** 1814 * Return the properly localized/configuration dependent name of this RouteCategory. 1815 * 1816 * @param context Context to resolve name resources 1817 * @return the name of this route category 1818 */ 1819 public CharSequence getName(Context context) { 1820 return getName(context.getResources()); 1821 } 1822 1823 CharSequence getName(Resources res) { 1824 if (mNameResId != 0) { 1825 return res.getText(mNameResId); 1826 } 1827 return mName; 1828 } 1829 1830 /** 1831 * Return the current list of routes in this category that have been added 1832 * to the MediaRouter. 1833 * 1834 * <p>This list will not include routes that are nested within RouteGroups. 1835 * A RouteGroup is treated as a single route within its category.</p> 1836 * 1837 * @param out a List to fill with the routes in this category. If this parameter is 1838 * non-null, it will be cleared, filled with the current routes with this 1839 * category, and returned. If this parameter is null, a new List will be 1840 * allocated to report the category's current routes. 1841 * @return A list with the routes in this category that have been added to the MediaRouter. 1842 */ 1843 public List<RouteInfo> getRoutes(List<RouteInfo> out) { 1844 if (out == null) { 1845 out = new ArrayList<RouteInfo>(); 1846 } else { 1847 out.clear(); 1848 } 1849 1850 final int count = getRouteCountStatic(); 1851 for (int i = 0; i < count; i++) { 1852 final RouteInfo route = getRouteAtStatic(i); 1853 if (route.mCategory == this) { 1854 out.add(route); 1855 } 1856 } 1857 return out; 1858 } 1859 1860 /** 1861 * @return Flag set describing the route types supported by this category 1862 */ 1863 public int getSupportedTypes() { 1864 return mTypes; 1865 } 1866 1867 /** 1868 * Return whether or not this category supports grouping. 1869 * 1870 * <p>If this method returns true, all routes obtained from this category 1871 * via calls to {@link #getRouteAt(int)} will be {@link MediaRouter.RouteGroup}s.</p> 1872 * 1873 * @return true if this category supports 1874 */ 1875 public boolean isGroupable() { 1876 return mGroupable; 1877 } 1878 1879 /** 1880 * @return true if this is the category reserved for system routes. 1881 * @hide 1882 */ 1883 public boolean isSystem() { 1884 return mIsSystem; 1885 } 1886 1887 public String toString() { 1888 return "RouteCategory{ name=" + mName + " types=" + typesToString(mTypes) + 1889 " groupable=" + mGroupable + " }"; 1890 } 1891 } 1892 1893 static class CallbackInfo { 1894 public int type; 1895 public final Callback cb; 1896 public final MediaRouter router; 1897 1898 public CallbackInfo(Callback cb, int type, MediaRouter router) { 1899 this.cb = cb; 1900 this.type = type; 1901 this.router = router; 1902 } 1903 } 1904 1905 /** 1906 * Interface for receiving events about media routing changes. 1907 * All methods of this interface will be called from the application's main thread. 1908 * 1909 * <p>A Callback will only receive events relevant to routes that the callback 1910 * was registered for.</p> 1911 * 1912 * @see MediaRouter#addCallback(int, Callback) 1913 * @see MediaRouter#removeCallback(Callback) 1914 */ 1915 public static abstract class Callback { 1916 /** 1917 * Called when the supplied route becomes selected as the active route 1918 * for the given route type. 1919 * 1920 * @param router the MediaRouter reporting the event 1921 * @param type Type flag set indicating the routes that have been selected 1922 * @param info Route that has been selected for the given route types 1923 */ 1924 public abstract void onRouteSelected(MediaRouter router, int type, RouteInfo info); 1925 1926 /** 1927 * Called when the supplied route becomes unselected as the active route 1928 * for the given route type. 1929 * 1930 * @param router the MediaRouter reporting the event 1931 * @param type Type flag set indicating the routes that have been unselected 1932 * @param info Route that has been unselected for the given route types 1933 */ 1934 public abstract void onRouteUnselected(MediaRouter router, int type, RouteInfo info); 1935 1936 /** 1937 * Called when a route for the specified type was added. 1938 * 1939 * @param router the MediaRouter reporting the event 1940 * @param info Route that has become available for use 1941 */ 1942 public abstract void onRouteAdded(MediaRouter router, RouteInfo info); 1943 1944 /** 1945 * Called when a route for the specified type was removed. 1946 * 1947 * @param router the MediaRouter reporting the event 1948 * @param info Route that has been removed from availability 1949 */ 1950 public abstract void onRouteRemoved(MediaRouter router, RouteInfo info); 1951 1952 /** 1953 * Called when an aspect of the indicated route has changed. 1954 * 1955 * <p>This will not indicate that the types supported by this route have 1956 * changed, only that cosmetic info such as name or status have been updated.</p> 1957 * 1958 * @param router the MediaRouter reporting the event 1959 * @param info The route that was changed 1960 */ 1961 public abstract void onRouteChanged(MediaRouter router, RouteInfo info); 1962 1963 /** 1964 * Called when a route is added to a group. 1965 * 1966 * @param router the MediaRouter reporting the event 1967 * @param info The route that was added 1968 * @param group The group the route was added to 1969 * @param index The route index within group that info was added at 1970 */ 1971 public abstract void onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group, 1972 int index); 1973 1974 /** 1975 * Called when a route is removed from a group. 1976 * 1977 * @param router the MediaRouter reporting the event 1978 * @param info The route that was removed 1979 * @param group The group the route was removed from 1980 */ 1981 public abstract void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group); 1982 1983 /** 1984 * Called when a route's volume changes. 1985 * 1986 * @param router the MediaRouter reporting the event 1987 * @param info The route with altered volume 1988 */ 1989 public abstract void onRouteVolumeChanged(MediaRouter router, RouteInfo info); 1990 1991 /** 1992 * Called when a route's presentation display changes. 1993 * <p> 1994 * This method is called whenever the route's presentation display becomes 1995 * available, is removes or has changes to some of its properties (such as its size). 1996 * </p> 1997 * 1998 * @param router the MediaRouter reporting the event 1999 * @param info The route whose presentation display changed 2000 * 2001 * @see RouteInfo#getPresentationDisplay() 2002 */ 2003 public void onRoutePresentationDisplayChanged(MediaRouter router, RouteInfo info) { 2004 } 2005 } 2006 2007 /** 2008 * Stub implementation of {@link MediaRouter.Callback}. 2009 * Each abstract method is defined as a no-op. Override just the ones 2010 * you need. 2011 */ 2012 public static class SimpleCallback extends Callback { 2013 2014 @Override 2015 public void onRouteSelected(MediaRouter router, int type, RouteInfo info) { 2016 } 2017 2018 @Override 2019 public void onRouteUnselected(MediaRouter router, int type, RouteInfo info) { 2020 } 2021 2022 @Override 2023 public void onRouteAdded(MediaRouter router, RouteInfo info) { 2024 } 2025 2026 @Override 2027 public void onRouteRemoved(MediaRouter router, RouteInfo info) { 2028 } 2029 2030 @Override 2031 public void onRouteChanged(MediaRouter router, RouteInfo info) { 2032 } 2033 2034 @Override 2035 public void onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group, 2036 int index) { 2037 } 2038 2039 @Override 2040 public void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group) { 2041 } 2042 2043 @Override 2044 public void onRouteVolumeChanged(MediaRouter router, RouteInfo info) { 2045 } 2046 } 2047 2048 static class VolumeCallbackInfo { 2049 public final VolumeCallback vcb; 2050 public final RouteInfo route; 2051 2052 public VolumeCallbackInfo(VolumeCallback vcb, RouteInfo route) { 2053 this.vcb = vcb; 2054 this.route = route; 2055 } 2056 } 2057 2058 /** 2059 * Interface for receiving events about volume changes. 2060 * All methods of this interface will be called from the application's main thread. 2061 * 2062 * <p>A VolumeCallback will only receive events relevant to routes that the callback 2063 * was registered for.</p> 2064 * 2065 * @see UserRouteInfo#setVolumeCallback(VolumeCallback) 2066 */ 2067 public static abstract class VolumeCallback { 2068 /** 2069 * Called when the volume for the route should be increased or decreased. 2070 * @param info the route affected by this event 2071 * @param direction an integer indicating whether the volume is to be increased 2072 * (positive value) or decreased (negative value). 2073 * For bundled changes, the absolute value indicates the number of changes 2074 * in the same direction, e.g. +3 corresponds to three "volume up" changes. 2075 */ 2076 public abstract void onVolumeUpdateRequest(RouteInfo info, int direction); 2077 /** 2078 * Called when the volume for the route should be set to the given value 2079 * @param info the route affected by this event 2080 * @param volume an integer indicating the new volume value that should be used, always 2081 * between 0 and the value set by {@link UserRouteInfo#setVolumeMax(int)}. 2082 */ 2083 public abstract void onVolumeSetRequest(RouteInfo info, int volume); 2084 } 2085 2086 static class VolumeChangeReceiver extends BroadcastReceiver { 2087 @Override 2088 public void onReceive(Context context, Intent intent) { 2089 if (intent.getAction().equals(AudioManager.VOLUME_CHANGED_ACTION)) { 2090 final int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, 2091 -1); 2092 if (streamType != AudioManager.STREAM_MUSIC) { 2093 return; 2094 } 2095 2096 final int newVolume = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, 0); 2097 final int oldVolume = intent.getIntExtra( 2098 AudioManager.EXTRA_PREV_VOLUME_STREAM_VALUE, 0); 2099 if (newVolume != oldVolume) { 2100 systemVolumeChanged(newVolume); 2101 } 2102 } 2103 } 2104 } 2105 2106 static class WifiDisplayStatusChangedReceiver extends BroadcastReceiver { 2107 @Override 2108 public void onReceive(Context context, Intent intent) { 2109 if (intent.getAction().equals(DisplayManager.ACTION_WIFI_DISPLAY_STATUS_CHANGED)) { 2110 updateWifiDisplayStatus((WifiDisplayStatus) intent.getParcelableExtra( 2111 DisplayManager.EXTRA_WIFI_DISPLAY_STATUS)); 2112 } 2113 } 2114 } 2115} 2116