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