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