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