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