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