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