1 /* 2 * Copyright 2018 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 package com.android.settingslib.media; 17 18 import static android.media.MediaRoute2Info.TYPE_BLE_HEADSET; 19 import static android.media.MediaRoute2Info.TYPE_BLUETOOTH_A2DP; 20 import static android.media.MediaRoute2Info.TYPE_BUILTIN_SPEAKER; 21 import static android.media.MediaRoute2Info.TYPE_DOCK; 22 import static android.media.MediaRoute2Info.TYPE_GROUP; 23 import static android.media.MediaRoute2Info.TYPE_HDMI; 24 import static android.media.MediaRoute2Info.TYPE_HEARING_AID; 25 import static android.media.MediaRoute2Info.TYPE_REMOTE_SPEAKER; 26 import static android.media.MediaRoute2Info.TYPE_REMOTE_TV; 27 import static android.media.MediaRoute2Info.TYPE_UNKNOWN; 28 import static android.media.MediaRoute2Info.TYPE_USB_ACCESSORY; 29 import static android.media.MediaRoute2Info.TYPE_USB_DEVICE; 30 import static android.media.MediaRoute2Info.TYPE_USB_HEADSET; 31 import static android.media.MediaRoute2Info.TYPE_WIRED_HEADPHONES; 32 import static android.media.MediaRoute2Info.TYPE_WIRED_HEADSET; 33 import static android.media.MediaRoute2ProviderService.REASON_UNKNOWN_ERROR; 34 35 import static com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_SELECTED; 36 37 import android.annotation.TargetApi; 38 import android.app.Notification; 39 import android.bluetooth.BluetoothAdapter; 40 import android.bluetooth.BluetoothDevice; 41 import android.content.Context; 42 import android.media.MediaRoute2Info; 43 import android.media.MediaRouter2Manager; 44 import android.media.RoutingSessionInfo; 45 import android.os.Build; 46 import android.text.TextUtils; 47 import android.util.Log; 48 49 import androidx.annotation.RequiresApi; 50 51 import com.android.internal.annotations.VisibleForTesting; 52 import com.android.settingslib.bluetooth.CachedBluetoothDevice; 53 import com.android.settingslib.bluetooth.LocalBluetoothManager; 54 55 import java.util.ArrayList; 56 import java.util.List; 57 import java.util.concurrent.Executor; 58 import java.util.concurrent.Executors; 59 60 /** 61 * InfoMediaManager provide interface to get InfoMediaDevice list. 62 */ 63 @RequiresApi(Build.VERSION_CODES.R) 64 public class InfoMediaManager extends MediaManager { 65 66 private static final String TAG = "InfoMediaManager"; 67 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 68 @VisibleForTesting 69 final RouterManagerCallback mMediaRouterCallback = new RouterManagerCallback(); 70 @VisibleForTesting 71 final Executor mExecutor = Executors.newSingleThreadExecutor(); 72 @VisibleForTesting 73 MediaRouter2Manager mRouterManager; 74 @VisibleForTesting 75 String mPackageName; 76 private final boolean mVolumeAdjustmentForRemoteGroupSessions; 77 78 private MediaDevice mCurrentConnectedDevice; 79 private LocalBluetoothManager mBluetoothManager; 80 InfoMediaManager(Context context, String packageName, Notification notification, LocalBluetoothManager localBluetoothManager)81 public InfoMediaManager(Context context, String packageName, Notification notification, 82 LocalBluetoothManager localBluetoothManager) { 83 super(context, notification); 84 85 mRouterManager = MediaRouter2Manager.getInstance(context); 86 mBluetoothManager = localBluetoothManager; 87 if (!TextUtils.isEmpty(packageName)) { 88 mPackageName = packageName; 89 } 90 91 mVolumeAdjustmentForRemoteGroupSessions = context.getResources().getBoolean( 92 com.android.internal.R.bool.config_volumeAdjustmentForRemoteGroupSessions); 93 } 94 95 @Override startScan()96 public void startScan() { 97 mMediaDevices.clear(); 98 mRouterManager.registerCallback(mExecutor, mMediaRouterCallback); 99 mRouterManager.registerScanRequest(); 100 refreshDevices(); 101 } 102 103 @Override stopScan()104 public void stopScan() { 105 mRouterManager.unregisterCallback(mMediaRouterCallback); 106 mRouterManager.unregisterScanRequest(); 107 } 108 109 /** 110 * Get current device that played media. 111 * @return MediaDevice 112 */ getCurrentConnectedDevice()113 MediaDevice getCurrentConnectedDevice() { 114 return mCurrentConnectedDevice; 115 } 116 117 /** 118 * Transfer MediaDevice for media without package name. 119 */ connectDeviceWithoutPackageName(MediaDevice device)120 boolean connectDeviceWithoutPackageName(MediaDevice device) { 121 boolean isConnected = false; 122 final RoutingSessionInfo info = mRouterManager.getSystemRoutingSession(null); 123 if (info != null) { 124 mRouterManager.transfer(info, device.mRouteInfo); 125 isConnected = true; 126 } 127 return isConnected; 128 } 129 130 /** 131 * Add a MediaDevice to let it play current media. 132 * 133 * @param device MediaDevice 134 * @return If add device successful return {@code true}, otherwise return {@code false} 135 */ addDeviceToPlayMedia(MediaDevice device)136 boolean addDeviceToPlayMedia(MediaDevice device) { 137 if (TextUtils.isEmpty(mPackageName)) { 138 Log.w(TAG, "addDeviceToPlayMedia() package name is null or empty!"); 139 return false; 140 } 141 142 final RoutingSessionInfo info = getRoutingSessionInfo(); 143 if (info != null && info.getSelectableRoutes().contains(device.mRouteInfo.getId())) { 144 mRouterManager.selectRoute(info, device.mRouteInfo); 145 return true; 146 } 147 148 Log.w(TAG, "addDeviceToPlayMedia() Ignoring selecting a non-selectable device : " 149 + device.getName()); 150 151 return false; 152 } 153 getRoutingSessionInfo()154 private RoutingSessionInfo getRoutingSessionInfo() { 155 return getRoutingSessionInfo(mPackageName); 156 } 157 getRoutingSessionInfo(String packageName)158 private RoutingSessionInfo getRoutingSessionInfo(String packageName) { 159 final List<RoutingSessionInfo> sessionInfos = 160 mRouterManager.getRoutingSessions(packageName); 161 162 if (sessionInfos == null || sessionInfos.isEmpty()) { 163 return null; 164 } 165 return sessionInfos.get(sessionInfos.size() - 1); 166 } 167 isRoutingSessionAvailableForVolumeControl()168 boolean isRoutingSessionAvailableForVolumeControl() { 169 if (mVolumeAdjustmentForRemoteGroupSessions) { 170 return true; 171 } 172 List<RoutingSessionInfo> sessions = 173 mRouterManager.getRoutingSessions(mPackageName); 174 boolean foundNonSystemSession = false; 175 boolean isGroup = false; 176 for (RoutingSessionInfo session : sessions) { 177 if (!session.isSystemSession()) { 178 foundNonSystemSession = true; 179 int selectedRouteCount = session.getSelectedRoutes().size(); 180 if (selectedRouteCount > 1) { 181 isGroup = true; 182 break; 183 } 184 } 185 } 186 if (!foundNonSystemSession) { 187 Log.d(TAG, "No routing session for " + mPackageName); 188 return false; 189 } 190 return !isGroup; 191 } 192 preferRouteListingOrdering()193 boolean preferRouteListingOrdering() { 194 return false; 195 } 196 197 /** 198 * Remove a {@code device} from current media. 199 * 200 * @param device MediaDevice 201 * @return If device stop successful return {@code true}, otherwise return {@code false} 202 */ removeDeviceFromPlayMedia(MediaDevice device)203 boolean removeDeviceFromPlayMedia(MediaDevice device) { 204 if (TextUtils.isEmpty(mPackageName)) { 205 Log.w(TAG, "removeDeviceFromMedia() package name is null or empty!"); 206 return false; 207 } 208 209 final RoutingSessionInfo info = getRoutingSessionInfo(); 210 if (info != null && info.getSelectedRoutes().contains(device.mRouteInfo.getId())) { 211 mRouterManager.deselectRoute(info, device.mRouteInfo); 212 return true; 213 } 214 215 Log.w(TAG, "removeDeviceFromMedia() Ignoring deselecting a non-deselectable device : " 216 + device.getName()); 217 218 return false; 219 } 220 221 /** 222 * Release session to stop playing media on MediaDevice. 223 */ releaseSession()224 boolean releaseSession() { 225 if (TextUtils.isEmpty(mPackageName)) { 226 Log.w(TAG, "releaseSession() package name is null or empty!"); 227 return false; 228 } 229 230 final RoutingSessionInfo sessionInfo = getRoutingSessionInfo(); 231 232 if (sessionInfo != null) { 233 mRouterManager.releaseSession(sessionInfo); 234 return true; 235 } 236 237 Log.w(TAG, "releaseSession() Ignoring release session : " + mPackageName); 238 239 return false; 240 } 241 242 /** 243 * Get the MediaDevice list that can be added to current media. 244 * 245 * @return list of MediaDevice 246 */ getSelectableMediaDevice()247 List<MediaDevice> getSelectableMediaDevice() { 248 final List<MediaDevice> deviceList = new ArrayList<>(); 249 if (TextUtils.isEmpty(mPackageName)) { 250 Log.w(TAG, "getSelectableMediaDevice() package name is null or empty!"); 251 return deviceList; 252 } 253 254 final RoutingSessionInfo info = getRoutingSessionInfo(); 255 if (info != null) { 256 for (MediaRoute2Info route : mRouterManager.getSelectableRoutes(info)) { 257 deviceList.add(new InfoMediaDevice(mContext, mRouterManager, 258 route, mPackageName)); 259 } 260 return deviceList; 261 } 262 263 Log.w(TAG, "getSelectableMediaDevice() cannot found selectable MediaDevice from : " 264 + mPackageName); 265 266 return deviceList; 267 } 268 269 /** 270 * Get the MediaDevice list that can be removed from current media session. 271 * 272 * @return list of MediaDevice 273 */ getDeselectableMediaDevice()274 List<MediaDevice> getDeselectableMediaDevice() { 275 final List<MediaDevice> deviceList = new ArrayList<>(); 276 if (TextUtils.isEmpty(mPackageName)) { 277 Log.d(TAG, "getDeselectableMediaDevice() package name is null or empty!"); 278 return deviceList; 279 } 280 281 final RoutingSessionInfo info = getRoutingSessionInfo(); 282 if (info != null) { 283 for (MediaRoute2Info route : mRouterManager.getDeselectableRoutes(info)) { 284 deviceList.add(new InfoMediaDevice(mContext, mRouterManager, 285 route, mPackageName)); 286 Log.d(TAG, route.getName() + " is deselectable for " + mPackageName); 287 } 288 return deviceList; 289 } 290 Log.d(TAG, "getDeselectableMediaDevice() cannot found deselectable MediaDevice from : " 291 + mPackageName); 292 293 return deviceList; 294 } 295 296 /** 297 * Get the MediaDevice list that has been selected to current media. 298 * 299 * @return list of MediaDevice 300 */ getSelectedMediaDevice()301 List<MediaDevice> getSelectedMediaDevice() { 302 final List<MediaDevice> deviceList = new ArrayList<>(); 303 if (TextUtils.isEmpty(mPackageName)) { 304 Log.w(TAG, "getSelectedMediaDevice() package name is null or empty!"); 305 return deviceList; 306 } 307 308 final RoutingSessionInfo info = getRoutingSessionInfo(); 309 if (info != null) { 310 for (MediaRoute2Info route : mRouterManager.getSelectedRoutes(info)) { 311 deviceList.add(new InfoMediaDevice(mContext, mRouterManager, 312 route, mPackageName)); 313 } 314 return deviceList; 315 } 316 317 Log.w(TAG, "getSelectedMediaDevice() cannot found selectable MediaDevice from : " 318 + mPackageName); 319 320 return deviceList; 321 } 322 adjustSessionVolume(RoutingSessionInfo info, int volume)323 void adjustSessionVolume(RoutingSessionInfo info, int volume) { 324 if (info == null) { 325 Log.w(TAG, "Unable to adjust session volume. RoutingSessionInfo is empty"); 326 return; 327 } 328 329 mRouterManager.setSessionVolume(info, volume); 330 } 331 332 /** 333 * Adjust the volume of {@link android.media.RoutingSessionInfo}. 334 * 335 * @param volume the value of volume 336 */ adjustSessionVolume(int volume)337 void adjustSessionVolume(int volume) { 338 if (TextUtils.isEmpty(mPackageName)) { 339 Log.w(TAG, "adjustSessionVolume() package name is null or empty!"); 340 return; 341 } 342 343 final RoutingSessionInfo info = getRoutingSessionInfo(); 344 if (info != null) { 345 Log.d(TAG, "adjustSessionVolume() adjust volume : " + volume + ", with : " 346 + mPackageName); 347 mRouterManager.setSessionVolume(info, volume); 348 return; 349 } 350 351 Log.w(TAG, "adjustSessionVolume() can't found corresponding RoutingSession with : " 352 + mPackageName); 353 } 354 355 /** 356 * Gets the maximum volume of the {@link android.media.RoutingSessionInfo}. 357 * 358 * @return maximum volume of the session, and return -1 if not found. 359 */ getSessionVolumeMax()360 public int getSessionVolumeMax() { 361 if (TextUtils.isEmpty(mPackageName)) { 362 Log.w(TAG, "getSessionVolumeMax() package name is null or empty!"); 363 return -1; 364 } 365 366 final RoutingSessionInfo info = getRoutingSessionInfo(); 367 if (info != null) { 368 return info.getVolumeMax(); 369 } 370 371 Log.w(TAG, "getSessionVolumeMax() can't found corresponding RoutingSession with : " 372 + mPackageName); 373 return -1; 374 } 375 376 /** 377 * Gets the current volume of the {@link android.media.RoutingSessionInfo}. 378 * 379 * @return current volume of the session, and return -1 if not found. 380 */ getSessionVolume()381 public int getSessionVolume() { 382 if (TextUtils.isEmpty(mPackageName)) { 383 Log.w(TAG, "getSessionVolume() package name is null or empty!"); 384 return -1; 385 } 386 387 final RoutingSessionInfo info = getRoutingSessionInfo(); 388 if (info != null) { 389 return info.getVolume(); 390 } 391 392 Log.w(TAG, "getSessionVolume() can't found corresponding RoutingSession with : " 393 + mPackageName); 394 return -1; 395 } 396 getSessionName()397 CharSequence getSessionName() { 398 if (TextUtils.isEmpty(mPackageName)) { 399 Log.w(TAG, "Unable to get session name. The package name is null or empty!"); 400 return null; 401 } 402 403 final RoutingSessionInfo info = getRoutingSessionInfo(); 404 if (info != null) { 405 return info.getName(); 406 } 407 408 Log.w(TAG, "Unable to get session name for package: " + mPackageName); 409 return null; 410 } 411 shouldDisableMediaOutput(String packageName)412 boolean shouldDisableMediaOutput(String packageName) { 413 if (TextUtils.isEmpty(packageName)) { 414 Log.w(TAG, "shouldDisableMediaOutput() package name is null or empty!"); 415 return true; 416 } 417 418 // Disable when there is no transferable route 419 return mRouterManager.getTransferableRoutes(packageName).isEmpty(); 420 } 421 422 @TargetApi(Build.VERSION_CODES.R) shouldEnableVolumeSeekBar(RoutingSessionInfo sessionInfo)423 boolean shouldEnableVolumeSeekBar(RoutingSessionInfo sessionInfo) { 424 return sessionInfo.isSystemSession() // System sessions are not remote 425 || mVolumeAdjustmentForRemoteGroupSessions 426 || sessionInfo.getSelectedRoutes().size() <= 1; 427 } 428 refreshDevices()429 private synchronized void refreshDevices() { 430 mMediaDevices.clear(); 431 mCurrentConnectedDevice = null; 432 if (TextUtils.isEmpty(mPackageName)) { 433 buildAllRoutes(); 434 } else { 435 buildAvailableRoutes(); 436 } 437 dispatchDeviceListAdded(); 438 } 439 buildAllRoutes()440 private void buildAllRoutes() { 441 for (MediaRoute2Info route : mRouterManager.getAllRoutes()) { 442 if (DEBUG) { 443 Log.d(TAG, "buildAllRoutes() route : " + route.getName() + ", volume : " 444 + route.getVolume() + ", type : " + route.getType()); 445 } 446 if (route.isSystemRoute()) { 447 addMediaDevice(route); 448 } 449 } 450 } 451 getActiveMediaSession()452 List<RoutingSessionInfo> getActiveMediaSession() { 453 List<RoutingSessionInfo> infos = new ArrayList<>(); 454 infos.add(mRouterManager.getSystemRoutingSession(null)); 455 infos.addAll(mRouterManager.getRemoteSessions()); 456 return infos; 457 } 458 buildAvailableRoutes()459 private synchronized void buildAvailableRoutes() { 460 for (MediaRoute2Info route : getAvailableRoutes(mPackageName)) { 461 if (DEBUG) { 462 Log.d(TAG, "buildAvailableRoutes() route : " + route.getName() + ", volume : " 463 + route.getVolume() + ", type : " + route.getType()); 464 } 465 addMediaDevice(route); 466 } 467 } 468 getAvailableRoutes(String packageName)469 private synchronized List<MediaRoute2Info> getAvailableRoutes(String packageName) { 470 final List<MediaRoute2Info> infos = new ArrayList<>(); 471 RoutingSessionInfo routingSessionInfo = getRoutingSessionInfo(packageName); 472 if (routingSessionInfo != null) { 473 infos.addAll(mRouterManager.getSelectedRoutes(routingSessionInfo)); 474 infos.addAll(mRouterManager.getSelectableRoutes(routingSessionInfo)); 475 } 476 final List<MediaRoute2Info> transferableRoutes = 477 mRouterManager.getTransferableRoutes(packageName); 478 for (MediaRoute2Info transferableRoute : transferableRoutes) { 479 boolean alreadyAdded = false; 480 for (MediaRoute2Info mediaRoute2Info : infos) { 481 if (TextUtils.equals(transferableRoute.getId(), mediaRoute2Info.getId())) { 482 alreadyAdded = true; 483 break; 484 } 485 } 486 if (!alreadyAdded) { 487 infos.add(transferableRoute); 488 } 489 } 490 return infos; 491 } 492 493 @VisibleForTesting addMediaDevice(MediaRoute2Info route)494 void addMediaDevice(MediaRoute2Info route) { 495 final int deviceType = route.getType(); 496 MediaDevice mediaDevice = null; 497 switch (deviceType) { 498 case TYPE_UNKNOWN: 499 case TYPE_REMOTE_TV: 500 case TYPE_REMOTE_SPEAKER: 501 case TYPE_GROUP: 502 //TODO(b/148765806): use correct device type once api is ready. 503 mediaDevice = new InfoMediaDevice(mContext, mRouterManager, route, 504 mPackageName); 505 if (!TextUtils.isEmpty(mPackageName) 506 && getRoutingSessionInfo().getSelectedRoutes().contains(route.getId())) { 507 mediaDevice.setState(STATE_SELECTED); 508 if (mCurrentConnectedDevice == null) { 509 mCurrentConnectedDevice = mediaDevice; 510 } 511 } 512 break; 513 case TYPE_BUILTIN_SPEAKER: 514 case TYPE_USB_DEVICE: 515 case TYPE_USB_HEADSET: 516 case TYPE_USB_ACCESSORY: 517 case TYPE_DOCK: 518 case TYPE_HDMI: 519 case TYPE_WIRED_HEADSET: 520 case TYPE_WIRED_HEADPHONES: 521 mediaDevice = 522 new PhoneMediaDevice(mContext, mRouterManager, route, mPackageName); 523 break; 524 case TYPE_HEARING_AID: 525 case TYPE_BLUETOOTH_A2DP: 526 case TYPE_BLE_HEADSET: 527 final BluetoothDevice device = 528 BluetoothAdapter.getDefaultAdapter().getRemoteDevice(route.getAddress()); 529 final CachedBluetoothDevice cachedDevice = 530 mBluetoothManager.getCachedDeviceManager().findDevice(device); 531 if (cachedDevice != null) { 532 mediaDevice = new BluetoothMediaDevice(mContext, cachedDevice, mRouterManager, 533 route, mPackageName); 534 } 535 break; 536 default: 537 Log.w(TAG, "addMediaDevice() unknown device type : " + deviceType); 538 break; 539 540 } 541 542 if (mediaDevice != null) { 543 mMediaDevices.add(mediaDevice); 544 } 545 } 546 547 class RouterManagerCallback implements MediaRouter2Manager.Callback { 548 549 @Override onRoutesAdded(List<MediaRoute2Info> routes)550 public void onRoutesAdded(List<MediaRoute2Info> routes) { 551 refreshDevices(); 552 } 553 554 @Override onPreferredFeaturesChanged(String packageName, List<String> preferredFeatures)555 public void onPreferredFeaturesChanged(String packageName, List<String> preferredFeatures) { 556 if (TextUtils.equals(mPackageName, packageName)) { 557 refreshDevices(); 558 } 559 } 560 561 @Override onRoutesChanged(List<MediaRoute2Info> routes)562 public void onRoutesChanged(List<MediaRoute2Info> routes) { 563 refreshDevices(); 564 } 565 566 @Override onRoutesRemoved(List<MediaRoute2Info> routes)567 public void onRoutesRemoved(List<MediaRoute2Info> routes) { 568 refreshDevices(); 569 } 570 571 @Override onTransferred(RoutingSessionInfo oldSession, RoutingSessionInfo newSession)572 public void onTransferred(RoutingSessionInfo oldSession, RoutingSessionInfo newSession) { 573 if (DEBUG) { 574 Log.d(TAG, "onTransferred() oldSession : " + oldSession.getName() 575 + ", newSession : " + newSession.getName()); 576 } 577 mMediaDevices.clear(); 578 mCurrentConnectedDevice = null; 579 if (TextUtils.isEmpty(mPackageName)) { 580 buildAllRoutes(); 581 } else { 582 buildAvailableRoutes(); 583 } 584 585 final String id = mCurrentConnectedDevice != null 586 ? mCurrentConnectedDevice.getId() 587 : null; 588 dispatchConnectedDeviceChanged(id); 589 } 590 591 @Override onTransferFailed(RoutingSessionInfo session, MediaRoute2Info route)592 public void onTransferFailed(RoutingSessionInfo session, MediaRoute2Info route) { 593 dispatchOnRequestFailed(REASON_UNKNOWN_ERROR); 594 } 595 596 @Override onRequestFailed(int reason)597 public void onRequestFailed(int reason) { 598 dispatchOnRequestFailed(reason); 599 } 600 601 @Override onSessionUpdated(RoutingSessionInfo sessionInfo)602 public void onSessionUpdated(RoutingSessionInfo sessionInfo) { 603 refreshDevices(); 604 } 605 } 606 } 607