1 /* 2 * Copyright (C) 2008 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 17 package com.android.settingslib.bluetooth; 18 19 import static com.android.settingslib.media.flags.Flags.enableTvMediaOutputDialog; 20 21 import android.annotation.CallbackExecutor; 22 import android.annotation.StringRes; 23 import android.bluetooth.BluetoothAdapter; 24 import android.bluetooth.BluetoothClass; 25 import android.bluetooth.BluetoothCsipSetCoordinator; 26 import android.bluetooth.BluetoothDevice; 27 import android.bluetooth.BluetoothHearingAid; 28 import android.bluetooth.BluetoothLeBroadcastReceiveState; 29 import android.bluetooth.BluetoothProfile; 30 import android.bluetooth.BluetoothUuid; 31 import android.content.Context; 32 import android.content.SharedPreferences; 33 import android.content.res.Resources; 34 import android.graphics.drawable.BitmapDrawable; 35 import android.graphics.drawable.Drawable; 36 import android.net.Uri; 37 import android.os.Handler; 38 import android.os.Looper; 39 import android.os.Message; 40 import android.os.ParcelUuid; 41 import android.os.SystemClock; 42 import android.text.SpannableStringBuilder; 43 import android.text.TextUtils; 44 import android.text.style.ForegroundColorSpan; 45 import android.util.Log; 46 import android.util.LruCache; 47 import android.util.Pair; 48 import android.view.InputDevice; 49 50 import androidx.annotation.NonNull; 51 import androidx.annotation.Nullable; 52 import androidx.annotation.VisibleForTesting; 53 import androidx.annotation.WorkerThread; 54 55 import com.android.internal.util.ArrayUtils; 56 import com.android.settingslib.R; 57 import com.android.settingslib.Utils; 58 import com.android.settingslib.flags.Flags; 59 import com.android.settingslib.utils.ThreadUtils; 60 import com.android.settingslib.widget.AdaptiveOutlineDrawable; 61 62 import com.google.common.collect.ImmutableSet; 63 import com.google.common.util.concurrent.FutureCallback; 64 import com.google.common.util.concurrent.Futures; 65 import com.google.common.util.concurrent.ListenableFuture; 66 67 import java.sql.Timestamp; 68 import java.util.ArrayList; 69 import java.util.Arrays; 70 import java.util.Collection; 71 import java.util.HashSet; 72 import java.util.List; 73 import java.util.Map; 74 import java.util.Objects; 75 import java.util.Optional; 76 import java.util.Set; 77 import java.util.concurrent.ConcurrentHashMap; 78 import java.util.concurrent.CopyOnWriteArrayList; 79 import java.util.concurrent.Executor; 80 import java.util.stream.Collectors; 81 import java.util.stream.IntStream; 82 import java.util.stream.Stream; 83 84 /** 85 * CachedBluetoothDevice represents a remote Bluetooth device. It contains 86 * attributes of the device (such as the address, name, RSSI, etc.) and 87 * functionality that can be performed on the device (connect, pair, disconnect, 88 * etc.). 89 */ 90 public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> { 91 private static final String TAG = "CachedBluetoothDevice"; 92 private static final ParcelUuid ANDROID_AUTO_UUID = 93 ParcelUuid.fromString("4de17a00-52cb-11e6-bdf4-0800200c9a66"); 94 95 // See mConnectAttempted 96 private static final long MAX_UUID_DELAY_FOR_AUTO_CONNECT = 5000; 97 // Some Hearing Aids (especially the 2nd device) needs more time to do service discovery 98 private static final long MAX_HEARING_AIDS_DELAY_FOR_AUTO_CONNECT = 15000; 99 private static final long MAX_HOGP_DELAY_FOR_AUTO_CONNECT = 30000; 100 private static final long MAX_LEAUDIO_DELAY_FOR_AUTO_CONNECT = 30000; 101 private static final long MAX_MEDIA_PROFILE_CONNECT_DELAY = 60000; 102 103 private static final int DEFAULT_LOW_BATTERY_THRESHOLD = 20; 104 105 // To be used instead of a resource id to indicate that low battery states should not be 106 // changed to a different color. 107 private static final int SUMMARY_NO_COLOR_FOR_LOW_BATTERY = 0; 108 109 private final Context mContext; 110 private final BluetoothAdapter mLocalAdapter; 111 private final LocalBluetoothProfileManager mProfileManager; 112 private final Object mProfileLock = new Object(); 113 BluetoothDevice mDevice; 114 private HearingAidInfo mHearingAidInfo; 115 private int mGroupId; 116 private Timestamp mBondTimestamp; 117 private LocalBluetoothManager mBluetoothManager; 118 119 // Need this since there is no method for getting RSSI 120 short mRssi; 121 122 // mProfiles and mRemovedProfiles does not do swap() between main and sub device. It is 123 // because current sub device is only for HearingAid and its profile is the same. 124 private final Collection<LocalBluetoothProfile> mProfiles = new CopyOnWriteArrayList<>(); 125 126 // List of profiles that were previously in mProfiles, but have been removed 127 private final Collection<LocalBluetoothProfile> mRemovedProfiles = new CopyOnWriteArrayList<>(); 128 129 // Device supports PANU but not NAP: remove PanProfile after device disconnects from NAP 130 private boolean mLocalNapRoleConnected; 131 132 boolean mJustDiscovered; 133 134 boolean mIsCoordinatedSetMember = false; 135 136 private final Collection<Callback> mCallbacks = new CopyOnWriteArrayList<>(); 137 138 private final Map<Callback, Executor> mCallbackExecutorMap = new ConcurrentHashMap<>(); 139 140 /** 141 * Last time a bt profile auto-connect was attempted. 142 * If an ACTION_UUID intent comes in within 143 * MAX_UUID_DELAY_FOR_AUTO_CONNECT milliseconds, we will try auto-connect 144 * again with the new UUIDs 145 * The value is reset if a manual disconnection happens. 146 */ 147 private long mConnectAttempted = -1; 148 149 // Active device state 150 private boolean mIsActiveDeviceA2dp = false; 151 private boolean mIsActiveDeviceHeadset = false; 152 private boolean mIsActiveDeviceHearingAid = false; 153 private boolean mIsActiveDeviceLeAudio = false; 154 // Media profile connect state 155 private boolean mIsA2dpProfileConnectedFail = false; 156 private boolean mIsHeadsetProfileConnectedFail = false; 157 private boolean mIsHearingAidProfileConnectedFail = false; 158 private boolean mIsLeAudioProfileConnectedFail = false; 159 private boolean mUnpairing; 160 @Nullable 161 private InputDevice mInputDevice; 162 private boolean mIsDeviceStylus; 163 164 // Group second device for Hearing Aid 165 private CachedBluetoothDevice mSubDevice; 166 // Group member devices for the coordinated set 167 private Set<CachedBluetoothDevice> mMemberDevices = new HashSet<CachedBluetoothDevice>(); 168 @VisibleForTesting 169 LruCache<String, BitmapDrawable> mDrawableCache; 170 171 private final Handler mHandler = new Handler(Looper.getMainLooper()) { 172 @Override 173 public void handleMessage(Message msg) { 174 switch (msg.what) { 175 case BluetoothProfile.A2DP: 176 mIsA2dpProfileConnectedFail = true; 177 break; 178 case BluetoothProfile.HEADSET: 179 mIsHeadsetProfileConnectedFail = true; 180 break; 181 case BluetoothProfile.HEARING_AID: 182 mIsHearingAidProfileConnectedFail = true; 183 break; 184 case BluetoothProfile.LE_AUDIO: 185 mIsLeAudioProfileConnectedFail = true; 186 break; 187 default: 188 Log.w(TAG, "handleMessage(): unknown message : " + msg.what); 189 break; 190 } 191 Log.w(TAG, "Connect to profile : " + msg.what + " timeout, show error message !"); 192 refresh(); 193 } 194 }; 195 CachedBluetoothDevice(Context context, LocalBluetoothProfileManager profileManager, BluetoothDevice device)196 CachedBluetoothDevice(Context context, LocalBluetoothProfileManager profileManager, 197 BluetoothDevice device) { 198 mContext = context; 199 mLocalAdapter = BluetoothAdapter.getDefaultAdapter(); 200 mProfileManager = profileManager; 201 mDevice = device; 202 fillData(); 203 mGroupId = BluetoothCsipSetCoordinator.GROUP_ID_INVALID; 204 initDrawableCache(); 205 mUnpairing = false; 206 mInputDevice = BluetoothUtils.getInputDevice(mContext, getAddress()); 207 mIsDeviceStylus = BluetoothUtils.isDeviceStylus(mInputDevice, this); 208 } 209 210 /** Clears any pending messages in the message queue. */ release()211 public void release() { 212 mHandler.removeCallbacksAndMessages(null); 213 } 214 initDrawableCache()215 private void initDrawableCache() { 216 int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); 217 int cacheSize = maxMemory / 8; 218 219 mDrawableCache = new LruCache<String, BitmapDrawable>(cacheSize) { 220 @Override 221 protected int sizeOf(String key, BitmapDrawable bitmap) { 222 return bitmap.getBitmap().getByteCount() / 1024; 223 } 224 }; 225 } 226 227 /** 228 * Describes the current device and profile for logging. 229 * 230 * @param profile Profile to describe 231 * @return Description of the device and profile 232 */ describe(LocalBluetoothProfile profile)233 private String describe(LocalBluetoothProfile profile) { 234 StringBuilder sb = new StringBuilder(); 235 sb.append("Address:").append(mDevice); 236 if (profile != null) { 237 sb.append(" Profile:").append(profile); 238 } 239 240 return sb.toString(); 241 } 242 onProfileStateChanged(LocalBluetoothProfile profile, int newProfileState)243 void onProfileStateChanged(LocalBluetoothProfile profile, int newProfileState) { 244 if (BluetoothUtils.D) { 245 Log.d(TAG, "onProfileStateChanged: profile " + profile + ", device " 246 + mDevice.getAnonymizedAddress() + ", newProfileState " + newProfileState); 247 } 248 if (mLocalAdapter.getState() == BluetoothAdapter.STATE_TURNING_OFF) 249 { 250 if (BluetoothUtils.D) { 251 Log.d(TAG, " BT Turninig Off...Profile conn state change ignored..."); 252 } 253 return; 254 } 255 256 synchronized (mProfileLock) { 257 if (profile instanceof A2dpProfile || profile instanceof HeadsetProfile 258 || profile instanceof HearingAidProfile || profile instanceof LeAudioProfile) { 259 setProfileConnectedStatus(profile.getProfileId(), false); 260 switch (newProfileState) { 261 case BluetoothProfile.STATE_CONNECTED: 262 mHandler.removeMessages(profile.getProfileId()); 263 break; 264 case BluetoothProfile.STATE_CONNECTING: 265 mHandler.sendEmptyMessageDelayed(profile.getProfileId(), 266 MAX_MEDIA_PROFILE_CONNECT_DELAY); 267 break; 268 case BluetoothProfile.STATE_DISCONNECTING: 269 if (mHandler.hasMessages(profile.getProfileId())) { 270 mHandler.removeMessages(profile.getProfileId()); 271 } 272 break; 273 case BluetoothProfile.STATE_DISCONNECTED: 274 if (mHandler.hasMessages(profile.getProfileId())) { 275 mHandler.removeMessages(profile.getProfileId()); 276 if (profile.getConnectionPolicy(mDevice) > 277 BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) { 278 if (Flags.ignoreA2dpDisconnectionForAndroidAuto() 279 && profile instanceof A2dpProfile && isAndroidAuto()) { 280 Log.w(TAG, 281 "onProfileStateChanged(): Skip setting A2DP " 282 + "connection fail for Android Auto"); 283 } else { 284 /* 285 * If we received state DISCONNECTED and previous state was 286 * CONNECTING and connection policy is FORBIDDEN or UNKNOWN 287 * then it's not really a failure to connect. 288 * 289 * Connection profile is considered as failed when connection 290 * policy indicates that profile should be connected 291 * but it got disconnected. 292 */ 293 Log.w(TAG, 294 "onProfileStateChanged(): Failed to connect profile"); 295 setProfileConnectedStatus(profile.getProfileId(), true); 296 } 297 } 298 } 299 break; 300 default: 301 Log.w(TAG, "onProfileStateChanged(): unknown profile state : " 302 + newProfileState); 303 break; 304 } 305 } 306 307 if (newProfileState == BluetoothProfile.STATE_CONNECTED) { 308 if (profile instanceof MapProfile) { 309 profile.setEnabled(mDevice, true); 310 } 311 if (!mProfiles.contains(profile)) { 312 mRemovedProfiles.remove(profile); 313 mProfiles.add(profile); 314 if (profile instanceof PanProfile 315 && ((PanProfile) profile).isLocalRoleNap(mDevice)) { 316 // Device doesn't support NAP, so remove PanProfile on disconnect 317 mLocalNapRoleConnected = true; 318 } 319 } 320 if (profile instanceof HidProfile) { 321 updatePreferredTransport(); 322 } 323 } else if (profile instanceof MapProfile 324 && newProfileState == BluetoothProfile.STATE_DISCONNECTED) { 325 profile.setEnabled(mDevice, false); 326 } else if (mLocalNapRoleConnected && profile instanceof PanProfile 327 && ((PanProfile) profile).isLocalRoleNap(mDevice) 328 && newProfileState == BluetoothProfile.STATE_DISCONNECTED) { 329 Log.d(TAG, "Removing PanProfile from device after NAP disconnect"); 330 mProfiles.remove(profile); 331 mRemovedProfiles.add(profile); 332 mLocalNapRoleConnected = false; 333 } 334 335 if (profile instanceof LeAudioProfile) { 336 updatePreferredTransport(); 337 } 338 339 HearingAidStatsLogUtils.updateHistoryIfNeeded(mContext, this, profile, newProfileState); 340 } 341 342 fetchActiveDevices(); 343 } 344 updatePreferredTransport()345 private void updatePreferredTransport() { 346 LeAudioProfile leAudioProfile = 347 (LeAudioProfile) 348 mProfiles.stream() 349 .filter(p -> p instanceof LeAudioProfile) 350 .findFirst() 351 .orElse(null); 352 HidProfile hidProfile = 353 (HidProfile) 354 mProfiles.stream() 355 .filter(p -> p instanceof HidProfile) 356 .findFirst() 357 .orElse(null); 358 if (leAudioProfile == null || hidProfile == null) { 359 return; 360 } 361 // Both LeAudioProfile and HidProfile are connectable. 362 if (!hidProfile.setPreferredTransport( 363 mDevice, 364 leAudioProfile.isEnabled(mDevice) 365 ? BluetoothDevice.TRANSPORT_LE 366 : BluetoothDevice.TRANSPORT_BREDR)) { 367 Log.w(TAG, "Fail to set preferred transport"); 368 } 369 } 370 371 @VisibleForTesting setProfileConnectedStatus(int profileId, boolean isFailed)372 void setProfileConnectedStatus(int profileId, boolean isFailed) { 373 switch (profileId) { 374 case BluetoothProfile.A2DP: 375 mIsA2dpProfileConnectedFail = isFailed; 376 break; 377 case BluetoothProfile.HEADSET: 378 mIsHeadsetProfileConnectedFail = isFailed; 379 break; 380 case BluetoothProfile.HEARING_AID: 381 mIsHearingAidProfileConnectedFail = isFailed; 382 break; 383 case BluetoothProfile.LE_AUDIO: 384 mIsLeAudioProfileConnectedFail = isFailed; 385 break; 386 default: 387 Log.w(TAG, "setProfileConnectedStatus(): unknown profile id : " + profileId); 388 break; 389 } 390 } 391 disconnect()392 public void disconnect() { 393 mConnectAttempted = -1; 394 synchronized (mProfileLock) { 395 if (getGroupId() != BluetoothCsipSetCoordinator.GROUP_ID_INVALID) { 396 for (CachedBluetoothDevice member : getMemberDevice()) { 397 Log.d(TAG, "Disconnect the member:" + member); 398 member.disconnect(); 399 } 400 } 401 Log.d(TAG, "Disconnect " + this); 402 if (Flags.enableLeAudioSharing()) { 403 removeBroadcastSource(ImmutableSet.of(mDevice)); 404 } 405 mDevice.disconnect(); 406 } 407 // Disconnect PBAP server in case its connected 408 // This is to ensure all the profiles are disconnected as some CK/Hs do not 409 // disconnect PBAP connection when HF connection is brought down 410 PbapServerProfile PbapProfile = mProfileManager.getPbapProfile(); 411 if (PbapProfile != null && isConnectedProfile(PbapProfile)) 412 { 413 PbapProfile.setEnabled(mDevice, false); 414 } 415 } 416 disconnect(LocalBluetoothProfile profile)417 public void disconnect(LocalBluetoothProfile profile) { 418 if (profile.setEnabled(mDevice, false)) { 419 if (BluetoothUtils.D) { 420 Log.d(TAG, "Command sent successfully:DISCONNECT " + describe(profile)); 421 } 422 } 423 } 424 425 /** 426 * Connect this device. 427 * 428 * @param connectAllProfiles {@code true} to connect all profile, {@code false} otherwise. 429 * 430 * @deprecated use {@link #connect()} instead. 431 */ 432 @Deprecated connect(boolean connectAllProfiles)433 public void connect(boolean connectAllProfiles) { 434 connect(); 435 } 436 437 /** 438 * Connect this device. 439 */ connect()440 public void connect() { 441 if (!ensurePaired()) { 442 return; 443 } 444 445 mConnectAttempted = SystemClock.elapsedRealtime(); 446 connectDevice(); 447 } 448 setHearingAidInfo(HearingAidInfo hearingAidInfo)449 public void setHearingAidInfo(HearingAidInfo hearingAidInfo) { 450 mHearingAidInfo = hearingAidInfo; 451 dispatchAttributesChanged(); 452 } 453 getHearingAidInfo()454 public HearingAidInfo getHearingAidInfo() { 455 return mHearingAidInfo; 456 } 457 458 /** 459 * @return {@code true} if {@code cachedBluetoothDevice} is hearing aid device. 460 * @deprecated use {@link #isHearingDevice() } 461 * // TODO: b/385679160 - Target to deprecate it and replace with #isHearingDevice() 462 */ 463 @Deprecated isHearingAidDevice()464 public boolean isHearingAidDevice() { 465 return mHearingAidInfo != null; 466 } 467 468 /** 469 * @return {@code true} if {@code cachedBluetoothDevice} support any of hearing device profile. 470 */ isHearingDevice()471 public boolean isHearingDevice() { 472 return getProfiles().stream().anyMatch( 473 p -> (p instanceof HearingAidProfile || p instanceof HapClientProfile)); 474 } 475 getDeviceSide()476 public int getDeviceSide() { 477 return mHearingAidInfo != null 478 ? mHearingAidInfo.getSide() : HearingAidInfo.DeviceSide.SIDE_INVALID; 479 } 480 getDeviceMode()481 public int getDeviceMode() { 482 return mHearingAidInfo != null 483 ? mHearingAidInfo.getMode() : HearingAidInfo.DeviceMode.MODE_INVALID; 484 } 485 getHiSyncId()486 public long getHiSyncId() { 487 return mHearingAidInfo != null 488 ? mHearingAidInfo.getHiSyncId() : BluetoothHearingAid.HI_SYNC_ID_INVALID; 489 } 490 491 /** 492 * Mark the discovered device as member of coordinated set. 493 * 494 * @param isCoordinatedSetMember {@code true}, if the device is a member of a coordinated set. 495 */ setIsCoordinatedSetMember(boolean isCoordinatedSetMember)496 public void setIsCoordinatedSetMember(boolean isCoordinatedSetMember) { 497 mIsCoordinatedSetMember = isCoordinatedSetMember; 498 } 499 500 /** 501 * Check if the device is a CSIP member device. 502 * 503 * @return {@code true}, if this device supports CSIP, otherwise returns {@code false}. 504 */ isCoordinatedSetMemberDevice()505 public boolean isCoordinatedSetMemberDevice() { 506 return mIsCoordinatedSetMember; 507 } 508 509 /** 510 * Get the coordinated set group id. 511 * 512 * @return the group id. 513 */ getGroupId()514 public int getGroupId() { 515 return mGroupId; 516 } 517 518 /** 519 * Set the coordinated set group id. 520 * 521 * @param id the group id from the CSIP. 522 */ setGroupId(int id)523 public void setGroupId(int id) { 524 Log.d(TAG, this.getDevice().getAnonymizedAddress() + " set GroupId " + id); 525 mGroupId = id; 526 } 527 onBondingDockConnect()528 void onBondingDockConnect() { 529 // Attempt to connect if UUIDs are available. Otherwise, 530 // we will connect when the ACTION_UUID intent arrives. 531 connect(); 532 } 533 connectDevice()534 private void connectDevice() { 535 synchronized (mProfileLock) { 536 // Try to initialize the profiles if they were not. 537 if (mProfiles.isEmpty()) { 538 // if mProfiles is empty, then do not invoke updateProfiles. This causes a race 539 // condition with carkits during pairing, wherein RemoteDevice.UUIDs have been 540 // updated from bluetooth stack but ACTION.uuid is not sent yet. 541 // Eventually ACTION.uuid will be received which shall trigger the connection of the 542 // various profiles 543 // If UUIDs are not available yet, connect will be happen 544 // upon arrival of the ACTION_UUID intent. 545 Log.d(TAG, "No profiles. Maybe we will connect later for device " + mDevice); 546 return; 547 } 548 Log.d(TAG, "connect " + this); 549 mDevice.connect(); 550 if (getGroupId() != BluetoothCsipSetCoordinator.GROUP_ID_INVALID) { 551 for (CachedBluetoothDevice member : getMemberDevice()) { 552 Log.d(TAG, "connect the member:" + member); 553 member.connect(); 554 } 555 } 556 } 557 } 558 559 /** 560 * Connect this device to the specified profile. 561 * 562 * @param profile the profile to use with the remote device 563 */ connectProfile(LocalBluetoothProfile profile)564 public void connectProfile(LocalBluetoothProfile profile) { 565 mConnectAttempted = SystemClock.elapsedRealtime(); 566 connectInt(profile); 567 // Refresh the UI based on profile.connect() call 568 refresh(); 569 } 570 connectInt(LocalBluetoothProfile profile)571 synchronized void connectInt(LocalBluetoothProfile profile) { 572 if (!ensurePaired()) { 573 return; 574 } 575 if (profile.setEnabled(mDevice, true)) { 576 if (BluetoothUtils.D) { 577 Log.d(TAG, "Command sent successfully:CONNECT " + describe(profile)); 578 } 579 return; 580 } 581 Log.i(TAG, "Failed to connect " + profile.toString() + " to " + getName()); 582 } 583 ensurePaired()584 private boolean ensurePaired() { 585 if (getBondState() == BluetoothDevice.BOND_NONE) { 586 startPairing(); 587 return false; 588 } else { 589 return true; 590 } 591 } 592 startPairing()593 public boolean startPairing() { 594 // Pairing is unreliable while scanning, so cancel discovery 595 if (mLocalAdapter.isDiscovering()) { 596 mLocalAdapter.cancelDiscovery(); 597 } 598 599 if (!mDevice.createBond()) { 600 return false; 601 } 602 603 return true; 604 } 605 unpair()606 public void unpair() { 607 int state = getBondState(); 608 609 if (state == BluetoothDevice.BOND_BONDING) { 610 mDevice.cancelBondProcess(); 611 } 612 613 if (state != BluetoothDevice.BOND_NONE) { 614 final BluetoothDevice dev = mDevice; 615 if (dev != null) { 616 mUnpairing = true; 617 if (Flags.enableLeAudioSharing()) { 618 Set<BluetoothDevice> devicesToRemoveSource = new HashSet<>(); 619 devicesToRemoveSource.add(dev); 620 if (getGroupId() != BluetoothCsipSetCoordinator.GROUP_ID_INVALID) { 621 for (CachedBluetoothDevice member : getMemberDevice()) { 622 devicesToRemoveSource.add(member.getDevice()); 623 } 624 } 625 removeBroadcastSource(devicesToRemoveSource); 626 } 627 final boolean successful = dev.removeBond(); 628 if (successful) { 629 releaseLruCache(); 630 if (BluetoothUtils.D) { 631 Log.d(TAG, "Command sent successfully:REMOVE_BOND " + describe(null)); 632 } 633 } else if (BluetoothUtils.V) { 634 Log.v(TAG, "Framework rejected command immediately:REMOVE_BOND " + 635 describe(null)); 636 } 637 } 638 } 639 } 640 641 @WorkerThread removeBroadcastSource(Set<BluetoothDevice> devices)642 private void removeBroadcastSource(Set<BluetoothDevice> devices) { 643 if (mProfileManager == null || devices.isEmpty()) return; 644 LocalBluetoothLeBroadcast broadcast = mProfileManager.getLeAudioBroadcastProfile(); 645 LocalBluetoothLeBroadcastAssistant assistant = 646 mProfileManager.getLeAudioBroadcastAssistantProfile(); 647 if (broadcast != null && assistant != null && broadcast.isEnabled(null)) { 648 for (BluetoothDevice device : devices) { 649 for (BluetoothLeBroadcastReceiveState state : assistant.getAllSources(device)) { 650 if (BluetoothUtils.D) { 651 Log.d(TAG, "Remove broadcast source " + state.getBroadcastId() 652 + " from device " + device.getAnonymizedAddress()); 653 } 654 assistant.removeSource(device, state.getSourceId()); 655 } 656 } 657 } 658 } 659 getProfileConnectionState(LocalBluetoothProfile profile)660 public int getProfileConnectionState(LocalBluetoothProfile profile) { 661 return profile != null 662 ? profile.getConnectionStatus(mDevice) 663 : BluetoothProfile.STATE_DISCONNECTED; 664 } 665 666 // TODO: do any of these need to run async on a background thread? fillData()667 void fillData() { 668 updateProfiles(); 669 fetchActiveDevices(); 670 migratePhonebookPermissionChoice(); 671 migrateMessagePermissionChoice(); 672 673 dispatchAttributesChanged(); 674 } 675 getDevice()676 public BluetoothDevice getDevice() { 677 return mDevice; 678 } 679 680 /** 681 * Convenience method that can be mocked - it lets tests avoid having to call getDevice() which 682 * causes problems in tests since BluetoothDevice is final and cannot be mocked. 683 * @return the address of this device 684 */ getAddress()685 public String getAddress() { 686 return mDevice.getAddress(); 687 } 688 689 /** 690 * Get identity address from remote device 691 * @return {@link BluetoothDevice#getIdentityAddress()} if 692 * {@link BluetoothDevice#getIdentityAddress()} is not null otherwise return 693 * {@link BluetoothDevice#getAddress()} 694 */ getIdentityAddress()695 public String getIdentityAddress() { 696 final String identityAddress = mDevice.getIdentityAddress(); 697 return TextUtils.isEmpty(identityAddress) ? getAddress() : identityAddress; 698 } 699 700 /** 701 * Get name from remote device 702 * @return {@link BluetoothDevice#getAlias()} if 703 * {@link BluetoothDevice#getAlias()} is not null otherwise return 704 * {@link BluetoothDevice#getAddress()} 705 */ getName()706 public String getName() { 707 final String aliasName = mDevice.getAlias(); 708 return TextUtils.isEmpty(aliasName) ? getAddress() : aliasName; 709 } 710 711 /** 712 * User changes the device name 713 * @param name new alias name to be set, should never be null 714 */ setName(String name)715 public void setName(String name) { 716 // Prevent getName() to be set to null if setName(null) is called 717 if (TextUtils.isEmpty(name) || TextUtils.equals(name, getName())) { 718 return; 719 } 720 mDevice.setAlias(name); 721 dispatchAttributesChanged(); 722 723 for (CachedBluetoothDevice cbd : mMemberDevices) { 724 cbd.setName(name); 725 } 726 if (mSubDevice != null) { 727 mSubDevice.setName(name); 728 } 729 } 730 731 /** 732 * Set this device as active device 733 * @return true if at least one profile on this device is set to active, false otherwise 734 */ setActive()735 public boolean setActive() { 736 boolean result = false; 737 A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile(); 738 if (a2dpProfile != null && isConnectedProfile(a2dpProfile)) { 739 if (a2dpProfile.setActiveDevice(getDevice())) { 740 Log.i(TAG, "OnPreferenceClickListener: A2DP active device=" + this); 741 result = true; 742 } 743 } 744 HeadsetProfile headsetProfile = mProfileManager.getHeadsetProfile(); 745 if ((headsetProfile != null) && isConnectedProfile(headsetProfile)) { 746 if (headsetProfile.setActiveDevice(getDevice())) { 747 Log.i(TAG, "OnPreferenceClickListener: Headset active device=" + this); 748 result = true; 749 } 750 } 751 HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile(); 752 if ((hearingAidProfile != null) && isConnectedProfile(hearingAidProfile)) { 753 if (hearingAidProfile.setActiveDevice(getDevice())) { 754 Log.i(TAG, "OnPreferenceClickListener: Hearing Aid active device=" + this); 755 result = true; 756 } 757 } 758 LeAudioProfile leAudioProfile = mProfileManager.getLeAudioProfile(); 759 if ((leAudioProfile != null) && isConnectedProfile(leAudioProfile)) { 760 if (leAudioProfile.setActiveDevice(getDevice())) { 761 Log.i(TAG, "OnPreferenceClickListener: LeAudio active device=" + this); 762 result = true; 763 } 764 } 765 return result; 766 } 767 refreshName()768 void refreshName() { 769 if (BluetoothUtils.D) { 770 Log.d(TAG, "Device name: " + getName()); 771 } 772 dispatchAttributesChanged(); 773 } 774 775 /** 776 * Checks if device has a human readable name besides MAC address 777 * @return true if device's alias name is not null nor empty, false otherwise 778 */ hasHumanReadableName()779 public boolean hasHumanReadableName() { 780 return !TextUtils.isEmpty(mDevice.getAlias()); 781 } 782 783 /** 784 * Get battery level from remote device 785 * @return battery level in percentage [0-100], 786 * {@link BluetoothDevice#BATTERY_LEVEL_BLUETOOTH_OFF}, or 787 * {@link BluetoothDevice#BATTERY_LEVEL_UNKNOWN} 788 */ getBatteryLevel()789 public int getBatteryLevel() { 790 return mDevice.getBatteryLevel(); 791 } 792 793 /** 794 * Get the lowest battery level from remote device and its member devices 795 * @return battery level in percentage [0-100] or 796 * {@link BluetoothDevice#BATTERY_LEVEL_UNKNOWN} 797 */ getMinBatteryLevelWithMemberDevices()798 public int getMinBatteryLevelWithMemberDevices() { 799 return getMinBatteryLevels(Stream.concat(Stream.of(this), mMemberDevices.stream()) 800 .mapToInt(CachedBluetoothDevice::getBatteryLevel)); 801 } 802 803 /** 804 * Get the lowest battery level from remote device and its member devices if it's greater than 805 * BluetoothDevice.BATTERY_LEVEL_UNKNOWN. 806 * 807 * <p>Android framework should only set mBatteryLevel to valid range [0-100], 808 * BluetoothDevice.BATTERY_LEVEL_BLUETOOTH_OFF, or BluetoothDevice.BATTERY_LEVEL_UNKNOWN, any 809 * other value should be a framework bug. Thus assume here that if value is greater than 810 * BluetoothDevice.BATTERY_LEVEL_UNKNOWN, it must be valid 811 * 812 * @return battery level in String [0-100] or Null if this lower than 813 * BluetoothDevice.BATTERY_LEVEL_UNKNOWN 814 */ 815 @Nullable getValidMinBatteryLevelWithMemberDevices()816 private String getValidMinBatteryLevelWithMemberDevices() { 817 final int batteryLevel = getMinBatteryLevelWithMemberDevices(); 818 return batteryLevel > BluetoothDevice.BATTERY_LEVEL_UNKNOWN 819 ? com.android.settingslib.Utils.formatPercentage(batteryLevel) 820 : null; 821 } 822 getMinBatteryLevels(IntStream batteryLevels)823 private int getMinBatteryLevels(IntStream batteryLevels) { 824 return batteryLevels 825 .filter(battery -> battery > BluetoothDevice.BATTERY_LEVEL_UNKNOWN) 826 .min() 827 .orElse(BluetoothDevice.BATTERY_LEVEL_UNKNOWN); 828 } 829 refresh()830 void refresh() { 831 ListenableFuture<Void> future = ThreadUtils.getBackgroundExecutor().submit(() -> { 832 if (BluetoothUtils.isAdvancedDetailsHeader(mDevice)) { 833 Uri uri = BluetoothUtils.getUriMetaData(getDevice(), 834 BluetoothDevice.METADATA_MAIN_ICON); 835 if (uri != null && mDrawableCache.get(uri.toString()) == null) { 836 mDrawableCache.put(uri.toString(), 837 (BitmapDrawable) BluetoothUtils.getBtDrawableWithDescription( 838 mContext, this).first); 839 } 840 } 841 return null; 842 }); 843 Futures.addCallback(future, new FutureCallback<>() { 844 @Override 845 public void onSuccess(Void result) { 846 dispatchAttributesChanged(); 847 } 848 849 @Override 850 public void onFailure(Throwable t) {} 851 }, mContext.getMainExecutor()); 852 } 853 setJustDiscovered(boolean justDiscovered)854 public void setJustDiscovered(boolean justDiscovered) { 855 if (mJustDiscovered != justDiscovered) { 856 mJustDiscovered = justDiscovered; 857 dispatchAttributesChanged(); 858 } 859 } 860 getBondState()861 public int getBondState() { 862 return mDevice.getBondState(); 863 } 864 865 /** 866 * Update the device status as active or non-active per Bluetooth profile. 867 * 868 * @param isActive true if the device is active 869 * @param bluetoothProfile the Bluetooth profile 870 */ onActiveDeviceChanged(boolean isActive, int bluetoothProfile)871 public void onActiveDeviceChanged(boolean isActive, int bluetoothProfile) { 872 if (BluetoothUtils.D) { 873 Log.d(TAG, "onActiveDeviceChanged: " 874 + "profile " + BluetoothProfile.getProfileName(bluetoothProfile) 875 + ", device " + mDevice.getAnonymizedAddress() 876 + ", isActive " + isActive); 877 } 878 boolean changed = false; 879 switch (bluetoothProfile) { 880 case BluetoothProfile.A2DP: 881 changed = (mIsActiveDeviceA2dp != isActive); 882 mIsActiveDeviceA2dp = isActive; 883 break; 884 case BluetoothProfile.HEADSET: 885 changed = (mIsActiveDeviceHeadset != isActive); 886 mIsActiveDeviceHeadset = isActive; 887 break; 888 case BluetoothProfile.HEARING_AID: 889 changed = (mIsActiveDeviceHearingAid != isActive); 890 mIsActiveDeviceHearingAid = isActive; 891 break; 892 case BluetoothProfile.LE_AUDIO: 893 changed = (mIsActiveDeviceLeAudio != isActive); 894 mIsActiveDeviceLeAudio = isActive; 895 break; 896 default: 897 Log.w(TAG, "onActiveDeviceChanged: unknown profile " + bluetoothProfile + 898 " isActive " + isActive); 899 break; 900 } 901 if (changed) { 902 dispatchAttributesChanged(); 903 } 904 } 905 906 /** 907 * Update the profile audio state. 908 */ onAudioModeChanged()909 void onAudioModeChanged() { 910 dispatchAttributesChanged(); 911 } 912 913 /** 914 * Notify that the audio category has changed. 915 */ onAudioDeviceCategoryChanged()916 public void onAudioDeviceCategoryChanged() { 917 dispatchAttributesChanged(); 918 } 919 920 /** 921 * Get the device status as active or non-active per Bluetooth profile. 922 * 923 * @param bluetoothProfile the Bluetooth profile 924 * @return true if the device is active 925 */ 926 @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) isActiveDevice(int bluetoothProfile)927 public boolean isActiveDevice(int bluetoothProfile) { 928 switch (bluetoothProfile) { 929 case BluetoothProfile.A2DP: 930 return mIsActiveDeviceA2dp; 931 case BluetoothProfile.HEADSET: 932 return mIsActiveDeviceHeadset; 933 case BluetoothProfile.HEARING_AID: 934 return mIsActiveDeviceHearingAid; 935 case BluetoothProfile.LE_AUDIO: 936 return mIsActiveDeviceLeAudio; 937 default: 938 Log.w(TAG, "getActiveDevice: unknown profile " + bluetoothProfile); 939 break; 940 } 941 return false; 942 } 943 setRssi(short rssi)944 void setRssi(short rssi) { 945 if (mRssi != rssi) { 946 mRssi = rssi; 947 dispatchAttributesChanged(); 948 } 949 } 950 951 /** 952 * Checks whether we are connected to this device (any profile counts). 953 * 954 * @return Whether it is connected. 955 */ isConnected()956 public boolean isConnected() { 957 synchronized (mProfileLock) { 958 for (LocalBluetoothProfile profile : mProfiles) { 959 int status = getProfileConnectionState(profile); 960 if (status == BluetoothProfile.STATE_CONNECTED) { 961 return true; 962 } 963 } 964 965 return false; 966 } 967 } 968 969 /** 970 * Checks if the device is connected to the specified Bluetooth profile. 971 * 972 * @param profile The Bluetooth profile to check. 973 * @return {@code true} if the device is connected to the profile. 974 */ isConnectedProfile(LocalBluetoothProfile profile)975 public boolean isConnectedProfile(LocalBluetoothProfile profile) { 976 int status = getProfileConnectionState(profile); 977 return status == BluetoothProfile.STATE_CONNECTED; 978 979 } 980 981 /** 982 * Checks if the device is connected to the Bluetooth profile with the given ID. 983 * 984 * @param profileId The ID of the Bluetooth profile to check. 985 * @return {@code true} if the device is connected to the profile. 986 */ isConnectedProfile(int profileId)987 public boolean isConnectedProfile(int profileId) { 988 for (LocalBluetoothProfile profile : getProfiles()) { 989 if (profile.getProfileId() == profileId) { 990 return isConnectedProfile(profile); 991 } 992 } 993 return false; 994 } 995 isBusy()996 public boolean isBusy() { 997 synchronized (mProfileLock) { 998 for (LocalBluetoothProfile profile : mProfiles) { 999 int status = getProfileConnectionState(profile); 1000 if (status == BluetoothProfile.STATE_CONNECTING 1001 || status == BluetoothProfile.STATE_DISCONNECTING) { 1002 return true; 1003 } 1004 } 1005 return getBondState() == BluetoothDevice.BOND_BONDING; 1006 } 1007 } 1008 updateProfiles()1009 private boolean updateProfiles() { 1010 ParcelUuid[] uuids = mDevice.getUuids(); 1011 if (uuids == null) return false; 1012 1013 List<ParcelUuid> uuidsList = mLocalAdapter.getUuidsList(); 1014 ParcelUuid[] localUuids = new ParcelUuid[uuidsList.size()]; 1015 uuidsList.toArray(localUuids); 1016 1017 /* 1018 * Now we know if the device supports PBAP, update permissions... 1019 */ 1020 processPhonebookAccess(); 1021 1022 synchronized (mProfileLock) { 1023 mProfileManager.updateProfiles(uuids, localUuids, mProfiles, mRemovedProfiles, 1024 mLocalNapRoleConnected, mDevice); 1025 } 1026 1027 if (BluetoothUtils.D) { 1028 Log.d(TAG, "updating profiles for " + mDevice.getAnonymizedAddress()); 1029 BluetoothClass bluetoothClass = mDevice.getBluetoothClass(); 1030 1031 if (bluetoothClass != null) Log.v(TAG, "Class: " + bluetoothClass.toString()); 1032 Log.v(TAG, "UUID:"); 1033 for (ParcelUuid uuid : uuids) { 1034 Log.v(TAG, " " + uuid); 1035 } 1036 } 1037 return true; 1038 } 1039 fetchActiveDevices()1040 private void fetchActiveDevices() { 1041 A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile(); 1042 if (a2dpProfile != null) { 1043 mIsActiveDeviceA2dp = mDevice.equals(a2dpProfile.getActiveDevice()); 1044 } 1045 HeadsetProfile headsetProfile = mProfileManager.getHeadsetProfile(); 1046 if (headsetProfile != null) { 1047 mIsActiveDeviceHeadset = mDevice.equals(headsetProfile.getActiveDevice()); 1048 } 1049 HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile(); 1050 if (hearingAidProfile != null) { 1051 mIsActiveDeviceHearingAid = hearingAidProfile.getActiveDevices().contains(mDevice); 1052 } 1053 LeAudioProfile leAudio = mProfileManager.getLeAudioProfile(); 1054 if (leAudio != null) { 1055 mIsActiveDeviceLeAudio = leAudio.getActiveDevices().contains(mDevice); 1056 } 1057 } 1058 1059 /** 1060 * Refreshes the UI when framework alerts us of a UUID change. 1061 */ onUuidChanged()1062 void onUuidChanged() { 1063 updateProfiles(); 1064 ParcelUuid[] uuids = mDevice.getUuids(); 1065 1066 long timeout = MAX_UUID_DELAY_FOR_AUTO_CONNECT; 1067 if (ArrayUtils.contains(uuids, BluetoothUuid.HOGP)) { 1068 timeout = MAX_HOGP_DELAY_FOR_AUTO_CONNECT; 1069 } else if (ArrayUtils.contains(uuids, BluetoothUuid.HEARING_AID)) { 1070 timeout = MAX_HEARING_AIDS_DELAY_FOR_AUTO_CONNECT; 1071 } else if (ArrayUtils.contains(uuids, BluetoothUuid.LE_AUDIO)) { 1072 timeout = MAX_LEAUDIO_DELAY_FOR_AUTO_CONNECT; 1073 } 1074 1075 if (BluetoothUtils.D) { 1076 long lastConnectAttempted = mConnectAttempted == -1 ? 0 : mConnectAttempted; 1077 Log.d( 1078 TAG, 1079 "onUuidChanged: Time since last connect/manual disconnect=" 1080 + (SystemClock.elapsedRealtime() - lastConnectAttempted)); 1081 } 1082 1083 /* 1084 * If a connect was attempted earlier without any UUID, we will do the connect now. 1085 * Otherwise, allow the connect on UUID change. 1086 */ 1087 if (mConnectAttempted != -1 1088 && (mConnectAttempted + timeout) > SystemClock.elapsedRealtime()) { 1089 Log.d(TAG, "onUuidChanged: triggering connectDevice"); 1090 connectDevice(); 1091 } 1092 1093 dispatchAttributesChanged(); 1094 } 1095 onBondingStateChanged(int bondState)1096 void onBondingStateChanged(int bondState) { 1097 if (bondState == BluetoothDevice.BOND_NONE) { 1098 synchronized (mProfileLock) { 1099 mProfiles.clear(); 1100 } 1101 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_UNKNOWN); 1102 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_UNKNOWN); 1103 mDevice.setSimAccessPermission(BluetoothDevice.ACCESS_UNKNOWN); 1104 1105 mBondTimestamp = null; 1106 } 1107 1108 refresh(); 1109 1110 if (bondState == BluetoothDevice.BOND_BONDED) { 1111 mBondTimestamp = new Timestamp(System.currentTimeMillis()); 1112 1113 if (mDevice.isBondingInitiatedLocally()) { 1114 connect(); 1115 } 1116 1117 // Saves this device as just bonded and checks if it's an hearing device after 1118 // profiles are connected. This is for judging whether to display the survey. 1119 HearingAidStatsLogUtils.addToJustBonded(getAddress()); 1120 } 1121 } 1122 getBondTimestamp()1123 public Timestamp getBondTimestamp() { 1124 return mBondTimestamp; 1125 } 1126 getBtClass()1127 public BluetoothClass getBtClass() { 1128 return mDevice.getBluetoothClass(); 1129 } 1130 1131 /** 1132 * Returns a list of {@link LocalBluetoothProfile} supported by the device. 1133 */ getProfiles()1134 public List<LocalBluetoothProfile> getProfiles() { 1135 return new ArrayList<>(mProfiles); 1136 } 1137 1138 /** 1139 * Returns a list of {@link LocalBluetoothProfile} that are user-accessible from UI to 1140 * initiate a connection. 1141 * 1142 * Note: Use {@link #getProfiles()} to retrieve all supported profiles on the device. 1143 */ getUiAccessibleProfiles()1144 public List<LocalBluetoothProfile> getUiAccessibleProfiles() { 1145 List<LocalBluetoothProfile> accessibleProfiles = new ArrayList<>(); 1146 synchronized (mProfileLock) { 1147 for (LocalBluetoothProfile profile : mProfiles) { 1148 if (profile.accessProfileEnabled()) { 1149 accessibleProfiles.add(profile); 1150 } 1151 } 1152 } 1153 return accessibleProfiles; 1154 } 1155 getRemovedProfiles()1156 public List<LocalBluetoothProfile> getRemovedProfiles() { 1157 return new ArrayList<>(mRemovedProfiles); 1158 } 1159 1160 /** 1161 * @deprecated Use {@link #registerCallback(Executor, Callback)}. 1162 */ 1163 @Deprecated registerCallback(Callback callback)1164 public void registerCallback(Callback callback) { 1165 mCallbacks.add(callback); 1166 } 1167 1168 /** 1169 * Registers a {@link Callback} that will be invoked when the bluetooth device attribute is 1170 * changed. 1171 * 1172 * @param executor an {@link Executor} to execute given callback 1173 * @param callback user implementation of the {@link Callback} 1174 */ registerCallback( @onNull @allbackExecutor Executor executor, @NonNull Callback callback)1175 public void registerCallback( 1176 @NonNull @CallbackExecutor Executor executor, @NonNull Callback callback) { 1177 Objects.requireNonNull(executor, "executor cannot be null"); 1178 Objects.requireNonNull(callback, "callback cannot be null"); 1179 mCallbackExecutorMap.put(callback, executor); 1180 } 1181 unregisterCallback(Callback callback)1182 public void unregisterCallback(Callback callback) { 1183 mCallbacks.remove(callback); 1184 mCallbackExecutorMap.remove(callback); 1185 } 1186 dispatchAttributesChanged()1187 void dispatchAttributesChanged() { 1188 for (Callback callback : mCallbacks) { 1189 callback.onDeviceAttributesChanged(); 1190 } 1191 mCallbackExecutorMap.forEach((callback, executor) -> 1192 executor.execute(callback::onDeviceAttributesChanged)); 1193 } 1194 1195 @Override toString()1196 public String toString() { 1197 StringBuilder builder = new StringBuilder("CachedBluetoothDevice{"); 1198 builder.append("anonymizedAddress=").append(mDevice.getAnonymizedAddress()); 1199 builder.append(", name=").append(getName()); 1200 builder.append(", groupId=").append(mGroupId); 1201 builder.append(", member=").append(mMemberDevices); 1202 if (isHearingAidDevice()) { 1203 builder.append(", hearingAidInfo=").append(mHearingAidInfo); 1204 builder.append(", subDevice=").append(mSubDevice); 1205 } 1206 builder.append("}"); 1207 return builder.toString(); 1208 } 1209 1210 @Override equals(Object o)1211 public boolean equals(Object o) { 1212 if ((o == null) || !(o instanceof CachedBluetoothDevice)) { 1213 return false; 1214 } 1215 return mDevice.equals(((CachedBluetoothDevice) o).mDevice); 1216 } 1217 1218 @Override hashCode()1219 public int hashCode() { 1220 return mDevice.getAddress().hashCode(); 1221 } 1222 1223 // This comparison uses non-final fields so the sort order may change 1224 // when device attributes change (such as bonding state). Settings 1225 // will completely refresh the device list when this happens. compareTo(CachedBluetoothDevice another)1226 public int compareTo(CachedBluetoothDevice another) { 1227 // Connected above not connected 1228 int comparison = (another.isConnected() ? 1 : 0) - (isConnected() ? 1 : 0); 1229 if (comparison != 0) return comparison; 1230 1231 // Paired above not paired 1232 comparison = (another.getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0) - 1233 (getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0); 1234 if (comparison != 0) return comparison; 1235 1236 // Just discovered above discovered in the past 1237 comparison = (another.mJustDiscovered ? 1 : 0) - (mJustDiscovered ? 1 : 0); 1238 if (comparison != 0) return comparison; 1239 1240 // Stronger signal above weaker signal 1241 comparison = another.mRssi - mRssi; 1242 if (comparison != 0) return comparison; 1243 1244 // Fallback on name 1245 return getName().compareTo(another.getName()); 1246 } 1247 1248 public interface Callback { onDeviceAttributesChanged()1249 void onDeviceAttributesChanged(); 1250 } 1251 1252 // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth 1253 // app's shared preferences). migratePhonebookPermissionChoice()1254 private void migratePhonebookPermissionChoice() { 1255 SharedPreferences preferences = mContext.getSharedPreferences( 1256 "bluetooth_phonebook_permission", Context.MODE_PRIVATE); 1257 if (!preferences.contains(mDevice.getAddress())) { 1258 return; 1259 } 1260 1261 if (mDevice.getPhonebookAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) { 1262 int oldPermission = 1263 preferences.getInt(mDevice.getAddress(), BluetoothDevice.ACCESS_UNKNOWN); 1264 if (oldPermission == BluetoothDevice.ACCESS_ALLOWED) { 1265 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED); 1266 } else if (oldPermission == BluetoothDevice.ACCESS_REJECTED) { 1267 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED); 1268 } 1269 } 1270 1271 SharedPreferences.Editor editor = preferences.edit(); 1272 editor.remove(mDevice.getAddress()); 1273 editor.commit(); 1274 } 1275 1276 // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth 1277 // app's shared preferences). migrateMessagePermissionChoice()1278 private void migrateMessagePermissionChoice() { 1279 SharedPreferences preferences = mContext.getSharedPreferences( 1280 "bluetooth_message_permission", Context.MODE_PRIVATE); 1281 if (!preferences.contains(mDevice.getAddress())) { 1282 return; 1283 } 1284 1285 if (mDevice.getMessageAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) { 1286 int oldPermission = 1287 preferences.getInt(mDevice.getAddress(), BluetoothDevice.ACCESS_UNKNOWN); 1288 if (oldPermission == BluetoothDevice.ACCESS_ALLOWED) { 1289 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_ALLOWED); 1290 } else if (oldPermission == BluetoothDevice.ACCESS_REJECTED) { 1291 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_REJECTED); 1292 } 1293 } 1294 1295 SharedPreferences.Editor editor = preferences.edit(); 1296 editor.remove(mDevice.getAddress()); 1297 editor.commit(); 1298 } 1299 processPhonebookAccess()1300 private void processPhonebookAccess() { 1301 if (mDevice.getBondState() != BluetoothDevice.BOND_BONDED) return; 1302 1303 ParcelUuid[] uuids = mDevice.getUuids(); 1304 if (BluetoothUuid.containsAnyUuid(uuids, PbapServerProfile.PBAB_CLIENT_UUIDS)) { 1305 // The pairing dialog now warns of phone-book access for paired devices. 1306 // No separate prompt is displayed after pairing. 1307 mDevice.getPhonebookAccessPermission(); 1308 } 1309 } 1310 getMaxConnectionState()1311 public int getMaxConnectionState() { 1312 int maxState = BluetoothProfile.STATE_DISCONNECTED; 1313 synchronized (mProfileLock) { 1314 for (LocalBluetoothProfile profile : getProfiles()) { 1315 int connectionStatus = getProfileConnectionState(profile); 1316 if (connectionStatus > maxState) { 1317 maxState = connectionStatus; 1318 } 1319 } 1320 } 1321 return maxState; 1322 } 1323 1324 /** 1325 * Return full summary that describes connection state of this device 1326 * 1327 * @see #getConnectionSummary(boolean shortSummary) 1328 */ getConnectionSummary()1329 public String getConnectionSummary() { 1330 return getConnectionSummary(false /* shortSummary */); 1331 } 1332 1333 /** 1334 * Return summary that describes connection state of this device. Summary depends on: 1. Whether 1335 * device has battery info 2. Whether device is in active usage(or in phone call) 3. Whether 1336 * device is in audio sharing process 1337 * 1338 * @param shortSummary {@code true} if need to return short version summary 1339 */ getConnectionSummary(boolean shortSummary)1340 public String getConnectionSummary(boolean shortSummary) { 1341 CharSequence summary = null; 1342 if (BluetoothUtils.isAudioSharingUIAvailable(mContext)) { 1343 if (mBluetoothManager == null) { 1344 mBluetoothManager = LocalBluetoothManager.getInstance(mContext, null); 1345 } 1346 if (BluetoothUtils.isBroadcasting(mBluetoothManager)) { 1347 summary = getBroadcastConnectionSummary(shortSummary); 1348 } 1349 } 1350 if (summary == null) { 1351 summary = 1352 getConnectionSummary( 1353 shortSummary, 1354 false /* isTvSummary */, 1355 SUMMARY_NO_COLOR_FOR_LOW_BATTERY); 1356 } 1357 return summary != null ? summary.toString() : null; 1358 } 1359 1360 /** 1361 * Returns the connection summary of this device during le audio sharing. 1362 * 1363 * @param shortSummary {@code true} if need to return short version summary 1364 */ 1365 @Nullable getBroadcastConnectionSummary(boolean shortSummary)1366 private String getBroadcastConnectionSummary(boolean shortSummary) { 1367 if (isProfileConnectedFail() && isConnected()) { 1368 return mContext.getString(R.string.profile_connect_timeout_subtext); 1369 } 1370 1371 synchronized (mProfileLock) { 1372 for (LocalBluetoothProfile profile : getProfiles()) { 1373 int connectionStatus = getProfileConnectionState(profile); 1374 if (connectionStatus == BluetoothProfile.STATE_CONNECTING 1375 || connectionStatus == BluetoothProfile.STATE_DISCONNECTING) { 1376 return mContext.getString( 1377 BluetoothUtils.getConnectionStateSummary(connectionStatus)); 1378 } 1379 } 1380 } 1381 1382 int leftBattery = 1383 BluetoothUtils.getIntMetaData( 1384 mDevice, BluetoothDevice.METADATA_UNTETHERED_LEFT_BATTERY); 1385 int rightBattery = 1386 BluetoothUtils.getIntMetaData( 1387 mDevice, BluetoothDevice.METADATA_UNTETHERED_RIGHT_BATTERY); 1388 String batteryLevelPercentageString = getValidMinBatteryLevelWithMemberDevices(); 1389 1390 if (mBluetoothManager == null) { 1391 mBluetoothManager = LocalBluetoothManager.getInstance(mContext, null); 1392 } 1393 boolean isTempBond = Flags.enableTemporaryBondDevicesUi() 1394 && BluetoothUtils.isTemporaryBondDevice(getDevice()); 1395 if (BluetoothUtils.hasConnectedBroadcastSource(this, mBluetoothManager)) { 1396 // Gets summary for the buds which are in the audio sharing. 1397 int groupId = BluetoothUtils.getGroupId(this); 1398 int primaryGroupId = BluetoothUtils.getPrimaryGroupIdForBroadcast( 1399 mContext.getContentResolver(), mBluetoothManager); 1400 if ((primaryGroupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID) 1401 ? (groupId == primaryGroupId) : isActiveDevice(BluetoothProfile.LE_AUDIO)) { 1402 // The buds are primary buds 1403 return getSummaryWithBatteryInfo( 1404 R.string.bluetooth_active_battery_level_untethered, 1405 R.string.bluetooth_active_battery_level, 1406 R.string.bluetooth_active_no_battery_level, 1407 leftBattery, 1408 rightBattery, 1409 batteryLevelPercentageString, 1410 shortSummary); 1411 } else { 1412 // The buds are not primary buds 1413 return isTempBond 1414 ? getSummaryWithBatteryInfo( 1415 R.string.bluetooth_guest_media_only_battery_level_untethered, 1416 R.string.bluetooth_guest_media_only_battery_level, 1417 R.string.bluetooth_guest_media_only_no_battery_level, 1418 leftBattery, 1419 rightBattery, 1420 batteryLevelPercentageString, 1421 shortSummary) 1422 : getSummaryWithBatteryInfo( 1423 R.string.bluetooth_active_media_only_battery_level_untethered, 1424 R.string.bluetooth_active_media_only_battery_level, 1425 R.string.bluetooth_active_media_only_no_battery_level, 1426 leftBattery, 1427 rightBattery, 1428 batteryLevelPercentageString, 1429 shortSummary); 1430 } 1431 } else { 1432 // Gets summary for the buds which are not in the audio sharing. 1433 if (getProfiles().stream() 1434 .anyMatch( 1435 profile -> 1436 profile instanceof LeAudioProfile 1437 && profile.isEnabled(getDevice()))) { 1438 // The buds support le audio. 1439 if (isConnected()) { 1440 return isTempBond 1441 ? getSummaryWithBatteryInfo( 1442 R.string.bluetooth_guest_battery_level_untethered_lea_support, 1443 R.string.bluetooth_guest_battery_level_lea_support, 1444 R.string.bluetooth_guest_no_battery_level_lea_support, 1445 leftBattery, 1446 rightBattery, 1447 batteryLevelPercentageString, 1448 shortSummary) 1449 : getSummaryWithBatteryInfo( 1450 R.string.bluetooth_battery_level_untethered_lea_support, 1451 R.string.bluetooth_battery_level_lea_support, 1452 R.string.bluetooth_no_battery_level_lea_support, 1453 leftBattery, 1454 rightBattery, 1455 batteryLevelPercentageString, 1456 shortSummary); 1457 } else { 1458 return isTempBond 1459 ? mContext.getString( 1460 R.string.bluetooth_guest_saved_device_lea_support) 1461 : mContext.getString(R.string.bluetooth_saved_device_lea_support); 1462 } 1463 } 1464 } 1465 return null; 1466 } 1467 1468 /** 1469 * Returns the summary with correct format depending the battery info. 1470 * 1471 * @param untetheredBatteryResId resource id for untethered device with battery info 1472 * @param batteryResId resource id for device with single battery info 1473 * @param noBatteryResId resource id for device with no battery info 1474 * @param shortSummary {@code true} if need to return short version summary 1475 */ getSummaryWithBatteryInfo( @tringRes int untetheredBatteryResId, @StringRes int batteryResId, @StringRes int noBatteryResId, int leftBattery, int rightBattery, String batteryLevelPercentageString, boolean shortSummary)1476 private String getSummaryWithBatteryInfo( 1477 @StringRes int untetheredBatteryResId, 1478 @StringRes int batteryResId, 1479 @StringRes int noBatteryResId, 1480 int leftBattery, 1481 int rightBattery, 1482 String batteryLevelPercentageString, 1483 boolean shortSummary) { 1484 if (isTwsBatteryAvailable(leftBattery, rightBattery) && !shortSummary) { 1485 return mContext.getString( 1486 untetheredBatteryResId, 1487 Utils.formatPercentage(leftBattery), 1488 Utils.formatPercentage(rightBattery)); 1489 } else if (batteryLevelPercentageString != null && !shortSummary) { 1490 return mContext.getString(batteryResId, batteryLevelPercentageString); 1491 } else { 1492 return mContext.getString(noBatteryResId); 1493 } 1494 } 1495 1496 /** 1497 * Returns android tv string that describes the connection state of this device. 1498 */ getTvConnectionSummary()1499 public CharSequence getTvConnectionSummary() { 1500 return getTvConnectionSummary(SUMMARY_NO_COLOR_FOR_LOW_BATTERY); 1501 } 1502 1503 /** 1504 * Returns android tv string that describes the connection state of this device, with low 1505 * battery states highlighted in color. 1506 * 1507 * @param lowBatteryColorRes - resource id for the color that should be used for the part of the 1508 * CharSequence that contains low battery information. 1509 */ getTvConnectionSummary(int lowBatteryColorRes)1510 public CharSequence getTvConnectionSummary(int lowBatteryColorRes) { 1511 return getConnectionSummary(false /* shortSummary */, true /* isTvSummary */, 1512 lowBatteryColorRes); 1513 } 1514 1515 /** 1516 * Return summary that describes connection state of this device. Summary depends on: 1517 * 1. Whether device has battery info 1518 * 2. Whether device is in active usage(or in phone call) 1519 * 1520 * @param shortSummary {@code true} if need to return short version summary 1521 * @param isTvSummary {@code true} if the summary should be TV specific 1522 * @param lowBatteryColorRes Resource id of the color to be used for low battery strings. Use 1523 * {@link SUMMARY_NO_COLOR_FOR_LOW_BATTERY} if no separate color 1524 * should be used. 1525 */ getConnectionSummary(boolean shortSummary, boolean isTvSummary, int lowBatteryColorRes)1526 private CharSequence getConnectionSummary(boolean shortSummary, boolean isTvSummary, 1527 int lowBatteryColorRes) { 1528 boolean profileConnected = false; // Updated as long as BluetoothProfile is connected 1529 boolean a2dpConnected = true; // A2DP is connected 1530 boolean hfpConnected = true; // HFP is connected 1531 boolean hearingAidConnected = true; // Hearing Aid is connected 1532 boolean leAudioConnected = true; // LeAudio is connected 1533 int leftBattery = -1; 1534 int rightBattery = -1; 1535 1536 Integer keyMissingCount = BluetoothUtils.getKeyMissingCount(mDevice); 1537 if (keyMissingCount != null && keyMissingCount > 0) { 1538 return mContext.getString(R.string.bluetooth_key_missing_subtext); 1539 } 1540 1541 if (isProfileConnectedFail() && isConnected()) { 1542 return mContext.getString(R.string.profile_connect_timeout_subtext); 1543 } 1544 1545 synchronized (mProfileLock) { 1546 for (LocalBluetoothProfile profile : getProfiles()) { 1547 int connectionStatus = getProfileConnectionState(profile); 1548 1549 switch (connectionStatus) { 1550 case BluetoothProfile.STATE_CONNECTING: 1551 case BluetoothProfile.STATE_DISCONNECTING: 1552 return mContext.getString( 1553 BluetoothUtils.getConnectionStateSummary(connectionStatus)); 1554 1555 case BluetoothProfile.STATE_CONNECTED: 1556 profileConnected = true; 1557 break; 1558 1559 case BluetoothProfile.STATE_DISCONNECTED: 1560 if (profile.isProfileReady()) { 1561 if (profile instanceof A2dpProfile 1562 || profile instanceof A2dpSinkProfile) { 1563 a2dpConnected = false; 1564 } else if (profile instanceof HeadsetProfile 1565 || profile instanceof HfpClientProfile) { 1566 hfpConnected = false; 1567 } else if (profile instanceof HearingAidProfile) { 1568 hearingAidConnected = false; 1569 } else if (profile instanceof LeAudioProfile) { 1570 leAudioConnected = false; 1571 } 1572 } 1573 break; 1574 } 1575 } 1576 } 1577 1578 String batteryLevelPercentageString = getValidMinBatteryLevelWithMemberDevices(); 1579 int stringRes = R.string.bluetooth_pairing; 1580 //when profile is connected, information would be available 1581 if (profileConnected) { 1582 leftBattery = getLeftBatteryLevel(); 1583 rightBattery = getRightBatteryLevel(); 1584 1585 boolean isTempBond = Flags.enableTemporaryBondDevicesUi() 1586 && BluetoothUtils.isTemporaryBondDevice(getDevice()); 1587 // Set default string with battery level in device connected situation. 1588 if (isTwsBatteryAvailable(leftBattery, rightBattery)) { 1589 stringRes = 1590 isTempBond 1591 ? R.string.bluetooth_guest_battery_level_untethered 1592 : R.string.bluetooth_battery_level_untethered; 1593 } else if (batteryLevelPercentageString != null && !shortSummary) { 1594 stringRes = 1595 isTempBond 1596 ? R.string.bluetooth_guest_battery_level 1597 : R.string.bluetooth_battery_level; 1598 } 1599 1600 // Set active string in following device connected situation, also show battery 1601 // information if they have. 1602 // 1. Hearing Aid device active. 1603 // 2. Headset device active with in-calling state. 1604 // 3. A2DP device active without in-calling state. 1605 // 4. Le Audio device active 1606 if (a2dpConnected || hfpConnected || hearingAidConnected || leAudioConnected) { 1607 final boolean isOnCall = Utils.isAudioModeOngoingCall(mContext); 1608 if ((mIsActiveDeviceHearingAid) 1609 || (mIsActiveDeviceHeadset && isOnCall) 1610 || (mIsActiveDeviceA2dp && !isOnCall) 1611 || mIsActiveDeviceLeAudio) { 1612 if (isTwsBatteryAvailable(leftBattery, rightBattery) && !shortSummary) { 1613 stringRes = 1614 isTempBond 1615 ? R.string.bluetooth_guest_battery_level_untethered 1616 : R.string.bluetooth_active_battery_level_untethered; 1617 } else if (batteryLevelPercentageString != null && !shortSummary) { 1618 stringRes = 1619 isTempBond 1620 ? R.string.bluetooth_guest_battery_level 1621 : R.string.bluetooth_active_battery_level; 1622 } else { 1623 stringRes = 1624 isTempBond 1625 ? R.string.bluetooth_guest_no_battery_level 1626 : R.string.bluetooth_active_no_battery_level; 1627 } 1628 } 1629 1630 // Try to show left/right information for hearing 1631 // aids specifically. 1632 boolean isActiveAshaHearingAid = mIsActiveDeviceHearingAid; 1633 boolean isActiveLeAudioHearingAid = mIsActiveDeviceLeAudio 1634 && isConnectedHapClientDevice(); 1635 if (isActiveAshaHearingAid || isActiveLeAudioHearingAid) { 1636 stringRes = getHearingDeviceSummaryRes(leftBattery, rightBattery, shortSummary); 1637 } 1638 } 1639 } 1640 1641 if (stringRes == R.string.bluetooth_pairing 1642 && getBondState() != BluetoothDevice.BOND_BONDING) { 1643 return null; 1644 } 1645 1646 boolean summaryIncludesBatteryLevel = stringRes == R.string.bluetooth_battery_level 1647 || stringRes == R.string.bluetooth_active_battery_level 1648 || stringRes == R.string.bluetooth_active_battery_level_untethered 1649 || stringRes == R.string.bluetooth_active_battery_level_untethered_left 1650 || stringRes == R.string.bluetooth_active_battery_level_untethered_right 1651 || stringRes == R.string.bluetooth_battery_level_untethered; 1652 if (isTvSummary && summaryIncludesBatteryLevel && enableTvMediaOutputDialog()) { 1653 return getTvBatterySummary( 1654 getMinBatteryLevelWithMemberDevices(), 1655 leftBattery, 1656 rightBattery, 1657 lowBatteryColorRes); 1658 } 1659 1660 if (isTwsBatteryAvailable(leftBattery, rightBattery)) { 1661 return mContext.getString(stringRes, Utils.formatPercentage(leftBattery), 1662 Utils.formatPercentage(rightBattery)); 1663 } else if (leftBattery > BluetoothDevice.BATTERY_LEVEL_UNKNOWN 1664 && !BluetoothUtils.getBooleanMetaData(mDevice, 1665 BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET)) { 1666 return mContext.getString(stringRes, Utils.formatPercentage(leftBattery)); 1667 } else if (rightBattery > BluetoothDevice.BATTERY_LEVEL_UNKNOWN 1668 && !BluetoothUtils.getBooleanMetaData(mDevice, 1669 BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET)) { 1670 return mContext.getString(stringRes, Utils.formatPercentage(rightBattery)); 1671 } else { 1672 return mContext.getString(stringRes, batteryLevelPercentageString); 1673 } 1674 } 1675 1676 /** 1677 * Returns the battery levels of all components of the bluetooth device. If no battery info is 1678 * available then returns null. 1679 */ 1680 @WorkerThread 1681 @Nullable getBatteryLevelsInfo()1682 public BatteryLevelsInfo getBatteryLevelsInfo() { 1683 // Try getting the battery information from metadata. 1684 BatteryLevelsInfo metadataSourceBattery = getBatteryFromMetadata(); 1685 if (metadataSourceBattery != null) { 1686 return metadataSourceBattery; 1687 } 1688 // Get the battery information from Bluetooth service. 1689 return getBatteryFromBluetoothService(); 1690 } 1691 1692 @Nullable getBatteryFromMetadata()1693 private BatteryLevelsInfo getBatteryFromMetadata() { 1694 if (BluetoothUtils.getBooleanMetaData(mDevice, 1695 BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET)) { 1696 // The device is untethered headset, containing both earbuds and case. 1697 int leftBattery = 1698 BluetoothUtils.getIntMetaData( 1699 mDevice, BluetoothDevice.METADATA_UNTETHERED_LEFT_BATTERY); 1700 int rightBattery = 1701 BluetoothUtils.getIntMetaData( 1702 mDevice, BluetoothDevice.METADATA_UNTETHERED_RIGHT_BATTERY); 1703 int caseBattery = 1704 BluetoothUtils.getIntMetaData( 1705 mDevice, BluetoothDevice.METADATA_UNTETHERED_CASE_BATTERY); 1706 1707 if (leftBattery <= BluetoothDevice.BATTERY_LEVEL_UNKNOWN 1708 && rightBattery <= BluetoothDevice.BATTERY_LEVEL_UNKNOWN 1709 && caseBattery <= BluetoothDevice.BATTERY_LEVEL_UNKNOWN) { 1710 Log.d(TAG, "No battery info from metadata is available for untethered device " 1711 + mDevice.getAnonymizedAddress()); 1712 return null; 1713 } else { 1714 int overallBattery = 1715 getMinBatteryLevels( 1716 Arrays.stream(new int[]{leftBattery, rightBattery, caseBattery})); 1717 Log.d(TAG, "Acquired battery info from metadata for untethered device " 1718 + mDevice.getAnonymizedAddress() 1719 + " left earbud battery: " + leftBattery 1720 + " right earbud battery: " + rightBattery 1721 + " case battery: " + caseBattery 1722 + " overall battery: " + overallBattery); 1723 return new BatteryLevelsInfo( 1724 leftBattery, rightBattery, caseBattery, overallBattery); 1725 } 1726 } else if (mInputDevice != null || mIsDeviceStylus) { 1727 // The device is input device, using METADATA_MAIN_BATTERY field to get battery info. 1728 int overallBattery = BluetoothUtils.getIntMetaData( 1729 mDevice, BluetoothDevice.METADATA_MAIN_BATTERY); 1730 if (overallBattery <= BluetoothDevice.BATTERY_LEVEL_UNKNOWN) { 1731 Log.d(TAG, "No battery info from metadata is available for input device " 1732 + mDevice.getAnonymizedAddress()); 1733 return null; 1734 } else { 1735 Log.d(TAG, "Acquired battery info from metadata for input device " 1736 + mDevice.getAnonymizedAddress() 1737 + " overall battery: " + overallBattery); 1738 return new BatteryLevelsInfo( 1739 BluetoothDevice.BATTERY_LEVEL_UNKNOWN, 1740 BluetoothDevice.BATTERY_LEVEL_UNKNOWN, 1741 BluetoothDevice.BATTERY_LEVEL_UNKNOWN, 1742 overallBattery); 1743 } 1744 } 1745 return null; 1746 } 1747 1748 @Nullable getBatteryFromBluetoothService()1749 private BatteryLevelsInfo getBatteryFromBluetoothService() { 1750 BatteryLevelsInfo batteryLevelsInfo; 1751 if (isConnectedHearingAidDevice()) { 1752 // If the device is hearing aid device, sides can be distinguished by HearingAidInfo. 1753 batteryLevelsInfo = getBatteryOfHearingAidDeviceComponents(); 1754 if (batteryLevelsInfo != null) { 1755 return batteryLevelsInfo; 1756 } 1757 } 1758 if (isConnectedLeAudioDevice()) { 1759 // If the device is LE Audio device, sides can be distinguished by LeAudioProfile. 1760 batteryLevelsInfo = getBatteryOfLeAudioDeviceComponents(); 1761 if (batteryLevelsInfo != null) { 1762 return batteryLevelsInfo; 1763 } 1764 } 1765 int overallBattery = getMinBatteryLevelWithMemberDevices(); 1766 return overallBattery > BluetoothDevice.BATTERY_LEVEL_UNKNOWN 1767 ? new BatteryLevelsInfo( 1768 BluetoothDevice.BATTERY_LEVEL_UNKNOWN, 1769 BluetoothDevice.BATTERY_LEVEL_UNKNOWN, 1770 BluetoothDevice.BATTERY_LEVEL_UNKNOWN, 1771 overallBattery) 1772 : null; 1773 } 1774 1775 @Nullable getBatteryOfHearingAidDeviceComponents()1776 private BatteryLevelsInfo getBatteryOfHearingAidDeviceComponents() { 1777 if (getDeviceSide() == HearingAidInfo.DeviceSide.SIDE_LEFT_AND_RIGHT) { 1778 return new BatteryLevelsInfo( 1779 BluetoothDevice.BATTERY_LEVEL_UNKNOWN, 1780 BluetoothDevice.BATTERY_LEVEL_UNKNOWN, 1781 BluetoothDevice.BATTERY_LEVEL_UNKNOWN, 1782 mDevice.getBatteryLevel()); 1783 } 1784 1785 int leftBattery = getHearingAidSideBattery(HearingAidInfo.DeviceSide.SIDE_LEFT); 1786 int rightBattery = getHearingAidSideBattery(HearingAidInfo.DeviceSide.SIDE_RIGHT); 1787 int overallBattery = getMinBatteryLevels( 1788 Arrays.stream(new int[]{leftBattery, rightBattery})); 1789 1790 Log.d(TAG, "Acquired battery info from Bluetooth service for hearing aid device " 1791 + mDevice.getAnonymizedAddress() 1792 + " left battery: " + leftBattery 1793 + " right battery: " + rightBattery 1794 + " overall battery: " + overallBattery); 1795 return overallBattery > BluetoothDevice.BATTERY_LEVEL_UNKNOWN 1796 ? new BatteryLevelsInfo( 1797 leftBattery, 1798 rightBattery, 1799 BluetoothDevice.BATTERY_LEVEL_UNKNOWN, 1800 overallBattery) 1801 : null; 1802 } 1803 getHearingAidSideBattery(int side)1804 private int getHearingAidSideBattery(int side) { 1805 Optional<CachedBluetoothDevice> connectedHearingAidSide = getConnectedHearingAidSide(side); 1806 return connectedHearingAidSide.isPresent() 1807 ? connectedHearingAidSide 1808 .map(CachedBluetoothDevice::getBatteryLevel) 1809 .filter(batteryLevel -> batteryLevel > BluetoothDevice.BATTERY_LEVEL_UNKNOWN) 1810 .orElse(BluetoothDevice.BATTERY_LEVEL_UNKNOWN) 1811 : BluetoothDevice.BATTERY_LEVEL_UNKNOWN; 1812 } 1813 1814 @Nullable getBatteryOfLeAudioDeviceComponents()1815 private BatteryLevelsInfo getBatteryOfLeAudioDeviceComponents() { 1816 LeAudioProfile leAudio = mProfileManager.getLeAudioProfile(); 1817 if (leAudio == null) { 1818 return null; 1819 } 1820 int leftBattery = BluetoothDevice.BATTERY_LEVEL_UNKNOWN; 1821 int rightBattery = BluetoothDevice.BATTERY_LEVEL_UNKNOWN; 1822 int overallBattery = BluetoothDevice.BATTERY_LEVEL_UNKNOWN; 1823 1824 Set<BluetoothDevice> allDevices = 1825 Stream.concat( 1826 mMemberDevices.stream().map(CachedBluetoothDevice::getDevice), 1827 Stream.of(mDevice)) 1828 .collect(Collectors.toSet()); 1829 for (BluetoothDevice device : allDevices) { 1830 int battery = device.getBatteryLevel(); 1831 if (battery <= BluetoothDevice.BATTERY_LEVEL_UNKNOWN) { 1832 continue; 1833 } 1834 int deviceId = leAudio.getAudioLocation(device); 1835 boolean isLeft = (deviceId & LeAudioProfile.LEFT_DEVICE_ID) != 0; 1836 boolean isRight = (deviceId & LeAudioProfile.RIGHT_DEVICE_ID) != 0; 1837 boolean isLeftRight = isLeft && isRight; 1838 // We should expect only one device assign to one side, but if it happens, 1839 // we don't care which one. 1840 if (isLeftRight) { 1841 overallBattery = battery; 1842 } else if (isLeft) { 1843 leftBattery = battery; 1844 } else if (isRight) { 1845 rightBattery = battery; 1846 } 1847 } 1848 overallBattery = getMinBatteryLevels( 1849 Arrays.stream(new int[]{leftBattery, rightBattery, overallBattery})); 1850 1851 Log.d(TAG, "Acquired battery info from Bluetooth service for le audio device " 1852 + mDevice.getAnonymizedAddress() 1853 + " left battery: " + leftBattery 1854 + " right battery: " + rightBattery 1855 + " overall battery: " + overallBattery); 1856 return overallBattery > BluetoothDevice.BATTERY_LEVEL_UNKNOWN 1857 ? new BatteryLevelsInfo( 1858 leftBattery, 1859 rightBattery, 1860 BluetoothDevice.BATTERY_LEVEL_UNKNOWN, 1861 overallBattery) 1862 : null; 1863 } 1864 getTvBatterySummary(int mainBattery, int leftBattery, int rightBattery, int lowBatteryColorRes)1865 private CharSequence getTvBatterySummary(int mainBattery, int leftBattery, int rightBattery, 1866 int lowBatteryColorRes) { 1867 // Since there doesn't seem to be a way to use format strings to add the 1868 // percentages and also mark which part of the string is left and right to color 1869 // them, we are using one string resource per battery. 1870 Resources res = mContext.getResources(); 1871 SpannableStringBuilder spannableBuilder = new SpannableStringBuilder(); 1872 if (leftBattery >= 0 || rightBattery >= 0) { 1873 // Not switching the left and right for RTL to keep the left earbud always on 1874 // the left. 1875 if (leftBattery >= 0) { 1876 String left = res.getString( 1877 R.string.tv_bluetooth_battery_level_untethered_left, 1878 Utils.formatPercentage(leftBattery)); 1879 addBatterySpan(spannableBuilder, left, isBatteryLow(leftBattery, 1880 BluetoothDevice.METADATA_UNTETHERED_LEFT_LOW_BATTERY_THRESHOLD), 1881 lowBatteryColorRes); 1882 } 1883 if (rightBattery >= 0) { 1884 if (spannableBuilder.length() > 0) { 1885 spannableBuilder.append(" "); 1886 } 1887 String right = res.getString( 1888 R.string.tv_bluetooth_battery_level_untethered_right, 1889 Utils.formatPercentage(rightBattery)); 1890 addBatterySpan(spannableBuilder, right, isBatteryLow(rightBattery, 1891 BluetoothDevice.METADATA_UNTETHERED_RIGHT_LOW_BATTERY_THRESHOLD), 1892 lowBatteryColorRes); 1893 } 1894 } else { 1895 addBatterySpan(spannableBuilder, res.getString(R.string.tv_bluetooth_battery_level, 1896 Utils.formatPercentage(mainBattery)), 1897 isBatteryLow(mainBattery, BluetoothDevice.METADATA_MAIN_LOW_BATTERY_THRESHOLD), 1898 lowBatteryColorRes); 1899 } 1900 return spannableBuilder; 1901 } 1902 getHearingDeviceSummaryRes(int leftBattery, int rightBattery, boolean shortSummary)1903 private int getHearingDeviceSummaryRes(int leftBattery, int rightBattery, 1904 boolean shortSummary) { 1905 if (getDeviceSide() == HearingAidInfo.DeviceSide.SIDE_MONO 1906 || getDeviceSide() == HearingAidInfo.DeviceSide.SIDE_LEFT_AND_RIGHT) { 1907 return !shortSummary && (getBatteryLevel() > BluetoothDevice.BATTERY_LEVEL_UNKNOWN) 1908 ? R.string.bluetooth_active_battery_level 1909 : R.string.bluetooth_active_no_battery_level; 1910 } 1911 boolean isLeftDeviceConnected = getConnectedHearingAidSide( 1912 HearingAidInfo.DeviceSide.SIDE_LEFT).isPresent(); 1913 boolean isRightDeviceConnected = getConnectedHearingAidSide( 1914 HearingAidInfo.DeviceSide.SIDE_RIGHT).isPresent(); 1915 boolean shouldShowLeftBattery = 1916 !shortSummary && (leftBattery > BluetoothDevice.BATTERY_LEVEL_UNKNOWN); 1917 boolean shouldShowRightBattery = 1918 !shortSummary && (rightBattery > BluetoothDevice.BATTERY_LEVEL_UNKNOWN); 1919 1920 if (isLeftDeviceConnected && isRightDeviceConnected) { 1921 return (shouldShowLeftBattery && shouldShowRightBattery) 1922 ? R.string.bluetooth_active_battery_level_untethered 1923 : R.string.bluetooth_hearing_aid_left_and_right_active; 1924 } 1925 if (isLeftDeviceConnected) { 1926 return shouldShowLeftBattery 1927 ? R.string.bluetooth_active_battery_level_untethered_left 1928 : R.string.bluetooth_hearing_aid_left_active; 1929 } 1930 if (isRightDeviceConnected) { 1931 return shouldShowRightBattery 1932 ? R.string.bluetooth_active_battery_level_untethered_right 1933 : R.string.bluetooth_hearing_aid_right_active; 1934 } 1935 1936 return R.string.bluetooth_active_no_battery_level; 1937 } 1938 addBatterySpan(SpannableStringBuilder builder, String batteryString, boolean lowBattery, int lowBatteryColorRes)1939 private void addBatterySpan(SpannableStringBuilder builder, 1940 String batteryString, boolean lowBattery, int lowBatteryColorRes) { 1941 if (lowBattery && lowBatteryColorRes != SUMMARY_NO_COLOR_FOR_LOW_BATTERY) { 1942 builder.append(batteryString, 1943 new ForegroundColorSpan(mContext.getResources().getColor(lowBatteryColorRes)), 1944 0 /* flags */); 1945 } else { 1946 builder.append(batteryString); 1947 } 1948 } 1949 isBatteryLow(int batteryLevel, int metadataKey)1950 private boolean isBatteryLow(int batteryLevel, int metadataKey) { 1951 int lowBatteryThreshold = BluetoothUtils.getIntMetaData(mDevice, metadataKey); 1952 if (lowBatteryThreshold <= 0) { 1953 lowBatteryThreshold = DEFAULT_LOW_BATTERY_THRESHOLD; 1954 } 1955 return batteryLevel <= lowBatteryThreshold; 1956 } 1957 isTwsBatteryAvailable(int leftBattery, int rightBattery)1958 private boolean isTwsBatteryAvailable(int leftBattery, int rightBattery) { 1959 return leftBattery >= 0 && rightBattery >= 0; 1960 } 1961 getConnectedHearingAidSide( @earingAidInfo.DeviceSide int side)1962 private Optional<CachedBluetoothDevice> getConnectedHearingAidSide( 1963 @HearingAidInfo.DeviceSide int side) { 1964 return Stream.concat(Stream.of(this, mSubDevice), mMemberDevices.stream()) 1965 .filter(Objects::nonNull) 1966 .filter(device -> device.getDeviceSide() == side) 1967 .filter(device -> device.getDevice().isConnected()) 1968 // For hearing aids, we should expect only one device assign to one side, but if 1969 // it happens, we don't care which one. 1970 .findAny(); 1971 } 1972 getLeftBatteryLevel()1973 private int getLeftBatteryLevel() { 1974 int leftBattery = BluetoothDevice.BATTERY_LEVEL_UNKNOWN; 1975 if (BluetoothUtils.getBooleanMetaData(mDevice, 1976 BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET)) { 1977 leftBattery = BluetoothUtils.getIntMetaData(mDevice, 1978 BluetoothDevice.METADATA_UNTETHERED_LEFT_BATTERY); 1979 } 1980 1981 // Retrieve hearing aids (ASHA, HAP) individual side battery level 1982 if (leftBattery == BluetoothDevice.BATTERY_LEVEL_UNKNOWN) { 1983 leftBattery = getHearingAidSideBattery(HearingAidInfo.DeviceSide.SIDE_LEFT); 1984 } 1985 1986 return leftBattery; 1987 } 1988 getRightBatteryLevel()1989 private int getRightBatteryLevel() { 1990 int rightBattery = BluetoothDevice.BATTERY_LEVEL_UNKNOWN; 1991 if (BluetoothUtils.getBooleanMetaData( 1992 mDevice, BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET)) { 1993 rightBattery = BluetoothUtils.getIntMetaData(mDevice, 1994 BluetoothDevice.METADATA_UNTETHERED_RIGHT_BATTERY); 1995 } 1996 1997 // Retrieve hearing aids (ASHA, HAP) individual side battery level 1998 if (rightBattery == BluetoothDevice.BATTERY_LEVEL_UNKNOWN) { 1999 rightBattery = getHearingAidSideBattery(HearingAidInfo.DeviceSide.SIDE_RIGHT); 2000 } 2001 2002 return rightBattery; 2003 } 2004 isProfileConnectedFail()2005 private boolean isProfileConnectedFail() { 2006 Log.d(TAG, "anonymizedAddress=" + mDevice.getAnonymizedAddress() 2007 + " mIsA2dpProfileConnectedFail=" + mIsA2dpProfileConnectedFail 2008 + " mIsHearingAidProfileConnectedFail=" + mIsHearingAidProfileConnectedFail 2009 + " mIsLeAudioProfileConnectedFail=" + mIsLeAudioProfileConnectedFail 2010 + " mIsHeadsetProfileConnectedFail=" + mIsHeadsetProfileConnectedFail 2011 + " isConnectedSapDevice()=" + isConnectedSapDevice()); 2012 if (mIsA2dpProfileConnectedFail) { 2013 A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile(); 2014 if (a2dpProfile != null && a2dpProfile.isEnabled(mDevice)) { 2015 return true; 2016 } 2017 } 2018 if (mIsHearingAidProfileConnectedFail) { 2019 HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile(); 2020 if (hearingAidProfile != null && hearingAidProfile.isEnabled(mDevice)) { 2021 return true; 2022 } 2023 } 2024 if (!isConnectedSapDevice() && mIsHeadsetProfileConnectedFail) { 2025 HeadsetProfile headsetProfile = mProfileManager.getHeadsetProfile(); 2026 if (headsetProfile != null && headsetProfile.isEnabled(mDevice)) { 2027 return true; 2028 } 2029 } 2030 if (mIsLeAudioProfileConnectedFail) { 2031 LeAudioProfile leAudioProfile = mProfileManager.getLeAudioProfile(); 2032 if (leAudioProfile != null && leAudioProfile.isEnabled(mDevice)) { 2033 return true; 2034 } 2035 } 2036 return false; 2037 } 2038 2039 /** 2040 * See {@link #getCarConnectionSummary(boolean, boolean)} 2041 */ getCarConnectionSummary()2042 public String getCarConnectionSummary() { 2043 return getCarConnectionSummary(false /* shortSummary */); 2044 } 2045 2046 /** 2047 * See {@link #getCarConnectionSummary(boolean, boolean)} 2048 */ getCarConnectionSummary(boolean shortSummary)2049 public String getCarConnectionSummary(boolean shortSummary) { 2050 return getCarConnectionSummary(shortSummary, true /* useDisconnectedString */); 2051 } 2052 2053 /** 2054 * Returns android auto string that describes the connection state of this device. 2055 * 2056 * @param shortSummary {@code true} if need to return short version summary 2057 * @param useDisconnectedString {@code true} if need to return disconnected summary string 2058 */ getCarConnectionSummary(boolean shortSummary, boolean useDisconnectedString)2059 public String getCarConnectionSummary(boolean shortSummary, boolean useDisconnectedString) { 2060 boolean profileConnected = false; // at least one profile is connected 2061 boolean a2dpNotConnected = false; // A2DP is preferred but not connected 2062 boolean hfpNotConnected = false; // HFP is preferred but not connected 2063 boolean hearingAidNotConnected = false; // Hearing Aid is preferred but not connected 2064 boolean leAudioNotConnected = false; // LeAudio is preferred but not connected 2065 2066 synchronized (mProfileLock) { 2067 for (LocalBluetoothProfile profile : getProfiles()) { 2068 int connectionStatus = getProfileConnectionState(profile); 2069 2070 switch (connectionStatus) { 2071 case BluetoothProfile.STATE_CONNECTING: 2072 case BluetoothProfile.STATE_DISCONNECTING: 2073 return mContext.getString( 2074 BluetoothUtils.getConnectionStateSummary(connectionStatus)); 2075 2076 case BluetoothProfile.STATE_CONNECTED: 2077 if (shortSummary) { 2078 return mContext.getString(BluetoothUtils.getConnectionStateSummary( 2079 connectionStatus), /* formatArgs= */ ""); 2080 } 2081 profileConnected = true; 2082 break; 2083 2084 case BluetoothProfile.STATE_DISCONNECTED: 2085 if (profile.isProfileReady()) { 2086 if (profile instanceof A2dpProfile 2087 || profile instanceof A2dpSinkProfile) { 2088 a2dpNotConnected = true; 2089 } else if (profile instanceof HeadsetProfile 2090 || profile instanceof HfpClientProfile) { 2091 hfpNotConnected = true; 2092 } else if (profile instanceof HearingAidProfile) { 2093 hearingAidNotConnected = true; 2094 } else if (profile instanceof LeAudioProfile) { 2095 leAudioNotConnected = true; 2096 } 2097 } 2098 break; 2099 } 2100 } 2101 } 2102 2103 String batteryLevelPercentageString = null; 2104 // Android framework should only set mBatteryLevel to valid range [0-100], 2105 // BluetoothDevice.BATTERY_LEVEL_BLUETOOTH_OFF, or BluetoothDevice.BATTERY_LEVEL_UNKNOWN, 2106 // any other value should be a framework bug. Thus assume here that if value is greater 2107 // than BluetoothDevice.BATTERY_LEVEL_UNKNOWN, it must be valid 2108 final int batteryLevel = getMinBatteryLevelWithMemberDevices(); 2109 if (batteryLevel > BluetoothDevice.BATTERY_LEVEL_UNKNOWN) { 2110 // TODO: name com.android.settingslib.bluetooth.Utils something different 2111 batteryLevelPercentageString = 2112 com.android.settingslib.Utils.formatPercentage(batteryLevel); 2113 } 2114 2115 // Prepare the string for the Active Device summary 2116 String[] activeDeviceStringsArray = mContext.getResources().getStringArray( 2117 R.array.bluetooth_audio_active_device_summaries); 2118 String activeDeviceString = activeDeviceStringsArray[0]; // Default value: not active 2119 if (mIsActiveDeviceA2dp && mIsActiveDeviceHeadset) { 2120 activeDeviceString = activeDeviceStringsArray[1]; // Active for Media and Phone 2121 } else { 2122 if (mIsActiveDeviceA2dp) { 2123 activeDeviceString = activeDeviceStringsArray[2]; // Active for Media only 2124 } 2125 if (mIsActiveDeviceHeadset) { 2126 activeDeviceString = activeDeviceStringsArray[3]; // Active for Phone only 2127 } 2128 } 2129 if (!hearingAidNotConnected && mIsActiveDeviceHearingAid) { 2130 activeDeviceString = activeDeviceStringsArray[1]; 2131 return mContext.getString(R.string.bluetooth_connected, activeDeviceString); 2132 } 2133 2134 if (!leAudioNotConnected && mIsActiveDeviceLeAudio) { 2135 activeDeviceString = activeDeviceStringsArray[1]; 2136 return mContext.getString(R.string.bluetooth_connected, activeDeviceString); 2137 } 2138 2139 if (profileConnected) { 2140 if (a2dpNotConnected && hfpNotConnected) { 2141 if (batteryLevelPercentageString != null) { 2142 return mContext.getString( 2143 R.string.bluetooth_connected_no_headset_no_a2dp_battery_level, 2144 batteryLevelPercentageString, activeDeviceString); 2145 } else { 2146 return mContext.getString(R.string.bluetooth_connected_no_headset_no_a2dp, 2147 activeDeviceString); 2148 } 2149 2150 } else if (a2dpNotConnected) { 2151 if (batteryLevelPercentageString != null) { 2152 return mContext.getString(R.string.bluetooth_connected_no_a2dp_battery_level, 2153 batteryLevelPercentageString, activeDeviceString); 2154 } else { 2155 return mContext.getString(R.string.bluetooth_connected_no_a2dp, 2156 activeDeviceString); 2157 } 2158 2159 } else if (hfpNotConnected) { 2160 if (batteryLevelPercentageString != null) { 2161 return mContext.getString(R.string.bluetooth_connected_no_headset_battery_level, 2162 batteryLevelPercentageString, activeDeviceString); 2163 } else { 2164 return mContext.getString(R.string.bluetooth_connected_no_headset, 2165 activeDeviceString); 2166 } 2167 } else { 2168 if (batteryLevelPercentageString != null) { 2169 return mContext.getString(R.string.bluetooth_connected_battery_level, 2170 batteryLevelPercentageString, activeDeviceString); 2171 } else { 2172 return mContext.getString(R.string.bluetooth_connected, activeDeviceString); 2173 } 2174 } 2175 } 2176 2177 if (getBondState() == BluetoothDevice.BOND_BONDING) { 2178 return mContext.getString(R.string.bluetooth_pairing); 2179 } 2180 return useDisconnectedString ? mContext.getString(R.string.bluetooth_disconnected) : null; 2181 } 2182 2183 /** 2184 * @return {@code true} if {@code cachedBluetoothDevice} is a2dp device 2185 */ isConnectedA2dpDevice()2186 public boolean isConnectedA2dpDevice() { 2187 A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile(); 2188 return a2dpProfile != null && a2dpProfile.getConnectionStatus(mDevice) == 2189 BluetoothProfile.STATE_CONNECTED; 2190 } 2191 2192 /** 2193 * @return {@code true} if {@code cachedBluetoothDevice} is HFP device 2194 */ isConnectedHfpDevice()2195 public boolean isConnectedHfpDevice() { 2196 HeadsetProfile headsetProfile = mProfileManager.getHeadsetProfile(); 2197 return headsetProfile != null && headsetProfile.getConnectionStatus(mDevice) == 2198 BluetoothProfile.STATE_CONNECTED; 2199 } 2200 2201 /** 2202 * @return {@code true} if {@code cachedBluetoothDevice} is ASHA hearing aid device 2203 */ isConnectedAshaHearingAidDevice()2204 public boolean isConnectedAshaHearingAidDevice() { 2205 HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile(); 2206 return hearingAidProfile != null && hearingAidProfile.getConnectionStatus(mDevice) == 2207 BluetoothProfile.STATE_CONNECTED; 2208 } 2209 2210 /** 2211 * @return {@code true} if {@code cachedBluetoothDevice} is HAP device 2212 */ isConnectedHapClientDevice()2213 public boolean isConnectedHapClientDevice() { 2214 HapClientProfile hapClientProfile = mProfileManager.getHapClientProfile(); 2215 return hapClientProfile != null && hapClientProfile.getConnectionStatus(mDevice) 2216 == BluetoothProfile.STATE_CONNECTED; 2217 } 2218 2219 /** 2220 * @return {@code true} if {@code cachedBluetoothDevice} is hearing aid device 2221 * 2222 * The device may be an ASHA hearing aid that supports {@link HearingAidProfile} or a LeAudio 2223 * hearing aid that supports {@link HapClientProfile} and {@link LeAudioProfile}. 2224 */ isConnectedHearingAidDevice()2225 public boolean isConnectedHearingAidDevice() { 2226 return isConnectedAshaHearingAidDevice() || isConnectedLeAudioHearingAidDevice(); 2227 } 2228 2229 /** 2230 * @return {@code true} if {@code cachedBluetoothDevice} is LeAudio hearing aid device 2231 */ isConnectedLeAudioHearingAidDevice()2232 public boolean isConnectedLeAudioHearingAidDevice() { 2233 return isConnectedHapClientDevice() && isConnectedLeAudioDevice(); 2234 } 2235 2236 /** 2237 * @return {@code true} if {@code cachedBluetoothDevice} is LeAudio device 2238 */ isConnectedLeAudioDevice()2239 public boolean isConnectedLeAudioDevice() { 2240 LeAudioProfile leAudio = mProfileManager.getLeAudioProfile(); 2241 return leAudio != null && leAudio.getConnectionStatus(mDevice) == 2242 BluetoothProfile.STATE_CONNECTED; 2243 } 2244 2245 /** 2246 * @return {@code true} if {@code cachedBluetoothDevice} has member which is LeAudio device 2247 */ hasConnectedLeAudioMemberDevice()2248 public boolean hasConnectedLeAudioMemberDevice() { 2249 LeAudioProfile leAudio = mProfileManager.getLeAudioProfile(); 2250 return leAudio != null && getMemberDevice().stream().anyMatch( 2251 cachedDevice -> cachedDevice != null && cachedDevice.getDevice() != null 2252 && leAudio.getConnectionStatus(cachedDevice.getDevice()) 2253 == BluetoothProfile.STATE_CONNECTED); 2254 } 2255 2256 /** 2257 * @return {@code true} if {@code cachedBluetoothDevice} supports broadcast assistant profile 2258 */ isConnectedLeAudioBroadcastAssistantDevice()2259 public boolean isConnectedLeAudioBroadcastAssistantDevice() { 2260 LocalBluetoothLeBroadcastAssistant leBroadcastAssistant = 2261 mProfileManager.getLeAudioBroadcastAssistantProfile(); 2262 return leBroadcastAssistant != null && leBroadcastAssistant.getConnectionStatus(mDevice) 2263 == BluetoothProfile.STATE_CONNECTED; 2264 } 2265 2266 /** 2267 * @return {@code true} if {@code cachedBluetoothDevice} supports volume control profile 2268 */ isConnectedVolumeControlDevice()2269 public boolean isConnectedVolumeControlDevice() { 2270 VolumeControlProfile volumeControl = mProfileManager.getVolumeControlProfile(); 2271 return volumeControl != null && volumeControl.getConnectionStatus(mDevice) 2272 == BluetoothProfile.STATE_CONNECTED; 2273 } 2274 isConnectedSapDevice()2275 private boolean isConnectedSapDevice() { 2276 SapProfile sapProfile = mProfileManager.getSapProfile(); 2277 return sapProfile != null && sapProfile.getConnectionStatus(mDevice) 2278 == BluetoothProfile.STATE_CONNECTED; 2279 } 2280 getSubDevice()2281 public CachedBluetoothDevice getSubDevice() { 2282 return mSubDevice; 2283 } 2284 setSubDevice(CachedBluetoothDevice subDevice)2285 public void setSubDevice(CachedBluetoothDevice subDevice) { 2286 mSubDevice = subDevice; 2287 } 2288 switchSubDeviceContent()2289 public void switchSubDeviceContent() { 2290 // Backup from main device 2291 BluetoothDevice tmpDevice = mDevice; 2292 final short tmpRssi = mRssi; 2293 final boolean tmpJustDiscovered = mJustDiscovered; 2294 final HearingAidInfo tmpHearingAidInfo = mHearingAidInfo; 2295 // Set main device from sub device 2296 release(); 2297 mDevice = mSubDevice.mDevice; 2298 mRssi = mSubDevice.mRssi; 2299 mJustDiscovered = mSubDevice.mJustDiscovered; 2300 mHearingAidInfo = mSubDevice.mHearingAidInfo; 2301 // Set sub device from backup 2302 mSubDevice.release(); 2303 mSubDevice.mDevice = tmpDevice; 2304 mSubDevice.mRssi = tmpRssi; 2305 mSubDevice.mJustDiscovered = tmpJustDiscovered; 2306 mSubDevice.mHearingAidInfo = tmpHearingAidInfo; 2307 fetchActiveDevices(); 2308 } 2309 2310 /** 2311 * @return a set of member devices that are in the same coordinated set with this device. 2312 */ getMemberDevice()2313 public Set<CachedBluetoothDevice> getMemberDevice() { 2314 return mMemberDevices; 2315 } 2316 2317 /** 2318 * Store the member devices that are in the same coordinated set. 2319 */ addMemberDevice(CachedBluetoothDevice memberDevice)2320 public void addMemberDevice(CachedBluetoothDevice memberDevice) { 2321 Log.d(TAG, this + " addMemberDevice = " + memberDevice); 2322 mMemberDevices.add(memberDevice); 2323 } 2324 2325 /** 2326 * Remove a device from the member device sets. 2327 */ removeMemberDevice(CachedBluetoothDevice memberDevice)2328 public void removeMemberDevice(CachedBluetoothDevice memberDevice) { 2329 memberDevice.release(); 2330 mMemberDevices.remove(memberDevice); 2331 } 2332 2333 /** 2334 * In order to show the preference for the whole group, we always set the main device as the 2335 * first connected device in the coordinated set, and then switch the content of the main 2336 * device and member devices. 2337 * 2338 * @param newMainDevice the new Main device which is from the previous main device's member 2339 * list. 2340 */ switchMemberDeviceContent(CachedBluetoothDevice newMainDevice)2341 public void switchMemberDeviceContent(CachedBluetoothDevice newMainDevice) { 2342 // Remove the sub device from mMemberDevices first to prevent hash mismatch problem due 2343 // to mDevice switch 2344 removeMemberDevice(newMainDevice); 2345 2346 // Backup from current main device 2347 final BluetoothDevice tmpDevice = mDevice; 2348 final short tmpRssi = mRssi; 2349 final boolean tmpJustDiscovered = mJustDiscovered; 2350 final HearingAidInfo tmpHearingAidInfo = mHearingAidInfo; 2351 2352 // Set main device from sub device 2353 release(); 2354 mDevice = newMainDevice.mDevice; 2355 mRssi = newMainDevice.mRssi; 2356 mJustDiscovered = newMainDevice.mJustDiscovered; 2357 mHearingAidInfo = newMainDevice.mHearingAidInfo; 2358 fillData(); 2359 2360 // Set sub device from backup 2361 newMainDevice.release(); 2362 newMainDevice.mDevice = tmpDevice; 2363 newMainDevice.mRssi = tmpRssi; 2364 newMainDevice.mJustDiscovered = tmpJustDiscovered; 2365 newMainDevice.mHearingAidInfo = tmpHearingAidInfo; 2366 newMainDevice.fillData(); 2367 2368 // Add the sub device back into mMemberDevices with correct hash 2369 addMemberDevice(newMainDevice); 2370 } 2371 2372 /** 2373 * Get cached bluetooth icon with description 2374 */ getDrawableWithDescription()2375 public Pair<Drawable, String> getDrawableWithDescription() { 2376 Uri uri = BluetoothUtils.getUriMetaData(mDevice, BluetoothDevice.METADATA_MAIN_ICON); 2377 Pair<Drawable, String> pair = BluetoothUtils.getBtClassDrawableWithDescription( 2378 mContext, this); 2379 2380 if (BluetoothUtils.isAdvancedDetailsHeader(mDevice) && uri != null) { 2381 BitmapDrawable drawable = mDrawableCache.get(uri.toString()); 2382 if (drawable != null) { 2383 Resources resources = mContext.getResources(); 2384 return new Pair<>(new AdaptiveOutlineDrawable( 2385 resources, drawable.getBitmap()), pair.second); 2386 } 2387 2388 refresh(); 2389 } 2390 2391 return BluetoothUtils.getBtRainbowDrawableWithDescription(mContext, this); 2392 } 2393 releaseLruCache()2394 void releaseLruCache() { 2395 mDrawableCache.evictAll(); 2396 } 2397 getUnpairing()2398 boolean getUnpairing() { 2399 return mUnpairing; 2400 } 2401 2402 @VisibleForTesting setLocalBluetoothManager(LocalBluetoothManager bluetoothManager)2403 void setLocalBluetoothManager(LocalBluetoothManager bluetoothManager) { 2404 mBluetoothManager = bluetoothManager; 2405 } 2406 2407 @VisibleForTesting setIsDeviceStylus(Boolean isDeviceStylus)2408 void setIsDeviceStylus(Boolean isDeviceStylus) { 2409 mIsDeviceStylus = isDeviceStylus; 2410 } 2411 2412 @VisibleForTesting setInputDevice(@ullable InputDevice inputDevice)2413 void setInputDevice(@Nullable InputDevice inputDevice) { 2414 mInputDevice = inputDevice; 2415 } 2416 isAndroidAuto()2417 private boolean isAndroidAuto() { 2418 try { 2419 ParcelUuid[] uuids = mDevice.getUuids(); 2420 if (ArrayUtils.contains(uuids, ANDROID_AUTO_UUID)) { 2421 return true; 2422 } 2423 } catch (RuntimeException e) { 2424 Log.w(TAG, "Fail to check isAndroidAuto for " + this); 2425 } 2426 return false; 2427 } 2428 } 2429