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