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