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