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