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