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