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