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