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