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