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