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