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