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.MediaRoute2ProviderService.REASON_UNKNOWN_ERROR; 19 20 import android.bluetooth.BluetoothAdapter; 21 import android.bluetooth.BluetoothDevice; 22 import android.content.ComponentName; 23 import android.content.Context; 24 import android.content.pm.PackageManager; 25 import android.graphics.drawable.Drawable; 26 import android.media.AudioDeviceAttributes; 27 import android.media.AudioManager; 28 import android.media.RoutingSessionInfo; 29 import android.os.Build; 30 import android.text.TextUtils; 31 import android.util.Log; 32 33 import androidx.annotation.IntDef; 34 import androidx.annotation.NonNull; 35 import androidx.annotation.Nullable; 36 import androidx.annotation.RequiresApi; 37 38 import com.android.internal.annotations.VisibleForTesting; 39 import com.android.settingslib.bluetooth.A2dpProfile; 40 import com.android.settingslib.bluetooth.BluetoothCallback; 41 import com.android.settingslib.bluetooth.CachedBluetoothDevice; 42 import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; 43 import com.android.settingslib.bluetooth.HearingAidProfile; 44 import com.android.settingslib.bluetooth.LeAudioProfile; 45 import com.android.settingslib.bluetooth.LocalBluetoothManager; 46 import com.android.settingslib.bluetooth.LocalBluetoothProfile; 47 48 import java.lang.annotation.Retention; 49 import java.lang.annotation.RetentionPolicy; 50 import java.util.ArrayList; 51 import java.util.Collection; 52 import java.util.List; 53 import java.util.concurrent.CopyOnWriteArrayList; 54 55 /** 56 * LocalMediaManager provide interface to get MediaDevice list and transfer media to MediaDevice. 57 */ 58 @RequiresApi(Build.VERSION_CODES.R) 59 public class LocalMediaManager implements BluetoothCallback { 60 private static final String TAG = "LocalMediaManager"; 61 private static final int MAX_DISCONNECTED_DEVICE_NUM = 5; 62 63 @Retention(RetentionPolicy.SOURCE) 64 @IntDef({MediaDeviceState.STATE_CONNECTED, 65 MediaDeviceState.STATE_CONNECTING, 66 MediaDeviceState.STATE_DISCONNECTED, 67 MediaDeviceState.STATE_CONNECTING_FAILED, 68 MediaDeviceState.STATE_SELECTED, 69 MediaDeviceState.STATE_GROUPING}) 70 public @interface MediaDeviceState { 71 int STATE_CONNECTED = 0; 72 int STATE_CONNECTING = 1; 73 int STATE_DISCONNECTED = 2; 74 int STATE_CONNECTING_FAILED = 3; 75 int STATE_SELECTED = 4; 76 int STATE_GROUPING = 5; 77 } 78 79 private final Collection<DeviceCallback> mCallbacks = new CopyOnWriteArrayList<>(); 80 private final Object mMediaDevicesLock = new Object(); 81 @VisibleForTesting 82 final MediaDeviceCallback mMediaDeviceCallback = new MediaDeviceCallback(); 83 84 private Context mContext; 85 private LocalBluetoothManager mLocalBluetoothManager; 86 private InfoMediaManager mInfoMediaManager; 87 private String mPackageName; 88 private MediaDevice mOnTransferBluetoothDevice; 89 @VisibleForTesting 90 AudioManager mAudioManager; 91 92 @VisibleForTesting 93 List<MediaDevice> mMediaDevices = new CopyOnWriteArrayList<>(); 94 @VisibleForTesting 95 List<MediaDevice> mDisconnectedMediaDevices = new CopyOnWriteArrayList<>(); 96 @VisibleForTesting 97 MediaDevice mCurrentConnectedDevice; 98 @VisibleForTesting 99 DeviceAttributeChangeCallback mDeviceAttributeChangeCallback = 100 new DeviceAttributeChangeCallback(); 101 @VisibleForTesting 102 BluetoothAdapter mBluetoothAdapter; 103 104 /** 105 * Register to start receiving callbacks for MediaDevice events. 106 */ registerCallback(DeviceCallback callback)107 public void registerCallback(DeviceCallback callback) { 108 boolean wasEmpty = mCallbacks.isEmpty(); 109 if (!mCallbacks.contains(callback)) { 110 mCallbacks.add(callback); 111 if (wasEmpty) { 112 mInfoMediaManager.registerCallback(mMediaDeviceCallback); 113 } 114 } 115 } 116 117 /** 118 * Unregister to stop receiving callbacks for MediaDevice events 119 */ unregisterCallback(DeviceCallback callback)120 public void unregisterCallback(DeviceCallback callback) { 121 if (mCallbacks.remove(callback) && mCallbacks.isEmpty()) { 122 mInfoMediaManager.unregisterCallback(mMediaDeviceCallback); 123 unRegisterDeviceAttributeChangeCallback(); 124 } 125 } 126 127 /** 128 * Creates a LocalMediaManager with references to given managers. 129 * 130 * It will obtain a {@link LocalBluetoothManager} by calling 131 * {@link LocalBluetoothManager#getInstance} and create an {@link InfoMediaManager} passing 132 * that bluetooth manager. 133 * 134 * It will use {@link BluetoothAdapter#getDefaultAdapter()] for setting the bluetooth adapter. 135 */ LocalMediaManager(Context context, String packageName)136 public LocalMediaManager(Context context, String packageName) { 137 mContext = context; 138 mPackageName = packageName; 139 mLocalBluetoothManager = 140 LocalBluetoothManager.getInstance(context, /* onInitCallback= */ null); 141 mAudioManager = context.getSystemService(AudioManager.class); 142 mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); 143 if (mLocalBluetoothManager == null) { 144 Log.e(TAG, "Bluetooth is not supported on this device"); 145 return; 146 } 147 148 mInfoMediaManager = 149 // TODO: b/321969740 - Take the userHandle as a parameter and pass it through. The 150 // package name is not sufficient to unambiguously identify an app. 151 InfoMediaManager.createInstance( 152 context, 153 packageName, 154 /* userHandle */ null, 155 mLocalBluetoothManager, 156 /* token */ null); 157 } 158 159 /** 160 * Creates a LocalMediaManager with references to given managers. 161 * 162 * It will use {@link BluetoothAdapter#getDefaultAdapter()] for setting the bluetooth adapter. 163 */ LocalMediaManager(Context context, LocalBluetoothManager localBluetoothManager, InfoMediaManager infoMediaManager, String packageName)164 public LocalMediaManager(Context context, LocalBluetoothManager localBluetoothManager, 165 InfoMediaManager infoMediaManager, String packageName) { 166 mContext = context; 167 mLocalBluetoothManager = localBluetoothManager; 168 mInfoMediaManager = infoMediaManager; 169 mPackageName = packageName; 170 mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); 171 mAudioManager = context.getSystemService(AudioManager.class); 172 } 173 174 /** 175 * Connect the MediaDevice to transfer media 176 * @param connectDevice the MediaDevice 177 * @return {@code true} if successfully call, otherwise return {@code false} 178 */ connectDevice(MediaDevice connectDevice)179 public boolean connectDevice(MediaDevice connectDevice) { 180 MediaDevice device = getMediaDeviceById(connectDevice.getId()); 181 if (device == null) { 182 Log.w(TAG, "connectDevice() connectDevice not in the list!"); 183 return false; 184 } 185 if (device instanceof BluetoothMediaDevice) { 186 final CachedBluetoothDevice cachedDevice = 187 ((BluetoothMediaDevice) device).getCachedDevice(); 188 if (!cachedDevice.isConnected() && !cachedDevice.isBusy()) { 189 mOnTransferBluetoothDevice = connectDevice; 190 device.setState(MediaDeviceState.STATE_CONNECTING); 191 cachedDevice.connect(); 192 return true; 193 } 194 } 195 196 if (device.equals(mCurrentConnectedDevice)) { 197 Log.d(TAG, "connectDevice() this device is already connected! : " + device.getName()); 198 return false; 199 } 200 201 device.setState(MediaDeviceState.STATE_CONNECTING); 202 mInfoMediaManager.connectToDevice(device); 203 return true; 204 } 205 dispatchSelectedDeviceStateChanged(MediaDevice device, @MediaDeviceState int state)206 void dispatchSelectedDeviceStateChanged(MediaDevice device, @MediaDeviceState int state) { 207 for (DeviceCallback callback : getCallbacks()) { 208 callback.onSelectedDeviceStateChanged(device, state); 209 } 210 } 211 212 /** 213 * Returns if the media session is available for volume control. 214 * @return True if this media session is available for colume control, false otherwise. 215 */ isMediaSessionAvailableForVolumeControl()216 public boolean isMediaSessionAvailableForVolumeControl() { 217 return mInfoMediaManager.isRoutingSessionAvailableForVolumeControl(); 218 } 219 220 /** 221 * Returns if media app establishes a preferred route listing order. 222 * 223 * @return True if route list ordering exist and not using system ordering, false otherwise. 224 */ isPreferenceRouteListingExist()225 public boolean isPreferenceRouteListingExist() { 226 return mInfoMediaManager.preferRouteListingOrdering(); 227 } 228 229 /** 230 * Returns required component name for system to take the user back to the app by launching an 231 * intent with the returned {@link ComponentName}, using action {@link #ACTION_TRANSFER_MEDIA}, 232 * with the extra {@link #EXTRA_ROUTE_ID}. 233 */ 234 @Nullable getLinkedItemComponentName()235 public ComponentName getLinkedItemComponentName() { 236 return mInfoMediaManager.getLinkedItemComponentName(); 237 } 238 239 /** 240 * Start scan connected MediaDevice 241 */ startScan()242 public void startScan() { 243 mInfoMediaManager.startScan(); 244 } 245 dispatchDeviceListUpdate()246 void dispatchDeviceListUpdate() { 247 final List<MediaDevice> mediaDevices = new ArrayList<>(mMediaDevices); 248 for (DeviceCallback callback : getCallbacks()) { 249 callback.onDeviceListUpdate(mediaDevices); 250 } 251 } 252 dispatchDeviceAttributesChanged()253 void dispatchDeviceAttributesChanged() { 254 for (DeviceCallback callback : getCallbacks()) { 255 callback.onDeviceAttributesChanged(); 256 } 257 } 258 dispatchOnRequestFailed(int reason)259 void dispatchOnRequestFailed(int reason) { 260 for (DeviceCallback callback : getCallbacks()) { 261 callback.onRequestFailed(reason); 262 } 263 } 264 265 /** 266 * Dispatch a change in the about-to-connect device. See 267 * {@link DeviceCallback#onAboutToConnectDeviceAdded} for more information. 268 */ dispatchAboutToConnectDeviceAdded( @onNull String deviceAddress, @NonNull String deviceName, @Nullable Drawable deviceIcon)269 public void dispatchAboutToConnectDeviceAdded( 270 @NonNull String deviceAddress, 271 @NonNull String deviceName, 272 @Nullable Drawable deviceIcon) { 273 for (DeviceCallback callback : getCallbacks()) { 274 callback.onAboutToConnectDeviceAdded(deviceAddress, deviceName, deviceIcon); 275 } 276 } 277 278 /** 279 * Dispatch a change in the about-to-connect device. See 280 * {@link DeviceCallback#onAboutToConnectDeviceRemoved} for more information. 281 */ dispatchAboutToConnectDeviceRemoved()282 public void dispatchAboutToConnectDeviceRemoved() { 283 for (DeviceCallback callback : getCallbacks()) { 284 callback.onAboutToConnectDeviceRemoved(); 285 } 286 } 287 288 /** 289 * Stop scan MediaDevice 290 */ stopScan()291 public void stopScan() { 292 mInfoMediaManager.stopScan(); 293 } 294 295 /** 296 * Find the MediaDevice through id. 297 * 298 * @param id the unique id of MediaDevice 299 * @return MediaDevice 300 */ getMediaDeviceById(String id)301 public MediaDevice getMediaDeviceById(String id) { 302 synchronized (mMediaDevicesLock) { 303 for (MediaDevice mediaDevice : mMediaDevices) { 304 if (TextUtils.equals(mediaDevice.getId(), id)) { 305 return mediaDevice; 306 } 307 } 308 } 309 Log.i(TAG, "getMediaDeviceById() failed to find device with id: " + id); 310 return null; 311 } 312 313 /** 314 * Find the current connected MediaDevice. 315 * 316 * @return MediaDevice 317 */ 318 @Nullable getCurrentConnectedDevice()319 public MediaDevice getCurrentConnectedDevice() { 320 return mCurrentConnectedDevice; 321 } 322 323 /** 324 * Add a MediaDevice to let it play current media. 325 * 326 * @param device MediaDevice 327 * @return If add device successful return {@code true}, otherwise return {@code false} 328 */ addDeviceToPlayMedia(MediaDevice device)329 public boolean addDeviceToPlayMedia(MediaDevice device) { 330 device.setState(MediaDeviceState.STATE_GROUPING); 331 return mInfoMediaManager.addDeviceToPlayMedia(device); 332 } 333 334 /** 335 * Remove a {@code device} from current media. 336 * 337 * @param device MediaDevice 338 * @return If device stop successful return {@code true}, otherwise return {@code false} 339 */ removeDeviceFromPlayMedia(MediaDevice device)340 public boolean removeDeviceFromPlayMedia(MediaDevice device) { 341 device.setState(MediaDeviceState.STATE_GROUPING); 342 return mInfoMediaManager.removeDeviceFromPlayMedia(device); 343 } 344 345 /** 346 * Get the MediaDevice list that can be added to current media. 347 * 348 * @return list of MediaDevice 349 */ getSelectableMediaDevice()350 public List<MediaDevice> getSelectableMediaDevice() { 351 return mInfoMediaManager.getSelectableMediaDevices(); 352 } 353 354 /** 355 * Gets the MediaDevice list that can be transferred to with the current media session by the 356 * media route provider. 357 * 358 * @return list of MediaDevice 359 */ 360 @NonNull getTransferableMediaDevices()361 public List<MediaDevice> getTransferableMediaDevices() { 362 return mInfoMediaManager.getTransferableMediaDevices(); 363 } 364 365 /** 366 * Get the MediaDevice list that can be removed from current media session. 367 * 368 * @return list of MediaDevice 369 */ getDeselectableMediaDevice()370 public List<MediaDevice> getDeselectableMediaDevice() { 371 return mInfoMediaManager.getDeselectableMediaDevices(); 372 } 373 374 /** 375 * Release session to stop playing media on MediaDevice. 376 */ releaseSession()377 public boolean releaseSession() { 378 return mInfoMediaManager.releaseSession(); 379 } 380 381 /** 382 * Get the MediaDevice list that has been selected to current media. 383 * 384 * @return list of MediaDevice 385 */ getSelectedMediaDevice()386 public List<MediaDevice> getSelectedMediaDevice() { 387 return mInfoMediaManager.getSelectedMediaDevices(); 388 } 389 390 /** 391 * Requests a volume change for a specific media device. 392 * 393 * This operation is different from {@link #adjustSessionVolume(String, int)}, which changes the 394 * volume of the overall session. 395 */ adjustDeviceVolume(MediaDevice device, int volume)396 public void adjustDeviceVolume(MediaDevice device, int volume) { 397 mInfoMediaManager.adjustDeviceVolume(device, volume); 398 } 399 400 /** 401 * Adjust the volume of session. 402 * 403 * @param sessionId the value of media session id 404 * @param volume the value of volume 405 */ adjustSessionVolume(String sessionId, int volume)406 public void adjustSessionVolume(String sessionId, int volume) { 407 RoutingSessionInfo session = mInfoMediaManager.getRoutingSessionById(sessionId); 408 if (session != null) { 409 mInfoMediaManager.adjustSessionVolume(session, volume); 410 } else { 411 Log.w(TAG, "adjustSessionVolume: Unable to find session: " + sessionId); 412 } 413 } 414 415 /** 416 * Adjust the volume of session. 417 * 418 * @param volume the value of volume 419 */ adjustSessionVolume(int volume)420 public void adjustSessionVolume(int volume) { 421 mInfoMediaManager.adjustSessionVolume(volume); 422 } 423 424 /** 425 * Gets the maximum volume of the {@link android.media.RoutingSessionInfo}. 426 * 427 * @return maximum volume of the session, and return -1 if not found. 428 */ getSessionVolumeMax()429 public int getSessionVolumeMax() { 430 return mInfoMediaManager.getSessionVolumeMax(); 431 } 432 433 /** 434 * Gets the current volume of the {@link android.media.RoutingSessionInfo}. 435 * 436 * @return current volume of the session, and return -1 if not found. 437 */ getSessionVolume()438 public int getSessionVolume() { 439 return mInfoMediaManager.getSessionVolume(); 440 } 441 442 /** 443 * Gets the user-visible name of the {@link android.media.RoutingSessionInfo}. 444 * 445 * @return current name of the session, and return {@code null} if not found. 446 */ 447 @Nullable getSessionName()448 public CharSequence getSessionName() { 449 return mInfoMediaManager.getSessionName(); 450 } 451 452 /** 453 * Gets the list of remote {@link RoutingSessionInfo routing sessions} known to the system. 454 * 455 * <p>This list does not include any system routing sessions. 456 */ getRemoteRoutingSessions()457 public List<RoutingSessionInfo> getRemoteRoutingSessions() { 458 return mInfoMediaManager.getRemoteSessions(); 459 } 460 461 /** 462 * Gets the current package name. 463 * 464 * @return current package name 465 */ getPackageName()466 public String getPackageName() { 467 return mPackageName; 468 } 469 470 /** 471 * Returns {@code true} if needed to enable volume seekbar, otherwise returns {@code false}. 472 */ shouldEnableVolumeSeekBar(RoutingSessionInfo sessionInfo)473 public boolean shouldEnableVolumeSeekBar(RoutingSessionInfo sessionInfo) { 474 return mInfoMediaManager.shouldEnableVolumeSeekBar(sessionInfo); 475 } 476 477 @VisibleForTesting updateCurrentConnectedDevice()478 MediaDevice updateCurrentConnectedDevice() { 479 MediaDevice connectedDevice = null; 480 synchronized (mMediaDevicesLock) { 481 for (MediaDevice device : mMediaDevices) { 482 if (device instanceof BluetoothMediaDevice) { 483 if (isActiveDevice(((BluetoothMediaDevice) device).getCachedDevice()) 484 && device.isConnected()) { 485 return device; 486 } 487 } else if (device instanceof PhoneMediaDevice) { 488 connectedDevice = device; 489 } 490 } 491 } 492 493 return connectedDevice; 494 } 495 isActiveDevice(CachedBluetoothDevice device)496 private boolean isActiveDevice(CachedBluetoothDevice device) { 497 boolean isActiveDeviceA2dp = false; 498 boolean isActiveDeviceHearingAid = false; 499 boolean isActiveLeAudio = false; 500 final A2dpProfile a2dpProfile = mLocalBluetoothManager.getProfileManager().getA2dpProfile(); 501 if (a2dpProfile != null) { 502 isActiveDeviceA2dp = device.getDevice().equals(a2dpProfile.getActiveDevice()); 503 } 504 if (!isActiveDeviceA2dp) { 505 final HearingAidProfile hearingAidProfile = mLocalBluetoothManager.getProfileManager() 506 .getHearingAidProfile(); 507 if (hearingAidProfile != null) { 508 isActiveDeviceHearingAid = 509 hearingAidProfile.getActiveDevices().contains(device.getDevice()); 510 } 511 } 512 513 if (!isActiveDeviceA2dp && !isActiveDeviceHearingAid) { 514 final LeAudioProfile leAudioProfile = mLocalBluetoothManager.getProfileManager() 515 .getLeAudioProfile(); 516 if (leAudioProfile != null) { 517 isActiveLeAudio = leAudioProfile.getActiveDevices().contains(device.getDevice()); 518 } 519 } 520 521 return isActiveDeviceA2dp || isActiveDeviceHearingAid || isActiveLeAudio; 522 } 523 getCallbacks()524 private Collection<DeviceCallback> getCallbacks() { 525 return new CopyOnWriteArrayList<>(mCallbacks); 526 } 527 528 class MediaDeviceCallback implements InfoMediaManager.MediaDeviceCallback { 529 @Override onDeviceListAdded(@onNull List<MediaDevice> devices)530 public void onDeviceListAdded(@NonNull List<MediaDevice> devices) { 531 synchronized (mMediaDevicesLock) { 532 mMediaDevices.clear(); 533 mMediaDevices.addAll(devices); 534 // Add muting expected bluetooth devices only when phone output device is available. 535 for (MediaDevice device : devices) { 536 final int type = device.getDeviceType(); 537 if (type == MediaDevice.MediaDeviceType.TYPE_USB_C_AUDIO_DEVICE 538 || type == MediaDevice.MediaDeviceType.TYPE_3POINT5_MM_AUDIO_DEVICE 539 || type == MediaDevice.MediaDeviceType.TYPE_PHONE_DEVICE) { 540 if (isTv()) { 541 mMediaDevices.addAll(buildDisconnectedBluetoothDevice()); 542 } else { 543 MediaDevice mutingExpectedDevice = getMutingExpectedDevice(); 544 if (mutingExpectedDevice != null) { 545 mMediaDevices.add(mutingExpectedDevice); 546 } 547 } 548 break; 549 } 550 } 551 } 552 553 final MediaDevice infoMediaDevice = mInfoMediaManager.getCurrentConnectedDevice(); 554 mCurrentConnectedDevice = infoMediaDevice != null 555 ? infoMediaDevice : updateCurrentConnectedDevice(); 556 dispatchDeviceListUpdate(); 557 if (mOnTransferBluetoothDevice != null && mOnTransferBluetoothDevice.isConnected()) { 558 connectDevice(mOnTransferBluetoothDevice); 559 mOnTransferBluetoothDevice.setState(MediaDeviceState.STATE_CONNECTED); 560 dispatchSelectedDeviceStateChanged(mOnTransferBluetoothDevice, 561 MediaDeviceState.STATE_CONNECTED); 562 mOnTransferBluetoothDevice = null; 563 } 564 } 565 isTv()566 private boolean isTv() { 567 PackageManager pm = mContext.getPackageManager(); 568 return pm.hasSystemFeature(PackageManager.FEATURE_TELEVISION) 569 || pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK); 570 } 571 getMutingExpectedDevice()572 private MediaDevice getMutingExpectedDevice() { 573 if (mBluetoothAdapter == null 574 || mAudioManager.getMutingExpectedDevice() == null) { 575 return null; 576 } 577 final List<BluetoothDevice> bluetoothDevices = 578 mBluetoothAdapter.getMostRecentlyConnectedDevices(); 579 final CachedBluetoothDeviceManager cachedDeviceManager = 580 mLocalBluetoothManager.getCachedDeviceManager(); 581 for (BluetoothDevice device : bluetoothDevices) { 582 final CachedBluetoothDevice cachedDevice = 583 cachedDeviceManager.findDevice(device); 584 if (isBondedMediaDevice(cachedDevice) && isMutingExpectedDevice(cachedDevice)) { 585 return new BluetoothMediaDevice(mContext, cachedDevice, null, /* item */ null); 586 } 587 } 588 return null; 589 } 590 isMutingExpectedDevice(CachedBluetoothDevice cachedDevice)591 private boolean isMutingExpectedDevice(CachedBluetoothDevice cachedDevice) { 592 AudioDeviceAttributes mutingExpectedDevice = mAudioManager.getMutingExpectedDevice(); 593 if (mutingExpectedDevice == null || cachedDevice == null) { 594 return false; 595 } 596 return cachedDevice.getAddress().equals(mutingExpectedDevice.getAddress()); 597 } 598 buildDisconnectedBluetoothDevice()599 private List<MediaDevice> buildDisconnectedBluetoothDevice() { 600 if (mBluetoothAdapter == null) { 601 Log.w(TAG, "buildDisconnectedBluetoothDevice() BluetoothAdapter is null"); 602 return new ArrayList<>(); 603 } 604 605 final List<BluetoothDevice> bluetoothDevices = 606 mBluetoothAdapter.getMostRecentlyConnectedDevices(); 607 final CachedBluetoothDeviceManager cachedDeviceManager = 608 mLocalBluetoothManager.getCachedDeviceManager(); 609 610 final List<CachedBluetoothDevice> cachedBluetoothDeviceList = new ArrayList<>(); 611 int deviceCount = 0; 612 for (BluetoothDevice device : bluetoothDevices) { 613 final CachedBluetoothDevice cachedDevice = 614 cachedDeviceManager.findDevice(device); 615 if (cachedDevice != null) { 616 if (cachedDevice.getBondState() == BluetoothDevice.BOND_BONDED 617 && !cachedDevice.isConnected() 618 && isMediaDevice(cachedDevice)) { 619 deviceCount++; 620 cachedBluetoothDeviceList.add(cachedDevice); 621 if (deviceCount >= MAX_DISCONNECTED_DEVICE_NUM) { 622 break; 623 } 624 } 625 } 626 } 627 628 unRegisterDeviceAttributeChangeCallback(); 629 mDisconnectedMediaDevices.clear(); 630 for (CachedBluetoothDevice cachedDevice : cachedBluetoothDeviceList) { 631 final MediaDevice mediaDevice = 632 new BluetoothMediaDevice(mContext, cachedDevice, null, /* item */ null); 633 if (!mMediaDevices.contains(mediaDevice)) { 634 cachedDevice.registerCallback(mDeviceAttributeChangeCallback); 635 mDisconnectedMediaDevices.add(mediaDevice); 636 } 637 } 638 return new ArrayList<>(mDisconnectedMediaDevices); 639 } 640 isBondedMediaDevice(CachedBluetoothDevice cachedDevice)641 private boolean isBondedMediaDevice(CachedBluetoothDevice cachedDevice) { 642 return cachedDevice != null 643 && cachedDevice.getBondState() == BluetoothDevice.BOND_BONDED 644 && !cachedDevice.isConnected() 645 && isMediaDevice(cachedDevice); 646 } 647 isMediaDevice(CachedBluetoothDevice device)648 private boolean isMediaDevice(CachedBluetoothDevice device) { 649 for (LocalBluetoothProfile profile : device.getUiAccessibleProfiles()) { 650 if (profile instanceof A2dpProfile || profile instanceof HearingAidProfile || 651 profile instanceof LeAudioProfile) { 652 return true; 653 } 654 } 655 return false; 656 } 657 658 @Override onDeviceListRemoved(@onNull List<MediaDevice> devices)659 public void onDeviceListRemoved(@NonNull List<MediaDevice> devices) { 660 synchronized (mMediaDevicesLock) { 661 mMediaDevices.removeAll(devices); 662 } 663 dispatchDeviceListUpdate(); 664 } 665 666 @Override onConnectedDeviceChanged(String id)667 public void onConnectedDeviceChanged(String id) { 668 MediaDevice connectDevice = getMediaDeviceById(id); 669 connectDevice = connectDevice != null 670 ? connectDevice : updateCurrentConnectedDevice(); 671 672 mCurrentConnectedDevice = connectDevice; 673 if (connectDevice != null) { 674 connectDevice.setState(MediaDeviceState.STATE_CONNECTED); 675 676 dispatchSelectedDeviceStateChanged(mCurrentConnectedDevice, 677 MediaDeviceState.STATE_CONNECTED); 678 } 679 } 680 681 @Override onRequestFailed(int reason)682 public void onRequestFailed(int reason) { 683 synchronized (mMediaDevicesLock) { 684 for (MediaDevice device : mMediaDevices) { 685 if (device.getState() == MediaDeviceState.STATE_CONNECTING) { 686 device.setState(MediaDeviceState.STATE_CONNECTING_FAILED); 687 } 688 } 689 } 690 dispatchOnRequestFailed(reason); 691 } 692 } 693 unRegisterDeviceAttributeChangeCallback()694 private void unRegisterDeviceAttributeChangeCallback() { 695 for (MediaDevice device : mDisconnectedMediaDevices) { 696 ((BluetoothMediaDevice) device).getCachedDevice() 697 .unregisterCallback(mDeviceAttributeChangeCallback); 698 } 699 } 700 701 /** 702 * Callback for notifying device information updating 703 */ 704 public interface DeviceCallback { 705 /** 706 * Callback for notifying device list updated. 707 * 708 * @param devices MediaDevice list 709 */ onDeviceListUpdate(List<MediaDevice> devices)710 default void onDeviceListUpdate(List<MediaDevice> devices) {}; 711 712 /** 713 * Callback for notifying the connected device is changed. 714 * 715 * @param device the changed connected MediaDevice 716 * @param state the current MediaDevice state, the possible values are: 717 * {@link MediaDeviceState#STATE_CONNECTED}, 718 * {@link MediaDeviceState#STATE_CONNECTING}, 719 * {@link MediaDeviceState#STATE_DISCONNECTED} 720 */ onSelectedDeviceStateChanged(MediaDevice device, @MediaDeviceState int state)721 default void onSelectedDeviceStateChanged(MediaDevice device, 722 @MediaDeviceState int state) {}; 723 724 /** 725 * Callback for notifying the device attributes is changed. 726 */ onDeviceAttributesChanged()727 default void onDeviceAttributesChanged() {}; 728 729 /** 730 * Callback for notifying that transferring is failed. 731 * 732 * @param reason the reason that the request has failed. Can be one of followings: 733 * {@link android.media.MediaRoute2ProviderService#REASON_UNKNOWN_ERROR}, 734 * {@link android.media.MediaRoute2ProviderService#REASON_REJECTED}, 735 * {@link android.media.MediaRoute2ProviderService#REASON_NETWORK_ERROR}, 736 * {@link android.media.MediaRoute2ProviderService#REASON_ROUTE_NOT_AVAILABLE}, 737 * {@link android.media.MediaRoute2ProviderService#REASON_INVALID_COMMAND}, 738 */ onRequestFailed(int reason)739 default void onRequestFailed(int reason){}; 740 741 /** 742 * Callback for notifying that we have a new about-to-connect device. 743 * 744 * An about-to-connect device is a device that is not yet connected but is expected to 745 * connect imminently and should be displayed as the current device in the media player. 746 * See [AudioManager.muteAwaitConnection] for more details. 747 * 748 * The information in the most recent callback should override information from any previous 749 * callbacks. 750 * 751 * @param deviceAddress the address of the device. {@see AudioDeviceAttributes.address}. 752 * If present, we'll use this address to fetch the full information 753 * about the device (if we can find that information). 754 * @param deviceName the name of the device (displayed to the user). Used as a backup in 755 * case using deviceAddress doesn't work. 756 * @param deviceIcon the icon that should be used with the device. Used as a backup in case 757 * using deviceAddress doesn't work. 758 */ onAboutToConnectDeviceAdded( @onNull String deviceAddress, @NonNull String deviceName, @Nullable Drawable deviceIcon )759 default void onAboutToConnectDeviceAdded( 760 @NonNull String deviceAddress, 761 @NonNull String deviceName, 762 @Nullable Drawable deviceIcon 763 ) {} 764 765 /** 766 * Callback for notifying that we no longer have an about-to-connect device. 767 */ onAboutToConnectDeviceRemoved()768 default void onAboutToConnectDeviceRemoved() {} 769 } 770 771 /** 772 * This callback is for update {@link BluetoothMediaDevice} summary when 773 * {@link CachedBluetoothDevice} connection state is changed. 774 */ 775 @VisibleForTesting 776 class DeviceAttributeChangeCallback implements CachedBluetoothDevice.Callback { 777 778 @Override onDeviceAttributesChanged()779 public void onDeviceAttributesChanged() { 780 if (mOnTransferBluetoothDevice != null 781 && !((BluetoothMediaDevice) mOnTransferBluetoothDevice).getCachedDevice() 782 .isBusy() 783 && !mOnTransferBluetoothDevice.isConnected()) { 784 // Failed to connect 785 mOnTransferBluetoothDevice.setState(MediaDeviceState.STATE_CONNECTING_FAILED); 786 mOnTransferBluetoothDevice = null; 787 dispatchOnRequestFailed(REASON_UNKNOWN_ERROR); 788 } 789 dispatchDeviceAttributesChanged(); 790 } 791 } 792 } 793