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