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