MediaRouter.java revision b072a9686c29bfbc05b732076a4e89bcca8db08a
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 wantScan = false; 697 boolean blockScan = false; 698 WifiDisplay[] oldDisplays = oldStatus != null ? 699 oldStatus.getRememberedDisplays() : new WifiDisplay[0]; 700 WifiDisplay[] newDisplays = newStatus.getRememberedDisplays(); 701 WifiDisplay[] availableDisplays = newStatus.getAvailableDisplays(); 702 WifiDisplay activeDisplay = newStatus.getActiveDisplay(); 703 704 for (int i = 0; i < newDisplays.length; i++) { 705 final WifiDisplay d = newDisplays[i]; 706 final WifiDisplay oldRemembered = findMatchingDisplay(d, oldDisplays); 707 if (oldRemembered == null) { 708 addRouteStatic(makeWifiDisplayRoute(d, 709 findMatchingDisplay(d, availableDisplays) != null)); 710 wantScan = true; 711 } else { 712 final boolean available = findMatchingDisplay(d, availableDisplays) != null; 713 final RouteInfo route = findWifiDisplayRoute(d); 714 updateWifiDisplayRoute(route, d, available, newStatus); 715 } 716 if (d.equals(activeDisplay)) { 717 final RouteInfo activeRoute = findWifiDisplayRoute(d); 718 if (activeRoute != null) { 719 selectRouteStatic(activeRoute.getSupportedTypes(), activeRoute); 720 721 // Don't scan if we're already connected to a wifi display, 722 // the scanning process can cause a hiccup with some configurations. 723 blockScan = true; 724 } 725 } 726 } 727 for (int i = 0; i < oldDisplays.length; i++) { 728 final WifiDisplay d = oldDisplays[i]; 729 final WifiDisplay newDisplay = findMatchingDisplay(d, newDisplays); 730 if (newDisplay == null) { 731 removeRoute(findWifiDisplayRoute(d)); 732 } 733 } 734 735 if (wantScan && !blockScan) { 736 sStatic.mDisplayService.scanWifiDisplays(); 737 } 738 739 sStatic.mLastKnownWifiDisplayStatus = newStatus; 740 } 741 742 static RouteInfo makeWifiDisplayRoute(WifiDisplay display, boolean available) { 743 final RouteInfo newRoute = new RouteInfo(sStatic.mSystemCategory); 744 newRoute.mDeviceAddress = display.getDeviceAddress(); 745 newRoute.mSupportedTypes = ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_LIVE_VIDEO; 746 newRoute.mVolumeHandling = RouteInfo.PLAYBACK_VOLUME_FIXED; 747 newRoute.mPlaybackType = RouteInfo.PLAYBACK_TYPE_REMOTE; 748 749 newRoute.setStatusCode(available ? 750 RouteInfo.STATUS_AVAILABLE : RouteInfo.STATUS_CONNECTING); 751 newRoute.mEnabled = available; 752 753 newRoute.mName = makeWifiDisplayName(display); 754 return newRoute; 755 } 756 757 static String makeWifiDisplayName(WifiDisplay display) { 758 String name = display.getDeviceAlias(); 759 if (TextUtils.isEmpty(name)) { 760 name = display.getDeviceName(); 761 } 762 return name; 763 } 764 765 private static void updateWifiDisplayRoute(RouteInfo route, WifiDisplay display, 766 boolean available, WifiDisplayStatus wifiDisplayStatus) { 767 final boolean isScanning = 768 wifiDisplayStatus.getScanState() == WifiDisplayStatus.SCAN_STATE_SCANNING; 769 770 boolean changed = false; 771 int newStatus = RouteInfo.STATUS_NONE; 772 773 if (available) { 774 newStatus = isScanning ? RouteInfo.STATUS_SCANNING : RouteInfo.STATUS_AVAILABLE; 775 } else { 776 newStatus = RouteInfo.STATUS_NOT_AVAILABLE; 777 } 778 779 if (display.equals(wifiDisplayStatus.getActiveDisplay())) { 780 final int activeState = wifiDisplayStatus.getActiveDisplayState(); 781 switch (activeState) { 782 case WifiDisplayStatus.DISPLAY_STATE_CONNECTED: 783 newStatus = RouteInfo.STATUS_NONE; 784 break; 785 case WifiDisplayStatus.DISPLAY_STATE_CONNECTING: 786 newStatus = RouteInfo.STATUS_CONNECTING; 787 break; 788 case WifiDisplayStatus.DISPLAY_STATE_NOT_CONNECTED: 789 Log.e(TAG, "Active display is not connected!"); 790 break; 791 } 792 } 793 794 final String newName = makeWifiDisplayName(display); 795 if (route.getName().equals(newName)) { 796 route.mName = newName; 797 changed = true; 798 } 799 800 changed |= route.mEnabled != available; 801 route.mEnabled = available; 802 803 changed |= route.setStatusCode(newStatus); 804 805 if (changed) { 806 dispatchRouteChanged(route); 807 } 808 809 if (!available && route == sStatic.mSelectedRoute) { 810 // Oops, no longer available. Reselect the default. 811 final RouteInfo defaultRoute = sStatic.mDefaultAudioVideo; 812 selectRouteStatic(defaultRoute.getSupportedTypes(), defaultRoute); 813 } 814 } 815 816 private static WifiDisplay findMatchingDisplay(WifiDisplay address, WifiDisplay[] displays) { 817 for (int i = 0; i < displays.length; i++) { 818 final WifiDisplay d = displays[i]; 819 if (d.equals(address)) { 820 return d; 821 } 822 } 823 return null; 824 } 825 826 private static RouteInfo findWifiDisplayRoute(WifiDisplay d) { 827 final int count = sStatic.mRoutes.size(); 828 for (int i = 0; i < count; i++) { 829 final RouteInfo info = sStatic.mRoutes.get(i); 830 if (d.getDeviceAddress().equals(info.mDeviceAddress)) { 831 return info; 832 } 833 } 834 return null; 835 } 836 837 /** 838 * Information about a media route. 839 */ 840 public static class RouteInfo { 841 CharSequence mName; 842 int mNameResId; 843 private CharSequence mStatus; 844 int mSupportedTypes; 845 RouteGroup mGroup; 846 final RouteCategory mCategory; 847 Drawable mIcon; 848 // playback information 849 int mPlaybackType = PLAYBACK_TYPE_LOCAL; 850 int mVolumeMax = RemoteControlClient.DEFAULT_PLAYBACK_VOLUME; 851 int mVolume = RemoteControlClient.DEFAULT_PLAYBACK_VOLUME; 852 int mVolumeHandling = RemoteControlClient.DEFAULT_PLAYBACK_VOLUME_HANDLING; 853 int mPlaybackStream = AudioManager.STREAM_MUSIC; 854 VolumeCallbackInfo mVcb; 855 856 String mDeviceAddress; 857 boolean mEnabled = true; 858 859 // A predetermined connection status that can override mStatus 860 private int mStatusCode; 861 862 /** @hide */ public static final int STATUS_NONE = 0; 863 /** @hide */ public static final int STATUS_SCANNING = 1; 864 /** @hide */ public static final int STATUS_CONNECTING = 2; 865 /** @hide */ public static final int STATUS_AVAILABLE = 3; 866 /** @hide */ public static final int STATUS_NOT_AVAILABLE = 4; 867 868 private Object mTag; 869 870 /** 871 * The default playback type, "local", indicating the presentation of the media is happening 872 * on the same device (e.g. a phone, a tablet) as where it is controlled from. 873 * @see #setPlaybackType(int) 874 */ 875 public final static int PLAYBACK_TYPE_LOCAL = 0; 876 /** 877 * A playback type indicating the presentation of the media is happening on 878 * a different device (i.e. the remote device) than where it is controlled from. 879 * @see #setPlaybackType(int) 880 */ 881 public final static int PLAYBACK_TYPE_REMOTE = 1; 882 /** 883 * Playback information indicating the playback volume is fixed, i.e. it cannot be 884 * controlled from this object. An example of fixed playback volume is a remote player, 885 * playing over HDMI where the user prefers to control the volume on the HDMI sink, rather 886 * than attenuate at the source. 887 * @see #setVolumeHandling(int) 888 */ 889 public final static int PLAYBACK_VOLUME_FIXED = 0; 890 /** 891 * Playback information indicating the playback volume is variable and can be controlled 892 * from this object. 893 */ 894 public final static int PLAYBACK_VOLUME_VARIABLE = 1; 895 896 RouteInfo(RouteCategory category) { 897 mCategory = category; 898 } 899 900 /** 901 * @return The user-friendly name of a media route. This is the string presented 902 * to users who may select this as the active route. 903 */ 904 public CharSequence getName() { 905 return getName(sStatic.mResources); 906 } 907 908 /** 909 * Return the properly localized/resource selected name of this route. 910 * 911 * @param context Context used to resolve the correct configuration to load 912 * @return The user-friendly name of the media route. This is the string presented 913 * to users who may select this as the active route. 914 */ 915 public CharSequence getName(Context context) { 916 return getName(context.getResources()); 917 } 918 919 CharSequence getName(Resources res) { 920 if (mNameResId != 0) { 921 return mName = res.getText(mNameResId); 922 } 923 return mName; 924 } 925 926 /** 927 * @return The user-friendly status for a media route. This may include a description 928 * of the currently playing media, if available. 929 */ 930 public CharSequence getStatus() { 931 return mStatus; 932 } 933 934 /** 935 * Set this route's status by predetermined status code. If the caller 936 * should dispatch a route changed event this call will return true; 937 */ 938 boolean setStatusCode(int statusCode) { 939 if (statusCode != mStatusCode) { 940 mStatusCode = statusCode; 941 int resId = 0; 942 switch (statusCode) { 943 case STATUS_SCANNING: 944 resId = com.android.internal.R.string.media_route_status_scanning; 945 break; 946 case STATUS_CONNECTING: 947 resId = com.android.internal.R.string.media_route_status_connecting; 948 break; 949 case STATUS_AVAILABLE: 950 resId = com.android.internal.R.string.media_route_status_available; 951 break; 952 case STATUS_NOT_AVAILABLE: 953 resId = com.android.internal.R.string.media_route_status_not_available; 954 break; 955 } 956 mStatus = resId != 0 ? sStatic.mResources.getText(resId) : null; 957 return true; 958 } 959 return false; 960 } 961 962 /** 963 * @hide 964 */ 965 public int getStatusCode() { 966 return mStatusCode; 967 } 968 969 /** 970 * @return A media type flag set describing which types this route supports. 971 */ 972 public int getSupportedTypes() { 973 return mSupportedTypes; 974 } 975 976 /** 977 * @return The group that this route belongs to. 978 */ 979 public RouteGroup getGroup() { 980 return mGroup; 981 } 982 983 /** 984 * @return the category this route belongs to. 985 */ 986 public RouteCategory getCategory() { 987 return mCategory; 988 } 989 990 /** 991 * Get the icon representing this route. 992 * This icon will be used in picker UIs if available. 993 * 994 * @return the icon representing this route or null if no icon is available 995 */ 996 public Drawable getIconDrawable() { 997 return mIcon; 998 } 999 1000 /** 1001 * Set an application-specific tag object for this route. 1002 * The application may use this to store arbitrary data associated with the 1003 * route for internal tracking. 1004 * 1005 * <p>Note that the lifespan of a route may be well past the lifespan of 1006 * an Activity or other Context; take care that objects you store here 1007 * will not keep more data in memory alive than you intend.</p> 1008 * 1009 * @param tag Arbitrary, app-specific data for this route to hold for later use 1010 */ 1011 public void setTag(Object tag) { 1012 mTag = tag; 1013 routeUpdated(); 1014 } 1015 1016 /** 1017 * @return The tag object previously set by the application 1018 * @see #setTag(Object) 1019 */ 1020 public Object getTag() { 1021 return mTag; 1022 } 1023 1024 /** 1025 * @return the type of playback associated with this route 1026 * @see UserRouteInfo#setPlaybackType(int) 1027 */ 1028 public int getPlaybackType() { 1029 return mPlaybackType; 1030 } 1031 1032 /** 1033 * @return the stream over which the playback associated with this route is performed 1034 * @see UserRouteInfo#setPlaybackStream(int) 1035 */ 1036 public int getPlaybackStream() { 1037 return mPlaybackStream; 1038 } 1039 1040 /** 1041 * Return the current volume for this route. Depending on the route, this may only 1042 * be valid if the route is currently selected. 1043 * 1044 * @return the volume at which the playback associated with this route is performed 1045 * @see UserRouteInfo#setVolume(int) 1046 */ 1047 public int getVolume() { 1048 if (mPlaybackType == PLAYBACK_TYPE_LOCAL) { 1049 int vol = 0; 1050 try { 1051 vol = sStatic.mAudioService.getStreamVolume(mPlaybackStream); 1052 } catch (RemoteException e) { 1053 Log.e(TAG, "Error getting local stream volume", e); 1054 } 1055 return vol; 1056 } else { 1057 return mVolume; 1058 } 1059 } 1060 1061 /** 1062 * Request a volume change for this route. 1063 * @param volume value between 0 and getVolumeMax 1064 */ 1065 public void requestSetVolume(int volume) { 1066 if (mPlaybackType == PLAYBACK_TYPE_LOCAL) { 1067 try { 1068 sStatic.mAudioService.setStreamVolume(mPlaybackStream, volume, 0); 1069 } catch (RemoteException e) { 1070 Log.e(TAG, "Error setting local stream volume", e); 1071 } 1072 } else { 1073 Log.e(TAG, getClass().getSimpleName() + ".requestSetVolume(): " + 1074 "Non-local volume playback on system route? " + 1075 "Could not request volume change."); 1076 } 1077 } 1078 1079 /** 1080 * Request an incremental volume update for this route. 1081 * @param direction Delta to apply to the current volume 1082 */ 1083 public void requestUpdateVolume(int direction) { 1084 if (mPlaybackType == PLAYBACK_TYPE_LOCAL) { 1085 try { 1086 final int volume = 1087 Math.max(0, Math.min(getVolume() + direction, getVolumeMax())); 1088 sStatic.mAudioService.setStreamVolume(mPlaybackStream, volume, 0); 1089 } catch (RemoteException e) { 1090 Log.e(TAG, "Error setting local stream volume", e); 1091 } 1092 } else { 1093 Log.e(TAG, getClass().getSimpleName() + ".requestChangeVolume(): " + 1094 "Non-local volume playback on system route? " + 1095 "Could not request volume change."); 1096 } 1097 } 1098 1099 /** 1100 * @return the maximum volume at which the playback associated with this route is performed 1101 * @see UserRouteInfo#setVolumeMax(int) 1102 */ 1103 public int getVolumeMax() { 1104 if (mPlaybackType == PLAYBACK_TYPE_LOCAL) { 1105 int volMax = 0; 1106 try { 1107 volMax = sStatic.mAudioService.getStreamMaxVolume(mPlaybackStream); 1108 } catch (RemoteException e) { 1109 Log.e(TAG, "Error getting local stream volume", e); 1110 } 1111 return volMax; 1112 } else { 1113 return mVolumeMax; 1114 } 1115 } 1116 1117 /** 1118 * @return how volume is handling on the route 1119 * @see UserRouteInfo#setVolumeHandling(int) 1120 */ 1121 public int getVolumeHandling() { 1122 return mVolumeHandling; 1123 } 1124 1125 /** 1126 * @return true if this route is enabled and may be selected 1127 */ 1128 public boolean isEnabled() { 1129 return mEnabled; 1130 } 1131 1132 void setStatusInt(CharSequence status) { 1133 if (!status.equals(mStatus)) { 1134 mStatus = status; 1135 if (mGroup != null) { 1136 mGroup.memberStatusChanged(this, status); 1137 } 1138 routeUpdated(); 1139 } 1140 } 1141 1142 final IRemoteVolumeObserver.Stub mRemoteVolObserver = new IRemoteVolumeObserver.Stub() { 1143 public void dispatchRemoteVolumeUpdate(final int direction, final int value) { 1144 sStatic.mHandler.post(new Runnable() { 1145 @Override 1146 public void run() { 1147 if (mVcb != null) { 1148 if (direction != 0) { 1149 mVcb.vcb.onVolumeUpdateRequest(mVcb.route, direction); 1150 } else { 1151 mVcb.vcb.onVolumeSetRequest(mVcb.route, value); 1152 } 1153 } 1154 } 1155 }); 1156 } 1157 }; 1158 1159 void routeUpdated() { 1160 updateRoute(this); 1161 } 1162 1163 @Override 1164 public String toString() { 1165 String supportedTypes = typesToString(getSupportedTypes()); 1166 return getClass().getSimpleName() + "{ name=" + getName() + ", status=" + getStatus() + 1167 " category=" + getCategory() + 1168 " supportedTypes=" + supportedTypes + "}"; 1169 } 1170 } 1171 1172 /** 1173 * Information about a route that the application may define and modify. 1174 * A user route defaults to {@link RouteInfo#PLAYBACK_TYPE_REMOTE} and 1175 * {@link RouteInfo#PLAYBACK_VOLUME_FIXED}. 1176 * 1177 * @see MediaRouter.RouteInfo 1178 */ 1179 public static class UserRouteInfo extends RouteInfo { 1180 RemoteControlClient mRcc; 1181 1182 UserRouteInfo(RouteCategory category) { 1183 super(category); 1184 mSupportedTypes = ROUTE_TYPE_USER; 1185 mPlaybackType = PLAYBACK_TYPE_REMOTE; 1186 mVolumeHandling = PLAYBACK_VOLUME_FIXED; 1187 } 1188 1189 /** 1190 * Set the user-visible name of this route. 1191 * @param name Name to display to the user to describe this route 1192 */ 1193 public void setName(CharSequence name) { 1194 mName = name; 1195 routeUpdated(); 1196 } 1197 1198 /** 1199 * Set the user-visible name of this route. 1200 * @param resId Resource ID of the name to display to the user to describe this route 1201 */ 1202 public void setName(int resId) { 1203 mNameResId = resId; 1204 mName = null; 1205 routeUpdated(); 1206 } 1207 1208 /** 1209 * Set the current user-visible status for this route. 1210 * @param status Status to display to the user to describe what the endpoint 1211 * of this route is currently doing 1212 */ 1213 public void setStatus(CharSequence status) { 1214 setStatusInt(status); 1215 } 1216 1217 /** 1218 * Set the RemoteControlClient responsible for reporting playback info for this 1219 * user route. 1220 * 1221 * <p>If this route manages remote playback, the data exposed by this 1222 * RemoteControlClient will be used to reflect and update information 1223 * such as route volume info in related UIs.</p> 1224 * 1225 * <p>The RemoteControlClient must have been previously registered with 1226 * {@link AudioManager#registerRemoteControlClient(RemoteControlClient)}.</p> 1227 * 1228 * @param rcc RemoteControlClient associated with this route 1229 */ 1230 public void setRemoteControlClient(RemoteControlClient rcc) { 1231 mRcc = rcc; 1232 updatePlaybackInfoOnRcc(); 1233 } 1234 1235 /** 1236 * Retrieve the RemoteControlClient associated with this route, if one has been set. 1237 * 1238 * @return the RemoteControlClient associated with this route 1239 * @see #setRemoteControlClient(RemoteControlClient) 1240 */ 1241 public RemoteControlClient getRemoteControlClient() { 1242 return mRcc; 1243 } 1244 1245 /** 1246 * Set an icon that will be used to represent this route. 1247 * The system may use this icon in picker UIs or similar. 1248 * 1249 * @param icon icon drawable to use to represent this route 1250 */ 1251 public void setIconDrawable(Drawable icon) { 1252 mIcon = icon; 1253 } 1254 1255 /** 1256 * Set an icon that will be used to represent this route. 1257 * The system may use this icon in picker UIs or similar. 1258 * 1259 * @param resId Resource ID of an icon drawable to use to represent this route 1260 */ 1261 public void setIconResource(int resId) { 1262 setIconDrawable(sStatic.mResources.getDrawable(resId)); 1263 } 1264 1265 /** 1266 * Set a callback to be notified of volume update requests 1267 * @param vcb 1268 */ 1269 public void setVolumeCallback(VolumeCallback vcb) { 1270 mVcb = new VolumeCallbackInfo(vcb, this); 1271 } 1272 1273 /** 1274 * Defines whether playback associated with this route is "local" 1275 * ({@link RouteInfo#PLAYBACK_TYPE_LOCAL}) or "remote" 1276 * ({@link RouteInfo#PLAYBACK_TYPE_REMOTE}). 1277 * @param type 1278 */ 1279 public void setPlaybackType(int type) { 1280 if (mPlaybackType != type) { 1281 mPlaybackType = type; 1282 setPlaybackInfoOnRcc(RemoteControlClient.PLAYBACKINFO_PLAYBACK_TYPE, type); 1283 } 1284 } 1285 1286 /** 1287 * Defines whether volume for the playback associated with this route is fixed 1288 * ({@link RouteInfo#PLAYBACK_VOLUME_FIXED}) or can modified 1289 * ({@link RouteInfo#PLAYBACK_VOLUME_VARIABLE}). 1290 * @param volumeHandling 1291 */ 1292 public void setVolumeHandling(int volumeHandling) { 1293 if (mVolumeHandling != volumeHandling) { 1294 mVolumeHandling = volumeHandling; 1295 setPlaybackInfoOnRcc( 1296 RemoteControlClient.PLAYBACKINFO_VOLUME_HANDLING, volumeHandling); 1297 } 1298 } 1299 1300 /** 1301 * Defines at what volume the playback associated with this route is performed (for user 1302 * feedback purposes). This information is only used when the playback is not local. 1303 * @param volume 1304 */ 1305 public void setVolume(int volume) { 1306 volume = Math.max(0, Math.min(volume, getVolumeMax())); 1307 if (mVolume != volume) { 1308 mVolume = volume; 1309 setPlaybackInfoOnRcc(RemoteControlClient.PLAYBACKINFO_VOLUME, volume); 1310 dispatchRouteVolumeChanged(this); 1311 if (mGroup != null) { 1312 mGroup.memberVolumeChanged(this); 1313 } 1314 } 1315 } 1316 1317 @Override 1318 public void requestSetVolume(int volume) { 1319 if (mVolumeHandling == PLAYBACK_VOLUME_VARIABLE) { 1320 if (mVcb == null) { 1321 Log.e(TAG, "Cannot requestSetVolume on user route - no volume callback set"); 1322 return; 1323 } 1324 mVcb.vcb.onVolumeSetRequest(this, volume); 1325 } 1326 } 1327 1328 @Override 1329 public void requestUpdateVolume(int direction) { 1330 if (mVolumeHandling == PLAYBACK_VOLUME_VARIABLE) { 1331 if (mVcb == null) { 1332 Log.e(TAG, "Cannot requestChangeVolume on user route - no volumec callback set"); 1333 return; 1334 } 1335 mVcb.vcb.onVolumeUpdateRequest(this, direction); 1336 } 1337 } 1338 1339 /** 1340 * Defines the maximum volume at which the playback associated with this route is performed 1341 * (for user feedback purposes). This information is only used when the playback is not 1342 * local. 1343 * @param volumeMax 1344 */ 1345 public void setVolumeMax(int volumeMax) { 1346 if (mVolumeMax != volumeMax) { 1347 mVolumeMax = volumeMax; 1348 setPlaybackInfoOnRcc(RemoteControlClient.PLAYBACKINFO_VOLUME_MAX, volumeMax); 1349 } 1350 } 1351 1352 /** 1353 * Defines over what stream type the media is presented. 1354 * @param stream 1355 */ 1356 public void setPlaybackStream(int stream) { 1357 if (mPlaybackStream != stream) { 1358 mPlaybackStream = stream; 1359 setPlaybackInfoOnRcc(RemoteControlClient.PLAYBACKINFO_USES_STREAM, stream); 1360 } 1361 } 1362 1363 private void updatePlaybackInfoOnRcc() { 1364 if ((mRcc != null) && (mRcc.getRcseId() != RemoteControlClient.RCSE_ID_UNREGISTERED)) { 1365 mRcc.setPlaybackInformation( 1366 RemoteControlClient.PLAYBACKINFO_VOLUME_MAX, mVolumeMax); 1367 mRcc.setPlaybackInformation( 1368 RemoteControlClient.PLAYBACKINFO_VOLUME, mVolume); 1369 mRcc.setPlaybackInformation( 1370 RemoteControlClient.PLAYBACKINFO_VOLUME_HANDLING, mVolumeHandling); 1371 mRcc.setPlaybackInformation( 1372 RemoteControlClient.PLAYBACKINFO_USES_STREAM, mPlaybackStream); 1373 mRcc.setPlaybackInformation( 1374 RemoteControlClient.PLAYBACKINFO_PLAYBACK_TYPE, mPlaybackType); 1375 // let AudioService know whom to call when remote volume needs to be updated 1376 try { 1377 sStatic.mAudioService.registerRemoteVolumeObserverForRcc( 1378 mRcc.getRcseId() /* rccId */, mRemoteVolObserver /* rvo */); 1379 } catch (RemoteException e) { 1380 Log.e(TAG, "Error registering remote volume observer", e); 1381 } 1382 } 1383 } 1384 1385 private void setPlaybackInfoOnRcc(int what, int value) { 1386 if (mRcc != null) { 1387 mRcc.setPlaybackInformation(what, value); 1388 } 1389 } 1390 } 1391 1392 /** 1393 * Information about a route that consists of multiple other routes in a group. 1394 */ 1395 public static class RouteGroup extends RouteInfo { 1396 final ArrayList<RouteInfo> mRoutes = new ArrayList<RouteInfo>(); 1397 private boolean mUpdateName; 1398 1399 RouteGroup(RouteCategory category) { 1400 super(category); 1401 mGroup = this; 1402 mVolumeHandling = PLAYBACK_VOLUME_FIXED; 1403 } 1404 1405 CharSequence getName(Resources res) { 1406 if (mUpdateName) updateName(); 1407 return super.getName(res); 1408 } 1409 1410 /** 1411 * Add a route to this group. The route must not currently belong to another group. 1412 * 1413 * @param route route to add to this group 1414 */ 1415 public void addRoute(RouteInfo route) { 1416 if (route.getGroup() != null) { 1417 throw new IllegalStateException("Route " + route + " is already part of a group."); 1418 } 1419 if (route.getCategory() != mCategory) { 1420 throw new IllegalArgumentException( 1421 "Route cannot be added to a group with a different category. " + 1422 "(Route category=" + route.getCategory() + 1423 " group category=" + mCategory + ")"); 1424 } 1425 final int at = mRoutes.size(); 1426 mRoutes.add(route); 1427 route.mGroup = this; 1428 mUpdateName = true; 1429 updateVolume(); 1430 routeUpdated(); 1431 dispatchRouteGrouped(route, this, at); 1432 } 1433 1434 /** 1435 * Add a route to this group before the specified index. 1436 * 1437 * @param route route to add 1438 * @param insertAt insert the new route before this index 1439 */ 1440 public void addRoute(RouteInfo route, int insertAt) { 1441 if (route.getGroup() != null) { 1442 throw new IllegalStateException("Route " + route + " is already part of a group."); 1443 } 1444 if (route.getCategory() != mCategory) { 1445 throw new IllegalArgumentException( 1446 "Route cannot be added to a group with a different category. " + 1447 "(Route category=" + route.getCategory() + 1448 " group category=" + mCategory + ")"); 1449 } 1450 mRoutes.add(insertAt, route); 1451 route.mGroup = this; 1452 mUpdateName = true; 1453 updateVolume(); 1454 routeUpdated(); 1455 dispatchRouteGrouped(route, this, insertAt); 1456 } 1457 1458 /** 1459 * Remove a route from this group. 1460 * 1461 * @param route route to remove 1462 */ 1463 public void removeRoute(RouteInfo route) { 1464 if (route.getGroup() != this) { 1465 throw new IllegalArgumentException("Route " + route + 1466 " is not a member of this group."); 1467 } 1468 mRoutes.remove(route); 1469 route.mGroup = null; 1470 mUpdateName = true; 1471 updateVolume(); 1472 dispatchRouteUngrouped(route, this); 1473 routeUpdated(); 1474 } 1475 1476 /** 1477 * Remove the route at the specified index from this group. 1478 * 1479 * @param index index of the route to remove 1480 */ 1481 public void removeRoute(int index) { 1482 RouteInfo route = mRoutes.remove(index); 1483 route.mGroup = null; 1484 mUpdateName = true; 1485 updateVolume(); 1486 dispatchRouteUngrouped(route, this); 1487 routeUpdated(); 1488 } 1489 1490 /** 1491 * @return The number of routes in this group 1492 */ 1493 public int getRouteCount() { 1494 return mRoutes.size(); 1495 } 1496 1497 /** 1498 * Return the route in this group at the specified index 1499 * 1500 * @param index Index to fetch 1501 * @return The route at index 1502 */ 1503 public RouteInfo getRouteAt(int index) { 1504 return mRoutes.get(index); 1505 } 1506 1507 /** 1508 * Set an icon that will be used to represent this group. 1509 * The system may use this icon in picker UIs or similar. 1510 * 1511 * @param icon icon drawable to use to represent this group 1512 */ 1513 public void setIconDrawable(Drawable icon) { 1514 mIcon = icon; 1515 } 1516 1517 /** 1518 * Set an icon that will be used to represent this group. 1519 * The system may use this icon in picker UIs or similar. 1520 * 1521 * @param resId Resource ID of an icon drawable to use to represent this group 1522 */ 1523 public void setIconResource(int resId) { 1524 setIconDrawable(sStatic.mResources.getDrawable(resId)); 1525 } 1526 1527 @Override 1528 public void requestSetVolume(int volume) { 1529 final int maxVol = getVolumeMax(); 1530 if (maxVol == 0) { 1531 return; 1532 } 1533 1534 final float scaledVolume = (float) volume / maxVol; 1535 final int routeCount = getRouteCount(); 1536 for (int i = 0; i < routeCount; i++) { 1537 final RouteInfo route = getRouteAt(i); 1538 final int routeVol = (int) (scaledVolume * route.getVolumeMax()); 1539 route.requestSetVolume(routeVol); 1540 } 1541 if (volume != mVolume) { 1542 mVolume = volume; 1543 dispatchRouteVolumeChanged(this); 1544 } 1545 } 1546 1547 @Override 1548 public void requestUpdateVolume(int direction) { 1549 final int maxVol = getVolumeMax(); 1550 if (maxVol == 0) { 1551 return; 1552 } 1553 1554 final int routeCount = getRouteCount(); 1555 int volume = 0; 1556 for (int i = 0; i < routeCount; i++) { 1557 final RouteInfo route = getRouteAt(i); 1558 route.requestUpdateVolume(direction); 1559 final int routeVol = route.getVolume(); 1560 if (routeVol > volume) { 1561 volume = routeVol; 1562 } 1563 } 1564 if (volume != mVolume) { 1565 mVolume = volume; 1566 dispatchRouteVolumeChanged(this); 1567 } 1568 } 1569 1570 void memberNameChanged(RouteInfo info, CharSequence name) { 1571 mUpdateName = true; 1572 routeUpdated(); 1573 } 1574 1575 void memberStatusChanged(RouteInfo info, CharSequence status) { 1576 setStatusInt(status); 1577 } 1578 1579 void memberVolumeChanged(RouteInfo info) { 1580 updateVolume(); 1581 } 1582 1583 void updateVolume() { 1584 // A group always represents the highest component volume value. 1585 final int routeCount = getRouteCount(); 1586 int volume = 0; 1587 for (int i = 0; i < routeCount; i++) { 1588 final int routeVol = getRouteAt(i).getVolume(); 1589 if (routeVol > volume) { 1590 volume = routeVol; 1591 } 1592 } 1593 if (volume != mVolume) { 1594 mVolume = volume; 1595 dispatchRouteVolumeChanged(this); 1596 } 1597 } 1598 1599 @Override 1600 void routeUpdated() { 1601 int types = 0; 1602 final int count = mRoutes.size(); 1603 if (count == 0) { 1604 // Don't keep empty groups in the router. 1605 MediaRouter.removeRoute(this); 1606 return; 1607 } 1608 1609 int maxVolume = 0; 1610 boolean isLocal = true; 1611 boolean isFixedVolume = true; 1612 for (int i = 0; i < count; i++) { 1613 final RouteInfo route = mRoutes.get(i); 1614 types |= route.mSupportedTypes; 1615 final int routeMaxVolume = route.getVolumeMax(); 1616 if (routeMaxVolume > maxVolume) { 1617 maxVolume = routeMaxVolume; 1618 } 1619 isLocal &= route.getPlaybackType() == PLAYBACK_TYPE_LOCAL; 1620 isFixedVolume &= route.getVolumeHandling() == PLAYBACK_VOLUME_FIXED; 1621 } 1622 mPlaybackType = isLocal ? PLAYBACK_TYPE_LOCAL : PLAYBACK_TYPE_REMOTE; 1623 mVolumeHandling = isFixedVolume ? PLAYBACK_VOLUME_FIXED : PLAYBACK_VOLUME_VARIABLE; 1624 mSupportedTypes = types; 1625 mVolumeMax = maxVolume; 1626 mIcon = count == 1 ? mRoutes.get(0).getIconDrawable() : null; 1627 super.routeUpdated(); 1628 } 1629 1630 void updateName() { 1631 final StringBuilder sb = new StringBuilder(); 1632 final int count = mRoutes.size(); 1633 for (int i = 0; i < count; i++) { 1634 final RouteInfo info = mRoutes.get(i); 1635 // TODO: There's probably a much more correct way to localize this. 1636 if (i > 0) sb.append(", "); 1637 sb.append(info.mName); 1638 } 1639 mName = sb.toString(); 1640 mUpdateName = false; 1641 } 1642 1643 @Override 1644 public String toString() { 1645 StringBuilder sb = new StringBuilder(super.toString()); 1646 sb.append('['); 1647 final int count = mRoutes.size(); 1648 for (int i = 0; i < count; i++) { 1649 if (i > 0) sb.append(", "); 1650 sb.append(mRoutes.get(i)); 1651 } 1652 sb.append(']'); 1653 return sb.toString(); 1654 } 1655 } 1656 1657 /** 1658 * Definition of a category of routes. All routes belong to a category. 1659 */ 1660 public static class RouteCategory { 1661 CharSequence mName; 1662 int mNameResId; 1663 int mTypes; 1664 final boolean mGroupable; 1665 boolean mIsSystem; 1666 1667 RouteCategory(CharSequence name, int types, boolean groupable) { 1668 mName = name; 1669 mTypes = types; 1670 mGroupable = groupable; 1671 } 1672 1673 RouteCategory(int nameResId, int types, boolean groupable) { 1674 mNameResId = nameResId; 1675 mTypes = types; 1676 mGroupable = groupable; 1677 } 1678 1679 /** 1680 * @return the name of this route category 1681 */ 1682 public CharSequence getName() { 1683 return getName(sStatic.mResources); 1684 } 1685 1686 /** 1687 * Return the properly localized/configuration dependent name of this RouteCategory. 1688 * 1689 * @param context Context to resolve name resources 1690 * @return the name of this route category 1691 */ 1692 public CharSequence getName(Context context) { 1693 return getName(context.getResources()); 1694 } 1695 1696 CharSequence getName(Resources res) { 1697 if (mNameResId != 0) { 1698 return res.getText(mNameResId); 1699 } 1700 return mName; 1701 } 1702 1703 /** 1704 * Return the current list of routes in this category that have been added 1705 * to the MediaRouter. 1706 * 1707 * <p>This list will not include routes that are nested within RouteGroups. 1708 * A RouteGroup is treated as a single route within its category.</p> 1709 * 1710 * @param out a List to fill with the routes in this category. If this parameter is 1711 * non-null, it will be cleared, filled with the current routes with this 1712 * category, and returned. If this parameter is null, a new List will be 1713 * allocated to report the category's current routes. 1714 * @return A list with the routes in this category that have been added to the MediaRouter. 1715 */ 1716 public List<RouteInfo> getRoutes(List<RouteInfo> out) { 1717 if (out == null) { 1718 out = new ArrayList<RouteInfo>(); 1719 } else { 1720 out.clear(); 1721 } 1722 1723 final int count = getRouteCountStatic(); 1724 for (int i = 0; i < count; i++) { 1725 final RouteInfo route = getRouteAtStatic(i); 1726 if (route.mCategory == this) { 1727 out.add(route); 1728 } 1729 } 1730 return out; 1731 } 1732 1733 /** 1734 * @return Flag set describing the route types supported by this category 1735 */ 1736 public int getSupportedTypes() { 1737 return mTypes; 1738 } 1739 1740 /** 1741 * Return whether or not this category supports grouping. 1742 * 1743 * <p>If this method returns true, all routes obtained from this category 1744 * via calls to {@link #getRouteAt(int)} will be {@link MediaRouter.RouteGroup}s.</p> 1745 * 1746 * @return true if this category supports 1747 */ 1748 public boolean isGroupable() { 1749 return mGroupable; 1750 } 1751 1752 /** 1753 * @return true if this is the category reserved for system routes. 1754 * @hide 1755 */ 1756 public boolean isSystem() { 1757 return mIsSystem; 1758 } 1759 1760 public String toString() { 1761 return "RouteCategory{ name=" + mName + " types=" + typesToString(mTypes) + 1762 " groupable=" + mGroupable + " }"; 1763 } 1764 } 1765 1766 static class CallbackInfo { 1767 public int type; 1768 public final Callback cb; 1769 public final MediaRouter router; 1770 1771 public CallbackInfo(Callback cb, int type, MediaRouter router) { 1772 this.cb = cb; 1773 this.type = type; 1774 this.router = router; 1775 } 1776 } 1777 1778 /** 1779 * Interface for receiving events about media routing changes. 1780 * All methods of this interface will be called from the application's main thread. 1781 * 1782 * <p>A Callback will only receive events relevant to routes that the callback 1783 * was registered for.</p> 1784 * 1785 * @see MediaRouter#addCallback(int, Callback) 1786 * @see MediaRouter#removeCallback(Callback) 1787 */ 1788 public static abstract class Callback { 1789 /** 1790 * Called when the supplied route becomes selected as the active route 1791 * for the given route type. 1792 * 1793 * @param router the MediaRouter reporting the event 1794 * @param type Type flag set indicating the routes that have been selected 1795 * @param info Route that has been selected for the given route types 1796 */ 1797 public abstract void onRouteSelected(MediaRouter router, int type, RouteInfo info); 1798 1799 /** 1800 * Called when the supplied route becomes unselected as the active route 1801 * for the given route type. 1802 * 1803 * @param router the MediaRouter reporting the event 1804 * @param type Type flag set indicating the routes that have been unselected 1805 * @param info Route that has been unselected for the given route types 1806 */ 1807 public abstract void onRouteUnselected(MediaRouter router, int type, RouteInfo info); 1808 1809 /** 1810 * Called when a route for the specified type was added. 1811 * 1812 * @param router the MediaRouter reporting the event 1813 * @param info Route that has become available for use 1814 */ 1815 public abstract void onRouteAdded(MediaRouter router, RouteInfo info); 1816 1817 /** 1818 * Called when a route for the specified type was removed. 1819 * 1820 * @param router the MediaRouter reporting the event 1821 * @param info Route that has been removed from availability 1822 */ 1823 public abstract void onRouteRemoved(MediaRouter router, RouteInfo info); 1824 1825 /** 1826 * Called when an aspect of the indicated route has changed. 1827 * 1828 * <p>This will not indicate that the types supported by this route have 1829 * changed, only that cosmetic info such as name or status have been updated.</p> 1830 * 1831 * @param router the MediaRouter reporting the event 1832 * @param info The route that was changed 1833 */ 1834 public abstract void onRouteChanged(MediaRouter router, RouteInfo info); 1835 1836 /** 1837 * Called when a route is added to a group. 1838 * 1839 * @param router the MediaRouter reporting the event 1840 * @param info The route that was added 1841 * @param group The group the route was added to 1842 * @param index The route index within group that info was added at 1843 */ 1844 public abstract void onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group, 1845 int index); 1846 1847 /** 1848 * Called when a route is removed from a group. 1849 * 1850 * @param router the MediaRouter reporting the event 1851 * @param info The route that was removed 1852 * @param group The group the route was removed from 1853 */ 1854 public abstract void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group); 1855 1856 /** 1857 * Called when a route's volume changes. 1858 * 1859 * @param router the MediaRouter reporting the event 1860 * @param info The route with altered volume 1861 */ 1862 public abstract void onRouteVolumeChanged(MediaRouter router, RouteInfo info); 1863 } 1864 1865 /** 1866 * Stub implementation of {@link MediaRouter.Callback}. 1867 * Each abstract method is defined as a no-op. Override just the ones 1868 * you need. 1869 */ 1870 public static class SimpleCallback extends Callback { 1871 1872 @Override 1873 public void onRouteSelected(MediaRouter router, int type, RouteInfo info) { 1874 } 1875 1876 @Override 1877 public void onRouteUnselected(MediaRouter router, int type, RouteInfo info) { 1878 } 1879 1880 @Override 1881 public void onRouteAdded(MediaRouter router, RouteInfo info) { 1882 } 1883 1884 @Override 1885 public void onRouteRemoved(MediaRouter router, RouteInfo info) { 1886 } 1887 1888 @Override 1889 public void onRouteChanged(MediaRouter router, RouteInfo info) { 1890 } 1891 1892 @Override 1893 public void onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group, 1894 int index) { 1895 } 1896 1897 @Override 1898 public void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group) { 1899 } 1900 1901 @Override 1902 public void onRouteVolumeChanged(MediaRouter router, RouteInfo info) { 1903 } 1904 } 1905 1906 static class VolumeCallbackInfo { 1907 public final VolumeCallback vcb; 1908 public final RouteInfo route; 1909 1910 public VolumeCallbackInfo(VolumeCallback vcb, RouteInfo route) { 1911 this.vcb = vcb; 1912 this.route = route; 1913 } 1914 } 1915 1916 /** 1917 * Interface for receiving events about volume changes. 1918 * All methods of this interface will be called from the application's main thread. 1919 * 1920 * <p>A VolumeCallback will only receive events relevant to routes that the callback 1921 * was registered for.</p> 1922 * 1923 * @see UserRouteInfo#setVolumeCallback(VolumeCallback) 1924 */ 1925 public static abstract class VolumeCallback { 1926 /** 1927 * Called when the volume for the route should be increased or decreased. 1928 * @param info the route affected by this event 1929 * @param direction an integer indicating whether the volume is to be increased 1930 * (positive value) or decreased (negative value). 1931 * For bundled changes, the absolute value indicates the number of changes 1932 * in the same direction, e.g. +3 corresponds to three "volume up" changes. 1933 */ 1934 public abstract void onVolumeUpdateRequest(RouteInfo info, int direction); 1935 /** 1936 * Called when the volume for the route should be set to the given value 1937 * @param info the route affected by this event 1938 * @param volume an integer indicating the new volume value that should be used, always 1939 * between 0 and the value set by {@link UserRouteInfo#setVolumeMax(int)}. 1940 */ 1941 public abstract void onVolumeSetRequest(RouteInfo info, int volume); 1942 } 1943 1944 static class VolumeChangeReceiver extends BroadcastReceiver { 1945 @Override 1946 public void onReceive(Context context, Intent intent) { 1947 if (intent.getAction().equals(AudioManager.VOLUME_CHANGED_ACTION)) { 1948 final int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, 1949 -1); 1950 if (streamType != AudioManager.STREAM_MUSIC) { 1951 return; 1952 } 1953 1954 final int newVolume = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, 0); 1955 final int oldVolume = intent.getIntExtra( 1956 AudioManager.EXTRA_PREV_VOLUME_STREAM_VALUE, 0); 1957 if (newVolume != oldVolume) { 1958 systemVolumeChanged(newVolume); 1959 } 1960 } 1961 } 1962 } 1963 1964 static class WifiDisplayStatusChangedReceiver extends BroadcastReceiver { 1965 @Override 1966 public void onReceive(Context context, Intent intent) { 1967 if (intent.getAction().equals(DisplayManager.ACTION_WIFI_DISPLAY_STATUS_CHANGED)) { 1968 updateWifiDisplayStatus((WifiDisplayStatus) intent.getParcelableExtra( 1969 DisplayManager.EXTRA_WIFI_DISPLAY_STATUS)); 1970 } 1971 } 1972 } 1973} 1974