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