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 * Get the MediaDevice list that can be removed from current media session. 356 * 357 * @return list of MediaDevice 358 */ getDeselectableMediaDevice()359 public List<MediaDevice> getDeselectableMediaDevice() { 360 return mInfoMediaManager.getDeselectableMediaDevices(); 361 } 362 363 /** 364 * Release session to stop playing media on MediaDevice. 365 */ releaseSession()366 public boolean releaseSession() { 367 return mInfoMediaManager.releaseSession(); 368 } 369 370 /** 371 * Get the MediaDevice list that has been selected to current media. 372 * 373 * @return list of MediaDevice 374 */ getSelectedMediaDevice()375 public List<MediaDevice> getSelectedMediaDevice() { 376 return mInfoMediaManager.getSelectedMediaDevices(); 377 } 378 379 /** 380 * Requests a volume change for a specific media device. 381 * 382 * This operation is different from {@link #adjustSessionVolume(String, int)}, which changes the 383 * volume of the overall session. 384 */ adjustDeviceVolume(MediaDevice device, int volume)385 public void adjustDeviceVolume(MediaDevice device, int volume) { 386 mInfoMediaManager.adjustDeviceVolume(device, volume); 387 } 388 389 /** 390 * Adjust the volume of session. 391 * 392 * @param sessionId the value of media session id 393 * @param volume the value of volume 394 */ adjustSessionVolume(String sessionId, int volume)395 public void adjustSessionVolume(String sessionId, int volume) { 396 RoutingSessionInfo session = mInfoMediaManager.getRoutingSessionById(sessionId); 397 if (session != null) { 398 mInfoMediaManager.adjustSessionVolume(session, volume); 399 } else { 400 Log.w(TAG, "adjustSessionVolume: Unable to find session: " + sessionId); 401 } 402 } 403 404 /** 405 * Adjust the volume of session. 406 * 407 * @param volume the value of volume 408 */ adjustSessionVolume(int volume)409 public void adjustSessionVolume(int volume) { 410 mInfoMediaManager.adjustSessionVolume(volume); 411 } 412 413 /** 414 * Gets the maximum volume of the {@link android.media.RoutingSessionInfo}. 415 * 416 * @return maximum volume of the session, and return -1 if not found. 417 */ getSessionVolumeMax()418 public int getSessionVolumeMax() { 419 return mInfoMediaManager.getSessionVolumeMax(); 420 } 421 422 /** 423 * Gets the current volume of the {@link android.media.RoutingSessionInfo}. 424 * 425 * @return current volume of the session, and return -1 if not found. 426 */ getSessionVolume()427 public int getSessionVolume() { 428 return mInfoMediaManager.getSessionVolume(); 429 } 430 431 /** 432 * Gets the user-visible name of the {@link android.media.RoutingSessionInfo}. 433 * 434 * @return current name of the session, and return {@code null} if not found. 435 */ getSessionName()436 public CharSequence getSessionName() { 437 return mInfoMediaManager.getSessionName(); 438 } 439 440 /** 441 * Gets the list of remote {@link RoutingSessionInfo routing sessions} known to the system. 442 * 443 * <p>This list does not include any system routing sessions. 444 */ getRemoteRoutingSessions()445 public List<RoutingSessionInfo> getRemoteRoutingSessions() { 446 return mInfoMediaManager.getRemoteSessions(); 447 } 448 449 /** 450 * Gets the current package name. 451 * 452 * @return current package name 453 */ getPackageName()454 public String getPackageName() { 455 return mPackageName; 456 } 457 458 /** 459 * Returns {@code true} if needed to enable volume seekbar, otherwise returns {@code false}. 460 */ shouldEnableVolumeSeekBar(RoutingSessionInfo sessionInfo)461 public boolean shouldEnableVolumeSeekBar(RoutingSessionInfo sessionInfo) { 462 return mInfoMediaManager.shouldEnableVolumeSeekBar(sessionInfo); 463 } 464 465 @VisibleForTesting updateCurrentConnectedDevice()466 MediaDevice updateCurrentConnectedDevice() { 467 MediaDevice connectedDevice = null; 468 synchronized (mMediaDevicesLock) { 469 for (MediaDevice device : mMediaDevices) { 470 if (device instanceof BluetoothMediaDevice) { 471 if (isActiveDevice(((BluetoothMediaDevice) device).getCachedDevice()) 472 && device.isConnected()) { 473 return device; 474 } 475 } else if (device instanceof PhoneMediaDevice) { 476 connectedDevice = device; 477 } 478 } 479 } 480 481 return connectedDevice; 482 } 483 isActiveDevice(CachedBluetoothDevice device)484 private boolean isActiveDevice(CachedBluetoothDevice device) { 485 boolean isActiveDeviceA2dp = false; 486 boolean isActiveDeviceHearingAid = false; 487 boolean isActiveLeAudio = false; 488 final A2dpProfile a2dpProfile = mLocalBluetoothManager.getProfileManager().getA2dpProfile(); 489 if (a2dpProfile != null) { 490 isActiveDeviceA2dp = device.getDevice().equals(a2dpProfile.getActiveDevice()); 491 } 492 if (!isActiveDeviceA2dp) { 493 final HearingAidProfile hearingAidProfile = mLocalBluetoothManager.getProfileManager() 494 .getHearingAidProfile(); 495 if (hearingAidProfile != null) { 496 isActiveDeviceHearingAid = 497 hearingAidProfile.getActiveDevices().contains(device.getDevice()); 498 } 499 } 500 501 if (!isActiveDeviceA2dp && !isActiveDeviceHearingAid) { 502 final LeAudioProfile leAudioProfile = mLocalBluetoothManager.getProfileManager() 503 .getLeAudioProfile(); 504 if (leAudioProfile != null) { 505 isActiveLeAudio = leAudioProfile.getActiveDevices().contains(device.getDevice()); 506 } 507 } 508 509 return isActiveDeviceA2dp || isActiveDeviceHearingAid || isActiveLeAudio; 510 } 511 getCallbacks()512 private Collection<DeviceCallback> getCallbacks() { 513 return new CopyOnWriteArrayList<>(mCallbacks); 514 } 515 516 class MediaDeviceCallback implements InfoMediaManager.MediaDeviceCallback { 517 @Override onDeviceListAdded(@onNull List<MediaDevice> devices)518 public void onDeviceListAdded(@NonNull List<MediaDevice> devices) { 519 synchronized (mMediaDevicesLock) { 520 mMediaDevices.clear(); 521 mMediaDevices.addAll(devices); 522 // Add muting expected bluetooth devices only when phone output device is available. 523 for (MediaDevice device : devices) { 524 final int type = device.getDeviceType(); 525 if (type == MediaDevice.MediaDeviceType.TYPE_USB_C_AUDIO_DEVICE 526 || type == MediaDevice.MediaDeviceType.TYPE_3POINT5_MM_AUDIO_DEVICE 527 || type == MediaDevice.MediaDeviceType.TYPE_PHONE_DEVICE) { 528 if (isTv()) { 529 mMediaDevices.addAll(buildDisconnectedBluetoothDevice()); 530 } else { 531 MediaDevice mutingExpectedDevice = getMutingExpectedDevice(); 532 if (mutingExpectedDevice != null) { 533 mMediaDevices.add(mutingExpectedDevice); 534 } 535 } 536 break; 537 } 538 } 539 } 540 541 final MediaDevice infoMediaDevice = mInfoMediaManager.getCurrentConnectedDevice(); 542 mCurrentConnectedDevice = infoMediaDevice != null 543 ? infoMediaDevice : updateCurrentConnectedDevice(); 544 dispatchDeviceListUpdate(); 545 if (mOnTransferBluetoothDevice != null && mOnTransferBluetoothDevice.isConnected()) { 546 connectDevice(mOnTransferBluetoothDevice); 547 mOnTransferBluetoothDevice.setState(MediaDeviceState.STATE_CONNECTED); 548 dispatchSelectedDeviceStateChanged(mOnTransferBluetoothDevice, 549 MediaDeviceState.STATE_CONNECTED); 550 mOnTransferBluetoothDevice = null; 551 } 552 } 553 isTv()554 private boolean isTv() { 555 PackageManager pm = mContext.getPackageManager(); 556 return pm.hasSystemFeature(PackageManager.FEATURE_TELEVISION) 557 || pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK); 558 } 559 getMutingExpectedDevice()560 private MediaDevice getMutingExpectedDevice() { 561 if (mBluetoothAdapter == null 562 || mAudioManager.getMutingExpectedDevice() == null) { 563 return null; 564 } 565 final List<BluetoothDevice> bluetoothDevices = 566 mBluetoothAdapter.getMostRecentlyConnectedDevices(); 567 final CachedBluetoothDeviceManager cachedDeviceManager = 568 mLocalBluetoothManager.getCachedDeviceManager(); 569 for (BluetoothDevice device : bluetoothDevices) { 570 final CachedBluetoothDevice cachedDevice = 571 cachedDeviceManager.findDevice(device); 572 if (isBondedMediaDevice(cachedDevice) && isMutingExpectedDevice(cachedDevice)) { 573 return new BluetoothMediaDevice(mContext, cachedDevice, null, /* item */ null); 574 } 575 } 576 return null; 577 } 578 isMutingExpectedDevice(CachedBluetoothDevice cachedDevice)579 private boolean isMutingExpectedDevice(CachedBluetoothDevice cachedDevice) { 580 AudioDeviceAttributes mutingExpectedDevice = mAudioManager.getMutingExpectedDevice(); 581 if (mutingExpectedDevice == null || cachedDevice == null) { 582 return false; 583 } 584 return cachedDevice.getAddress().equals(mutingExpectedDevice.getAddress()); 585 } 586 buildDisconnectedBluetoothDevice()587 private List<MediaDevice> buildDisconnectedBluetoothDevice() { 588 if (mBluetoothAdapter == null) { 589 Log.w(TAG, "buildDisconnectedBluetoothDevice() BluetoothAdapter is null"); 590 return new ArrayList<>(); 591 } 592 593 final List<BluetoothDevice> bluetoothDevices = 594 mBluetoothAdapter.getMostRecentlyConnectedDevices(); 595 final CachedBluetoothDeviceManager cachedDeviceManager = 596 mLocalBluetoothManager.getCachedDeviceManager(); 597 598 final List<CachedBluetoothDevice> cachedBluetoothDeviceList = new ArrayList<>(); 599 int deviceCount = 0; 600 for (BluetoothDevice device : bluetoothDevices) { 601 final CachedBluetoothDevice cachedDevice = 602 cachedDeviceManager.findDevice(device); 603 if (cachedDevice != null) { 604 if (cachedDevice.getBondState() == BluetoothDevice.BOND_BONDED 605 && !cachedDevice.isConnected() 606 && isMediaDevice(cachedDevice)) { 607 deviceCount++; 608 cachedBluetoothDeviceList.add(cachedDevice); 609 if (deviceCount >= MAX_DISCONNECTED_DEVICE_NUM) { 610 break; 611 } 612 } 613 } 614 } 615 616 unRegisterDeviceAttributeChangeCallback(); 617 mDisconnectedMediaDevices.clear(); 618 for (CachedBluetoothDevice cachedDevice : cachedBluetoothDeviceList) { 619 final MediaDevice mediaDevice = 620 new BluetoothMediaDevice(mContext, cachedDevice, null, /* item */ null); 621 if (!mMediaDevices.contains(mediaDevice)) { 622 cachedDevice.registerCallback(mDeviceAttributeChangeCallback); 623 mDisconnectedMediaDevices.add(mediaDevice); 624 } 625 } 626 return new ArrayList<>(mDisconnectedMediaDevices); 627 } 628 isBondedMediaDevice(CachedBluetoothDevice cachedDevice)629 private boolean isBondedMediaDevice(CachedBluetoothDevice cachedDevice) { 630 return cachedDevice != null 631 && cachedDevice.getBondState() == BluetoothDevice.BOND_BONDED 632 && !cachedDevice.isConnected() 633 && isMediaDevice(cachedDevice); 634 } 635 isMediaDevice(CachedBluetoothDevice device)636 private boolean isMediaDevice(CachedBluetoothDevice device) { 637 for (LocalBluetoothProfile profile : device.getConnectableProfiles()) { 638 if (profile instanceof A2dpProfile || profile instanceof HearingAidProfile || 639 profile instanceof LeAudioProfile) { 640 return true; 641 } 642 } 643 return false; 644 } 645 646 @Override onDeviceListRemoved(@onNull List<MediaDevice> devices)647 public void onDeviceListRemoved(@NonNull List<MediaDevice> devices) { 648 synchronized (mMediaDevicesLock) { 649 mMediaDevices.removeAll(devices); 650 } 651 dispatchDeviceListUpdate(); 652 } 653 654 @Override onConnectedDeviceChanged(String id)655 public void onConnectedDeviceChanged(String id) { 656 MediaDevice connectDevice = getMediaDeviceById(id); 657 connectDevice = connectDevice != null 658 ? connectDevice : updateCurrentConnectedDevice(); 659 660 mCurrentConnectedDevice = connectDevice; 661 if (connectDevice != null) { 662 connectDevice.setState(MediaDeviceState.STATE_CONNECTED); 663 664 dispatchSelectedDeviceStateChanged(mCurrentConnectedDevice, 665 MediaDeviceState.STATE_CONNECTED); 666 } 667 } 668 669 @Override onRequestFailed(int reason)670 public void onRequestFailed(int reason) { 671 synchronized (mMediaDevicesLock) { 672 for (MediaDevice device : mMediaDevices) { 673 if (device.getState() == MediaDeviceState.STATE_CONNECTING) { 674 device.setState(MediaDeviceState.STATE_CONNECTING_FAILED); 675 } 676 } 677 } 678 dispatchOnRequestFailed(reason); 679 } 680 } 681 unRegisterDeviceAttributeChangeCallback()682 private void unRegisterDeviceAttributeChangeCallback() { 683 for (MediaDevice device : mDisconnectedMediaDevices) { 684 ((BluetoothMediaDevice) device).getCachedDevice() 685 .unregisterCallback(mDeviceAttributeChangeCallback); 686 } 687 } 688 689 /** 690 * Callback for notifying device information updating 691 */ 692 public interface DeviceCallback { 693 /** 694 * Callback for notifying device list updated. 695 * 696 * @param devices MediaDevice list 697 */ onDeviceListUpdate(List<MediaDevice> devices)698 default void onDeviceListUpdate(List<MediaDevice> devices) {}; 699 700 /** 701 * Callback for notifying the connected device is changed. 702 * 703 * @param device the changed connected MediaDevice 704 * @param state the current MediaDevice state, the possible values are: 705 * {@link MediaDeviceState#STATE_CONNECTED}, 706 * {@link MediaDeviceState#STATE_CONNECTING}, 707 * {@link MediaDeviceState#STATE_DISCONNECTED} 708 */ onSelectedDeviceStateChanged(MediaDevice device, @MediaDeviceState int state)709 default void onSelectedDeviceStateChanged(MediaDevice device, 710 @MediaDeviceState int state) {}; 711 712 /** 713 * Callback for notifying the device attributes is changed. 714 */ onDeviceAttributesChanged()715 default void onDeviceAttributesChanged() {}; 716 717 /** 718 * Callback for notifying that transferring is failed. 719 * 720 * @param reason the reason that the request has failed. Can be one of followings: 721 * {@link android.media.MediaRoute2ProviderService#REASON_UNKNOWN_ERROR}, 722 * {@link android.media.MediaRoute2ProviderService#REASON_REJECTED}, 723 * {@link android.media.MediaRoute2ProviderService#REASON_NETWORK_ERROR}, 724 * {@link android.media.MediaRoute2ProviderService#REASON_ROUTE_NOT_AVAILABLE}, 725 * {@link android.media.MediaRoute2ProviderService#REASON_INVALID_COMMAND}, 726 */ onRequestFailed(int reason)727 default void onRequestFailed(int reason){}; 728 729 /** 730 * Callback for notifying that we have a new about-to-connect device. 731 * 732 * An about-to-connect device is a device that is not yet connected but is expected to 733 * connect imminently and should be displayed as the current device in the media player. 734 * See [AudioManager.muteAwaitConnection] for more details. 735 * 736 * The information in the most recent callback should override information from any previous 737 * callbacks. 738 * 739 * @param deviceAddress the address of the device. {@see AudioDeviceAttributes.address}. 740 * If present, we'll use this address to fetch the full information 741 * about the device (if we can find that information). 742 * @param deviceName the name of the device (displayed to the user). Used as a backup in 743 * case using deviceAddress doesn't work. 744 * @param deviceIcon the icon that should be used with the device. Used as a backup in case 745 * using deviceAddress doesn't work. 746 */ onAboutToConnectDeviceAdded( @onNull String deviceAddress, @NonNull String deviceName, @Nullable Drawable deviceIcon )747 default void onAboutToConnectDeviceAdded( 748 @NonNull String deviceAddress, 749 @NonNull String deviceName, 750 @Nullable Drawable deviceIcon 751 ) {} 752 753 /** 754 * Callback for notifying that we no longer have an about-to-connect device. 755 */ onAboutToConnectDeviceRemoved()756 default void onAboutToConnectDeviceRemoved() {} 757 } 758 759 /** 760 * This callback is for update {@link BluetoothMediaDevice} summary when 761 * {@link CachedBluetoothDevice} connection state is changed. 762 */ 763 @VisibleForTesting 764 class DeviceAttributeChangeCallback implements CachedBluetoothDevice.Callback { 765 766 @Override onDeviceAttributesChanged()767 public void onDeviceAttributesChanged() { 768 if (mOnTransferBluetoothDevice != null 769 && !((BluetoothMediaDevice) mOnTransferBluetoothDevice).getCachedDevice() 770 .isBusy() 771 && !mOnTransferBluetoothDevice.isConnected()) { 772 // Failed to connect 773 mOnTransferBluetoothDevice.setState(MediaDeviceState.STATE_CONNECTING_FAILED); 774 mOnTransferBluetoothDevice = null; 775 dispatchOnRequestFailed(REASON_UNKNOWN_ERROR); 776 } 777 dispatchDeviceAttributesChanged(); 778 } 779 } 780 } 781