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