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