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