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 android.bluetooth.BluetoothAdapter; 20 import android.bluetooth.BluetoothClass; 21 import android.bluetooth.BluetoothCsipSetCoordinator; 22 import android.bluetooth.BluetoothDevice; 23 import android.bluetooth.BluetoothHearingAid; 24 import android.bluetooth.BluetoothProfile; 25 import android.bluetooth.BluetoothUuid; 26 import android.content.Context; 27 import android.content.SharedPreferences; 28 import android.content.res.Resources; 29 import android.graphics.drawable.BitmapDrawable; 30 import android.graphics.drawable.Drawable; 31 import android.net.Uri; 32 import android.os.Handler; 33 import android.os.Looper; 34 import android.os.Message; 35 import android.os.ParcelUuid; 36 import android.os.SystemClock; 37 import android.text.TextUtils; 38 import android.util.Log; 39 import android.util.LruCache; 40 import android.util.Pair; 41 42 import androidx.annotation.VisibleForTesting; 43 44 import com.android.internal.util.ArrayUtils; 45 import com.android.settingslib.R; 46 import com.android.settingslib.Utils; 47 import com.android.settingslib.utils.ThreadUtils; 48 import com.android.settingslib.widget.AdaptiveOutlineDrawable; 49 50 import java.sql.Timestamp; 51 import java.util.ArrayList; 52 import java.util.Collection; 53 import java.util.HashSet; 54 import java.util.List; 55 import java.util.Set; 56 import java.util.concurrent.CopyOnWriteArrayList; 57 import java.util.stream.Stream; 58 59 /** 60 * CachedBluetoothDevice represents a remote Bluetooth device. It contains 61 * attributes of the device (such as the address, name, RSSI, etc.) and 62 * functionality that can be performed on the device (connect, pair, disconnect, 63 * etc.). 64 */ 65 public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> { 66 private static final String TAG = "CachedBluetoothDevice"; 67 68 // See mConnectAttempted 69 private static final long MAX_UUID_DELAY_FOR_AUTO_CONNECT = 5000; 70 // Some Hearing Aids (especially the 2nd device) needs more time to do service discovery 71 private static final long MAX_HEARING_AIDS_DELAY_FOR_AUTO_CONNECT = 15000; 72 private static final long MAX_HOGP_DELAY_FOR_AUTO_CONNECT = 30000; 73 private static final long MAX_LEAUDIO_DELAY_FOR_AUTO_CONNECT = 30000; 74 private static final long MAX_MEDIA_PROFILE_CONNECT_DELAY = 60000; 75 76 private final Context mContext; 77 private final BluetoothAdapter mLocalAdapter; 78 private final LocalBluetoothProfileManager mProfileManager; 79 private final Object mProfileLock = new Object(); 80 BluetoothDevice mDevice; 81 private HearingAidInfo mHearingAidInfo; 82 private int mGroupId; 83 private Timestamp mBondTimestamp; 84 85 // Need this since there is no method for getting RSSI 86 short mRssi; 87 88 // mProfiles and mRemovedProfiles does not do swap() between main and sub device. It is 89 // because current sub device is only for HearingAid and its profile is the same. 90 private final Collection<LocalBluetoothProfile> mProfiles = new CopyOnWriteArrayList<>(); 91 92 // List of profiles that were previously in mProfiles, but have been removed 93 private final Collection<LocalBluetoothProfile> mRemovedProfiles = new CopyOnWriteArrayList<>(); 94 95 // Device supports PANU but not NAP: remove PanProfile after device disconnects from NAP 96 private boolean mLocalNapRoleConnected; 97 98 boolean mJustDiscovered; 99 100 boolean mIsCoordinatedSetMember = false; 101 102 private final Collection<Callback> mCallbacks = new CopyOnWriteArrayList<>(); 103 104 /** 105 * Last time a bt profile auto-connect was attempted. 106 * If an ACTION_UUID intent comes in within 107 * MAX_UUID_DELAY_FOR_AUTO_CONNECT milliseconds, we will try auto-connect 108 * again with the new UUIDs 109 */ 110 private long mConnectAttempted; 111 112 // Active device state 113 private boolean mIsActiveDeviceA2dp = false; 114 private boolean mIsActiveDeviceHeadset = false; 115 private boolean mIsActiveDeviceHearingAid = false; 116 private boolean mIsActiveDeviceLeAudio = false; 117 // Media profile connect state 118 private boolean mIsA2dpProfileConnectedFail = false; 119 private boolean mIsHeadsetProfileConnectedFail = false; 120 private boolean mIsHearingAidProfileConnectedFail = false; 121 private boolean mIsLeAudioProfileConnectedFail = false; 122 private boolean mUnpairing; 123 124 // Group second device for Hearing Aid 125 private CachedBluetoothDevice mSubDevice; 126 // Group member devices for the coordinated set 127 private Set<CachedBluetoothDevice> mMemberDevices = new HashSet<CachedBluetoothDevice>(); 128 @VisibleForTesting 129 LruCache<String, BitmapDrawable> mDrawableCache; 130 131 private final Handler mHandler = new Handler(Looper.getMainLooper()) { 132 @Override 133 public void handleMessage(Message msg) { 134 switch (msg.what) { 135 case BluetoothProfile.A2DP: 136 mIsA2dpProfileConnectedFail = true; 137 break; 138 case BluetoothProfile.HEADSET: 139 mIsHeadsetProfileConnectedFail = true; 140 break; 141 case BluetoothProfile.HEARING_AID: 142 mIsHearingAidProfileConnectedFail = true; 143 break; 144 case BluetoothProfile.LE_AUDIO: 145 mIsLeAudioProfileConnectedFail = true; 146 break; 147 default: 148 Log.w(TAG, "handleMessage(): unknown message : " + msg.what); 149 break; 150 } 151 Log.w(TAG, "Connect to profile : " + msg.what + " timeout, show error message !"); 152 refresh(); 153 } 154 }; 155 CachedBluetoothDevice(Context context, LocalBluetoothProfileManager profileManager, BluetoothDevice device)156 CachedBluetoothDevice(Context context, LocalBluetoothProfileManager profileManager, 157 BluetoothDevice device) { 158 mContext = context; 159 mLocalAdapter = BluetoothAdapter.getDefaultAdapter(); 160 mProfileManager = profileManager; 161 mDevice = device; 162 fillData(); 163 mGroupId = BluetoothCsipSetCoordinator.GROUP_ID_INVALID; 164 initDrawableCache(); 165 mUnpairing = false; 166 } 167 168 /** Clears any pending messages in the message queue. */ release()169 public void release() { 170 mHandler.removeCallbacksAndMessages(null); 171 } 172 initDrawableCache()173 private void initDrawableCache() { 174 int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); 175 int cacheSize = maxMemory / 8; 176 177 mDrawableCache = new LruCache<String, BitmapDrawable>(cacheSize) { 178 @Override 179 protected int sizeOf(String key, BitmapDrawable bitmap) { 180 return bitmap.getBitmap().getByteCount() / 1024; 181 } 182 }; 183 } 184 185 /** 186 * Describes the current device and profile for logging. 187 * 188 * @param profile Profile to describe 189 * @return Description of the device and profile 190 */ describe(LocalBluetoothProfile profile)191 private String describe(LocalBluetoothProfile profile) { 192 StringBuilder sb = new StringBuilder(); 193 sb.append("Address:").append(mDevice); 194 if (profile != null) { 195 sb.append(" Profile:").append(profile); 196 } 197 198 return sb.toString(); 199 } 200 onProfileStateChanged(LocalBluetoothProfile profile, int newProfileState)201 void onProfileStateChanged(LocalBluetoothProfile profile, int newProfileState) { 202 if (BluetoothUtils.D) { 203 Log.d(TAG, "onProfileStateChanged: profile " + profile + ", device " 204 + mDevice.getAnonymizedAddress() + ", newProfileState " + newProfileState); 205 } 206 if (mLocalAdapter.getState() == BluetoothAdapter.STATE_TURNING_OFF) 207 { 208 if (BluetoothUtils.D) { 209 Log.d(TAG, " BT Turninig Off...Profile conn state change ignored..."); 210 } 211 return; 212 } 213 214 synchronized (mProfileLock) { 215 if (profile instanceof A2dpProfile || profile instanceof HeadsetProfile 216 || profile instanceof HearingAidProfile || profile instanceof LeAudioProfile) { 217 setProfileConnectedStatus(profile.getProfileId(), false); 218 switch (newProfileState) { 219 case BluetoothProfile.STATE_CONNECTED: 220 mHandler.removeMessages(profile.getProfileId()); 221 break; 222 case BluetoothProfile.STATE_CONNECTING: 223 mHandler.sendEmptyMessageDelayed(profile.getProfileId(), 224 MAX_MEDIA_PROFILE_CONNECT_DELAY); 225 break; 226 case BluetoothProfile.STATE_DISCONNECTING: 227 if (mHandler.hasMessages(profile.getProfileId())) { 228 mHandler.removeMessages(profile.getProfileId()); 229 } 230 break; 231 case BluetoothProfile.STATE_DISCONNECTED: 232 if (mHandler.hasMessages(profile.getProfileId())) { 233 mHandler.removeMessages(profile.getProfileId()); 234 if (profile.getConnectionPolicy(mDevice) > 235 BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) { 236 /* 237 * If we received state DISCONNECTED and previous state was 238 * CONNECTING and connection policy is FORBIDDEN or UNKNOWN 239 * then it's not really a failure to connect. 240 * 241 * Connection profile is considered as failed when connection 242 * policy indicates that profile should be connected 243 * but it got disconnected. 244 */ 245 Log.w(TAG, "onProfileStateChanged(): Failed to connect profile"); 246 setProfileConnectedStatus(profile.getProfileId(), true); 247 } 248 } 249 break; 250 default: 251 Log.w(TAG, "onProfileStateChanged(): unknown profile state : " 252 + newProfileState); 253 break; 254 } 255 } 256 257 if (newProfileState == BluetoothProfile.STATE_CONNECTED) { 258 if (profile instanceof MapProfile) { 259 profile.setEnabled(mDevice, true); 260 } 261 if (!mProfiles.contains(profile)) { 262 mRemovedProfiles.remove(profile); 263 mProfiles.add(profile); 264 if (profile instanceof PanProfile 265 && ((PanProfile) profile).isLocalRoleNap(mDevice)) { 266 // Device doesn't support NAP, so remove PanProfile on disconnect 267 mLocalNapRoleConnected = true; 268 } 269 } 270 } else if (profile instanceof MapProfile 271 && newProfileState == BluetoothProfile.STATE_DISCONNECTED) { 272 profile.setEnabled(mDevice, false); 273 } else if (mLocalNapRoleConnected && profile instanceof PanProfile 274 && ((PanProfile) profile).isLocalRoleNap(mDevice) 275 && newProfileState == BluetoothProfile.STATE_DISCONNECTED) { 276 Log.d(TAG, "Removing PanProfile from device after NAP disconnect"); 277 mProfiles.remove(profile); 278 mRemovedProfiles.add(profile); 279 mLocalNapRoleConnected = false; 280 } 281 } 282 283 fetchActiveDevices(); 284 } 285 286 @VisibleForTesting setProfileConnectedStatus(int profileId, boolean isFailed)287 void setProfileConnectedStatus(int profileId, boolean isFailed) { 288 switch (profileId) { 289 case BluetoothProfile.A2DP: 290 mIsA2dpProfileConnectedFail = isFailed; 291 break; 292 case BluetoothProfile.HEADSET: 293 mIsHeadsetProfileConnectedFail = isFailed; 294 break; 295 case BluetoothProfile.HEARING_AID: 296 mIsHearingAidProfileConnectedFail = isFailed; 297 break; 298 case BluetoothProfile.LE_AUDIO: 299 mIsLeAudioProfileConnectedFail = isFailed; 300 break; 301 default: 302 Log.w(TAG, "setProfileConnectedStatus(): unknown profile id : " + profileId); 303 break; 304 } 305 } 306 disconnect()307 public void disconnect() { 308 synchronized (mProfileLock) { 309 if (getGroupId() != BluetoothCsipSetCoordinator.GROUP_ID_INVALID) { 310 for (CachedBluetoothDevice member : getMemberDevice()) { 311 Log.d(TAG, "Disconnect the member:" + member); 312 member.disconnect(); 313 } 314 } 315 Log.d(TAG, "Disconnect " + this); 316 mDevice.disconnect(); 317 } 318 // Disconnect PBAP server in case its connected 319 // This is to ensure all the profiles are disconnected as some CK/Hs do not 320 // disconnect PBAP connection when HF connection is brought down 321 PbapServerProfile PbapProfile = mProfileManager.getPbapProfile(); 322 if (PbapProfile != null && isConnectedProfile(PbapProfile)) 323 { 324 PbapProfile.setEnabled(mDevice, false); 325 } 326 } 327 disconnect(LocalBluetoothProfile profile)328 public void disconnect(LocalBluetoothProfile profile) { 329 if (profile.setEnabled(mDevice, false)) { 330 if (BluetoothUtils.D) { 331 Log.d(TAG, "Command sent successfully:DISCONNECT " + describe(profile)); 332 } 333 } 334 } 335 336 /** 337 * Connect this device. 338 * 339 * @param connectAllProfiles {@code true} to connect all profile, {@code false} otherwise. 340 * 341 * @deprecated use {@link #connect()} instead. 342 */ 343 @Deprecated connect(boolean connectAllProfiles)344 public void connect(boolean connectAllProfiles) { 345 connect(); 346 } 347 348 /** 349 * Connect this device. 350 */ connect()351 public void connect() { 352 if (!ensurePaired()) { 353 return; 354 } 355 356 mConnectAttempted = SystemClock.elapsedRealtime(); 357 connectDevice(); 358 } 359 setHearingAidInfo(HearingAidInfo hearingAidInfo)360 void setHearingAidInfo(HearingAidInfo hearingAidInfo) { 361 mHearingAidInfo = hearingAidInfo; 362 } 363 364 /** 365 * @return {@code true} if {@code cachedBluetoothDevice} is hearing aid device 366 */ isHearingAidDevice()367 public boolean isHearingAidDevice() { 368 return mHearingAidInfo != null; 369 } 370 getDeviceSide()371 public int getDeviceSide() { 372 return mHearingAidInfo != null 373 ? mHearingAidInfo.getSide() : HearingAidInfo.DeviceSide.SIDE_INVALID; 374 } 375 getDeviceMode()376 public int getDeviceMode() { 377 return mHearingAidInfo != null 378 ? mHearingAidInfo.getMode() : HearingAidInfo.DeviceMode.MODE_INVALID; 379 } 380 getHiSyncId()381 public long getHiSyncId() { 382 return mHearingAidInfo != null 383 ? mHearingAidInfo.getHiSyncId() : BluetoothHearingAid.HI_SYNC_ID_INVALID; 384 } 385 386 /** 387 * Mark the discovered device as member of coordinated set. 388 * 389 * @param isCoordinatedSetMember {@code true}, if the device is a member of a coordinated set. 390 */ setIsCoordinatedSetMember(boolean isCoordinatedSetMember)391 public void setIsCoordinatedSetMember(boolean isCoordinatedSetMember) { 392 mIsCoordinatedSetMember = isCoordinatedSetMember; 393 } 394 395 /** 396 * Check if the device is a CSIP member device. 397 * 398 * @return {@code true}, if this device supports CSIP, otherwise returns {@code false}. 399 */ isCoordinatedSetMemberDevice()400 public boolean isCoordinatedSetMemberDevice() { 401 return mIsCoordinatedSetMember; 402 } 403 404 /** 405 * Get the coordinated set group id. 406 * 407 * @return the group id. 408 */ getGroupId()409 public int getGroupId() { 410 return mGroupId; 411 } 412 413 /** 414 * Set the coordinated set group id. 415 * 416 * @param id the group id from the CSIP. 417 */ setGroupId(int id)418 public void setGroupId(int id) { 419 Log.d(TAG, this.getDevice().getAnonymizedAddress() + " set GroupId " + id); 420 mGroupId = id; 421 } 422 onBondingDockConnect()423 void onBondingDockConnect() { 424 // Attempt to connect if UUIDs are available. Otherwise, 425 // we will connect when the ACTION_UUID intent arrives. 426 connect(); 427 } 428 connectDevice()429 private void connectDevice() { 430 synchronized (mProfileLock) { 431 // Try to initialize the profiles if they were not. 432 if (mProfiles.isEmpty()) { 433 // if mProfiles is empty, then do not invoke updateProfiles. This causes a race 434 // condition with carkits during pairing, wherein RemoteDevice.UUIDs have been 435 // updated from bluetooth stack but ACTION.uuid is not sent yet. 436 // Eventually ACTION.uuid will be received which shall trigger the connection of the 437 // various profiles 438 // If UUIDs are not available yet, connect will be happen 439 // upon arrival of the ACTION_UUID intent. 440 Log.d(TAG, "No profiles. Maybe we will connect later for device " + mDevice); 441 return; 442 } 443 Log.d(TAG, "connect " + this); 444 mDevice.connect(); 445 if (getGroupId() != BluetoothCsipSetCoordinator.GROUP_ID_INVALID) { 446 for (CachedBluetoothDevice member : getMemberDevice()) { 447 Log.d(TAG, "connect the member:" + member); 448 member.connect(); 449 } 450 } 451 } 452 } 453 454 /** 455 * Connect this device to the specified profile. 456 * 457 * @param profile the profile to use with the remote device 458 */ connectProfile(LocalBluetoothProfile profile)459 public void connectProfile(LocalBluetoothProfile profile) { 460 mConnectAttempted = SystemClock.elapsedRealtime(); 461 connectInt(profile); 462 // Refresh the UI based on profile.connect() call 463 refresh(); 464 } 465 connectInt(LocalBluetoothProfile profile)466 synchronized void connectInt(LocalBluetoothProfile profile) { 467 if (!ensurePaired()) { 468 return; 469 } 470 if (profile.setEnabled(mDevice, true)) { 471 if (BluetoothUtils.D) { 472 Log.d(TAG, "Command sent successfully:CONNECT " + describe(profile)); 473 } 474 return; 475 } 476 Log.i(TAG, "Failed to connect " + profile.toString() + " to " + getName()); 477 } 478 ensurePaired()479 private boolean ensurePaired() { 480 if (getBondState() == BluetoothDevice.BOND_NONE) { 481 startPairing(); 482 return false; 483 } else { 484 return true; 485 } 486 } 487 startPairing()488 public boolean startPairing() { 489 // Pairing is unreliable while scanning, so cancel discovery 490 if (mLocalAdapter.isDiscovering()) { 491 mLocalAdapter.cancelDiscovery(); 492 } 493 494 if (!mDevice.createBond()) { 495 return false; 496 } 497 498 return true; 499 } 500 unpair()501 public void unpair() { 502 int state = getBondState(); 503 504 if (state == BluetoothDevice.BOND_BONDING) { 505 mDevice.cancelBondProcess(); 506 } 507 508 if (state != BluetoothDevice.BOND_NONE) { 509 final BluetoothDevice dev = mDevice; 510 if (dev != null) { 511 mUnpairing = true; 512 final boolean successful = dev.removeBond(); 513 if (successful) { 514 releaseLruCache(); 515 if (BluetoothUtils.D) { 516 Log.d(TAG, "Command sent successfully:REMOVE_BOND " + describe(null)); 517 } 518 } else if (BluetoothUtils.V) { 519 Log.v(TAG, "Framework rejected command immediately:REMOVE_BOND " + 520 describe(null)); 521 } 522 } 523 } 524 } 525 getProfileConnectionState(LocalBluetoothProfile profile)526 public int getProfileConnectionState(LocalBluetoothProfile profile) { 527 return profile != null 528 ? profile.getConnectionStatus(mDevice) 529 : BluetoothProfile.STATE_DISCONNECTED; 530 } 531 532 // TODO: do any of these need to run async on a background thread? fillData()533 void fillData() { 534 updateProfiles(); 535 fetchActiveDevices(); 536 migratePhonebookPermissionChoice(); 537 migrateMessagePermissionChoice(); 538 539 dispatchAttributesChanged(); 540 } 541 getDevice()542 public BluetoothDevice getDevice() { 543 return mDevice; 544 } 545 546 /** 547 * Convenience method that can be mocked - it lets tests avoid having to call getDevice() which 548 * causes problems in tests since BluetoothDevice is final and cannot be mocked. 549 * @return the address of this device 550 */ getAddress()551 public String getAddress() { 552 return mDevice.getAddress(); 553 } 554 555 /** 556 * Get identity address from remote device 557 * @return {@link BluetoothDevice#getIdentityAddress()} if 558 * {@link BluetoothDevice#getIdentityAddress()} is not null otherwise return 559 * {@link BluetoothDevice#getAddress()} 560 */ getIdentityAddress()561 public String getIdentityAddress() { 562 final String identityAddress = mDevice.getIdentityAddress(); 563 return TextUtils.isEmpty(identityAddress) ? getAddress() : identityAddress; 564 } 565 566 /** 567 * Get name from remote device 568 * @return {@link BluetoothDevice#getAlias()} if 569 * {@link BluetoothDevice#getAlias()} is not null otherwise return 570 * {@link BluetoothDevice#getAddress()} 571 */ getName()572 public String getName() { 573 final String aliasName = mDevice.getAlias(); 574 return TextUtils.isEmpty(aliasName) ? getAddress() : aliasName; 575 } 576 577 /** 578 * User changes the device name 579 * @param name new alias name to be set, should never be null 580 */ setName(String name)581 public void setName(String name) { 582 // Prevent getName() to be set to null if setName(null) is called 583 if (TextUtils.isEmpty(name) || TextUtils.equals(name, getName())) { 584 return; 585 } 586 mDevice.setAlias(name); 587 dispatchAttributesChanged(); 588 589 for (CachedBluetoothDevice cbd : mMemberDevices) { 590 cbd.setName(name); 591 } 592 } 593 594 /** 595 * Set this device as active device 596 * @return true if at least one profile on this device is set to active, false otherwise 597 */ setActive()598 public boolean setActive() { 599 boolean result = false; 600 A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile(); 601 if (a2dpProfile != null && isConnectedProfile(a2dpProfile)) { 602 if (a2dpProfile.setActiveDevice(getDevice())) { 603 Log.i(TAG, "OnPreferenceClickListener: A2DP active device=" + this); 604 result = true; 605 } 606 } 607 HeadsetProfile headsetProfile = mProfileManager.getHeadsetProfile(); 608 if ((headsetProfile != null) && isConnectedProfile(headsetProfile)) { 609 if (headsetProfile.setActiveDevice(getDevice())) { 610 Log.i(TAG, "OnPreferenceClickListener: Headset active device=" + this); 611 result = true; 612 } 613 } 614 HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile(); 615 if ((hearingAidProfile != null) && isConnectedProfile(hearingAidProfile)) { 616 if (hearingAidProfile.setActiveDevice(getDevice())) { 617 Log.i(TAG, "OnPreferenceClickListener: Hearing Aid active device=" + this); 618 result = true; 619 } 620 } 621 LeAudioProfile leAudioProfile = mProfileManager.getLeAudioProfile(); 622 if ((leAudioProfile != null) && isConnectedProfile(leAudioProfile)) { 623 if (leAudioProfile.setActiveDevice(getDevice())) { 624 Log.i(TAG, "OnPreferenceClickListener: LeAudio active device=" + this); 625 result = true; 626 } 627 } 628 return result; 629 } 630 refreshName()631 void refreshName() { 632 if (BluetoothUtils.D) { 633 Log.d(TAG, "Device name: " + getName()); 634 } 635 dispatchAttributesChanged(); 636 } 637 638 /** 639 * Checks if device has a human readable name besides MAC address 640 * @return true if device's alias name is not null nor empty, false otherwise 641 */ hasHumanReadableName()642 public boolean hasHumanReadableName() { 643 return !TextUtils.isEmpty(mDevice.getAlias()); 644 } 645 646 /** 647 * Get battery level from remote device 648 * @return battery level in percentage [0-100], 649 * {@link BluetoothDevice#BATTERY_LEVEL_BLUETOOTH_OFF}, or 650 * {@link BluetoothDevice#BATTERY_LEVEL_UNKNOWN} 651 */ getBatteryLevel()652 public int getBatteryLevel() { 653 return mDevice.getBatteryLevel(); 654 } 655 656 /** 657 * Get the lowest battery level from remote device and its member devices 658 * @return battery level in percentage [0-100] or 659 * {@link BluetoothDevice#BATTERY_LEVEL_UNKNOWN} 660 */ getMinBatteryLevelWithMemberDevices()661 public int getMinBatteryLevelWithMemberDevices() { 662 return Stream.concat(Stream.of(this), mMemberDevices.stream()) 663 .mapToInt(cachedDevice -> cachedDevice.getBatteryLevel()) 664 .filter(batteryLevel -> batteryLevel > BluetoothDevice.BATTERY_LEVEL_UNKNOWN) 665 .min() 666 .orElse(BluetoothDevice.BATTERY_LEVEL_UNKNOWN); 667 } 668 669 refresh()670 void refresh() { 671 ThreadUtils.postOnBackgroundThread(() -> { 672 if (BluetoothUtils.isAdvancedDetailsHeader(mDevice)) { 673 Uri uri = BluetoothUtils.getUriMetaData(getDevice(), 674 BluetoothDevice.METADATA_MAIN_ICON); 675 if (uri != null && mDrawableCache.get(uri.toString()) == null) { 676 mDrawableCache.put(uri.toString(), 677 (BitmapDrawable) BluetoothUtils.getBtDrawableWithDescription( 678 mContext, this).first); 679 } 680 } 681 682 ThreadUtils.postOnMainThread(() -> { 683 dispatchAttributesChanged(); 684 }); 685 }); 686 } 687 setJustDiscovered(boolean justDiscovered)688 public void setJustDiscovered(boolean justDiscovered) { 689 if (mJustDiscovered != justDiscovered) { 690 mJustDiscovered = justDiscovered; 691 dispatchAttributesChanged(); 692 } 693 } 694 getBondState()695 public int getBondState() { 696 return mDevice.getBondState(); 697 } 698 699 /** 700 * Update the device status as active or non-active per Bluetooth profile. 701 * 702 * @param isActive true if the device is active 703 * @param bluetoothProfile the Bluetooth profile 704 */ onActiveDeviceChanged(boolean isActive, int bluetoothProfile)705 public void onActiveDeviceChanged(boolean isActive, int bluetoothProfile) { 706 if (BluetoothUtils.D) { 707 Log.d(TAG, "onActiveDeviceChanged: " 708 + "profile " + BluetoothProfile.getProfileName(bluetoothProfile) 709 + ", device " + mDevice.getAnonymizedAddress() 710 + ", isActive " + isActive); 711 } 712 boolean changed = false; 713 switch (bluetoothProfile) { 714 case BluetoothProfile.A2DP: 715 changed = (mIsActiveDeviceA2dp != isActive); 716 mIsActiveDeviceA2dp = isActive; 717 break; 718 case BluetoothProfile.HEADSET: 719 changed = (mIsActiveDeviceHeadset != isActive); 720 mIsActiveDeviceHeadset = isActive; 721 break; 722 case BluetoothProfile.HEARING_AID: 723 changed = (mIsActiveDeviceHearingAid != isActive); 724 mIsActiveDeviceHearingAid = isActive; 725 break; 726 case BluetoothProfile.LE_AUDIO: 727 changed = (mIsActiveDeviceLeAudio != isActive); 728 mIsActiveDeviceLeAudio = isActive; 729 break; 730 default: 731 Log.w(TAG, "onActiveDeviceChanged: unknown profile " + bluetoothProfile + 732 " isActive " + isActive); 733 break; 734 } 735 if (changed) { 736 dispatchAttributesChanged(); 737 } 738 } 739 740 /** 741 * Update the profile audio state. 742 */ onAudioModeChanged()743 void onAudioModeChanged() { 744 dispatchAttributesChanged(); 745 } 746 747 /** 748 * Notify that the audio category has changed. 749 */ onAudioDeviceCategoryChanged()750 public void onAudioDeviceCategoryChanged() { 751 dispatchAttributesChanged(); 752 } 753 754 /** 755 * Get the device status as active or non-active per Bluetooth profile. 756 * 757 * @param bluetoothProfile the Bluetooth profile 758 * @return true if the device is active 759 */ 760 @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) isActiveDevice(int bluetoothProfile)761 public boolean isActiveDevice(int bluetoothProfile) { 762 switch (bluetoothProfile) { 763 case BluetoothProfile.A2DP: 764 return mIsActiveDeviceA2dp; 765 case BluetoothProfile.HEADSET: 766 return mIsActiveDeviceHeadset; 767 case BluetoothProfile.HEARING_AID: 768 return mIsActiveDeviceHearingAid; 769 case BluetoothProfile.LE_AUDIO: 770 return mIsActiveDeviceLeAudio; 771 default: 772 Log.w(TAG, "getActiveDevice: unknown profile " + bluetoothProfile); 773 break; 774 } 775 return false; 776 } 777 setRssi(short rssi)778 void setRssi(short rssi) { 779 if (mRssi != rssi) { 780 mRssi = rssi; 781 dispatchAttributesChanged(); 782 } 783 } 784 785 /** 786 * Checks whether we are connected to this device (any profile counts). 787 * 788 * @return Whether it is connected. 789 */ isConnected()790 public boolean isConnected() { 791 synchronized (mProfileLock) { 792 for (LocalBluetoothProfile profile : mProfiles) { 793 int status = getProfileConnectionState(profile); 794 if (status == BluetoothProfile.STATE_CONNECTED) { 795 return true; 796 } 797 } 798 799 return false; 800 } 801 } 802 isConnectedProfile(LocalBluetoothProfile profile)803 public boolean isConnectedProfile(LocalBluetoothProfile profile) { 804 int status = getProfileConnectionState(profile); 805 return status == BluetoothProfile.STATE_CONNECTED; 806 807 } 808 isBusy()809 public boolean isBusy() { 810 synchronized (mProfileLock) { 811 for (LocalBluetoothProfile profile : mProfiles) { 812 int status = getProfileConnectionState(profile); 813 if (status == BluetoothProfile.STATE_CONNECTING 814 || status == BluetoothProfile.STATE_DISCONNECTING) { 815 return true; 816 } 817 } 818 return getBondState() == BluetoothDevice.BOND_BONDING; 819 } 820 } 821 updateProfiles()822 private boolean updateProfiles() { 823 ParcelUuid[] uuids = mDevice.getUuids(); 824 if (uuids == null) return false; 825 826 List<ParcelUuid> uuidsList = mLocalAdapter.getUuidsList(); 827 ParcelUuid[] localUuids = new ParcelUuid[uuidsList.size()]; 828 uuidsList.toArray(localUuids); 829 830 /* 831 * Now we know if the device supports PBAP, update permissions... 832 */ 833 processPhonebookAccess(); 834 835 synchronized (mProfileLock) { 836 mProfileManager.updateProfiles(uuids, localUuids, mProfiles, mRemovedProfiles, 837 mLocalNapRoleConnected, mDevice); 838 } 839 840 if (BluetoothUtils.D) { 841 Log.d(TAG, "updating profiles for " + mDevice.getAnonymizedAddress()); 842 BluetoothClass bluetoothClass = mDevice.getBluetoothClass(); 843 844 if (bluetoothClass != null) Log.v(TAG, "Class: " + bluetoothClass.toString()); 845 Log.v(TAG, "UUID:"); 846 for (ParcelUuid uuid : uuids) { 847 Log.v(TAG, " " + uuid); 848 } 849 } 850 return true; 851 } 852 fetchActiveDevices()853 private void fetchActiveDevices() { 854 A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile(); 855 if (a2dpProfile != null) { 856 mIsActiveDeviceA2dp = mDevice.equals(a2dpProfile.getActiveDevice()); 857 } 858 HeadsetProfile headsetProfile = mProfileManager.getHeadsetProfile(); 859 if (headsetProfile != null) { 860 mIsActiveDeviceHeadset = mDevice.equals(headsetProfile.getActiveDevice()); 861 } 862 HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile(); 863 if (hearingAidProfile != null) { 864 mIsActiveDeviceHearingAid = hearingAidProfile.getActiveDevices().contains(mDevice); 865 } 866 LeAudioProfile leAudio = mProfileManager.getLeAudioProfile(); 867 if (leAudio != null) { 868 mIsActiveDeviceLeAudio = leAudio.getActiveDevices().contains(mDevice); 869 } 870 } 871 872 /** 873 * Refreshes the UI when framework alerts us of a UUID change. 874 */ onUuidChanged()875 void onUuidChanged() { 876 updateProfiles(); 877 ParcelUuid[] uuids = mDevice.getUuids(); 878 879 long timeout = MAX_UUID_DELAY_FOR_AUTO_CONNECT; 880 if (ArrayUtils.contains(uuids, BluetoothUuid.HOGP)) { 881 timeout = MAX_HOGP_DELAY_FOR_AUTO_CONNECT; 882 } else if (ArrayUtils.contains(uuids, BluetoothUuid.HEARING_AID)) { 883 timeout = MAX_HEARING_AIDS_DELAY_FOR_AUTO_CONNECT; 884 } else if (ArrayUtils.contains(uuids, BluetoothUuid.LE_AUDIO)) { 885 timeout = MAX_LEAUDIO_DELAY_FOR_AUTO_CONNECT; 886 } 887 888 if (BluetoothUtils.D) { 889 Log.d(TAG, "onUuidChanged: Time since last connect=" 890 + (SystemClock.elapsedRealtime() - mConnectAttempted)); 891 } 892 893 /* 894 * If a connect was attempted earlier without any UUID, we will do the connect now. 895 * Otherwise, allow the connect on UUID change. 896 */ 897 if ((mConnectAttempted + timeout) > SystemClock.elapsedRealtime()) { 898 Log.d(TAG, "onUuidChanged: triggering connectDevice"); 899 connectDevice(); 900 } 901 902 dispatchAttributesChanged(); 903 } 904 onBondingStateChanged(int bondState)905 void onBondingStateChanged(int bondState) { 906 if (bondState == BluetoothDevice.BOND_NONE) { 907 synchronized (mProfileLock) { 908 mProfiles.clear(); 909 } 910 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_UNKNOWN); 911 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_UNKNOWN); 912 mDevice.setSimAccessPermission(BluetoothDevice.ACCESS_UNKNOWN); 913 914 mBondTimestamp = null; 915 } 916 917 refresh(); 918 919 if (bondState == BluetoothDevice.BOND_BONDED) { 920 mBondTimestamp = new Timestamp(System.currentTimeMillis()); 921 922 if (mDevice.isBondingInitiatedLocally()) { 923 connect(); 924 } 925 } 926 } 927 getBondTimestamp()928 public Timestamp getBondTimestamp() { 929 return mBondTimestamp; 930 } 931 getBtClass()932 public BluetoothClass getBtClass() { 933 return mDevice.getBluetoothClass(); 934 } 935 getProfiles()936 public List<LocalBluetoothProfile> getProfiles() { 937 return new ArrayList<>(mProfiles); 938 } 939 getConnectableProfiles()940 public List<LocalBluetoothProfile> getConnectableProfiles() { 941 List<LocalBluetoothProfile> connectableProfiles = 942 new ArrayList<LocalBluetoothProfile>(); 943 synchronized (mProfileLock) { 944 for (LocalBluetoothProfile profile : mProfiles) { 945 if (profile.accessProfileEnabled()) { 946 connectableProfiles.add(profile); 947 } 948 } 949 } 950 return connectableProfiles; 951 } 952 getRemovedProfiles()953 public List<LocalBluetoothProfile> getRemovedProfiles() { 954 return new ArrayList<>(mRemovedProfiles); 955 } 956 registerCallback(Callback callback)957 public void registerCallback(Callback callback) { 958 mCallbacks.add(callback); 959 } 960 unregisterCallback(Callback callback)961 public void unregisterCallback(Callback callback) { 962 mCallbacks.remove(callback); 963 } 964 dispatchAttributesChanged()965 void dispatchAttributesChanged() { 966 for (Callback callback : mCallbacks) { 967 callback.onDeviceAttributesChanged(); 968 } 969 } 970 971 @Override toString()972 public String toString() { 973 return "CachedBluetoothDevice{" 974 + "anonymizedAddress=" 975 + mDevice.getAnonymizedAddress() 976 + ", name=" 977 + getName() 978 + ", groupId=" 979 + mGroupId 980 + ", member=" + mMemberDevices 981 + "}"; 982 } 983 984 @Override equals(Object o)985 public boolean equals(Object o) { 986 if ((o == null) || !(o instanceof CachedBluetoothDevice)) { 987 return false; 988 } 989 return mDevice.equals(((CachedBluetoothDevice) o).mDevice); 990 } 991 992 @Override hashCode()993 public int hashCode() { 994 return mDevice.getAddress().hashCode(); 995 } 996 997 // This comparison uses non-final fields so the sort order may change 998 // when device attributes change (such as bonding state). Settings 999 // will completely refresh the device list when this happens. compareTo(CachedBluetoothDevice another)1000 public int compareTo(CachedBluetoothDevice another) { 1001 // Connected above not connected 1002 int comparison = (another.isConnected() ? 1 : 0) - (isConnected() ? 1 : 0); 1003 if (comparison != 0) return comparison; 1004 1005 // Paired above not paired 1006 comparison = (another.getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0) - 1007 (getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0); 1008 if (comparison != 0) return comparison; 1009 1010 // Just discovered above discovered in the past 1011 comparison = (another.mJustDiscovered ? 1 : 0) - (mJustDiscovered ? 1 : 0); 1012 if (comparison != 0) return comparison; 1013 1014 // Stronger signal above weaker signal 1015 comparison = another.mRssi - mRssi; 1016 if (comparison != 0) return comparison; 1017 1018 // Fallback on name 1019 return getName().compareTo(another.getName()); 1020 } 1021 1022 public interface Callback { onDeviceAttributesChanged()1023 void onDeviceAttributesChanged(); 1024 } 1025 1026 // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth 1027 // app's shared preferences). migratePhonebookPermissionChoice()1028 private void migratePhonebookPermissionChoice() { 1029 SharedPreferences preferences = mContext.getSharedPreferences( 1030 "bluetooth_phonebook_permission", Context.MODE_PRIVATE); 1031 if (!preferences.contains(mDevice.getAddress())) { 1032 return; 1033 } 1034 1035 if (mDevice.getPhonebookAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) { 1036 int oldPermission = 1037 preferences.getInt(mDevice.getAddress(), BluetoothDevice.ACCESS_UNKNOWN); 1038 if (oldPermission == BluetoothDevice.ACCESS_ALLOWED) { 1039 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED); 1040 } else if (oldPermission == BluetoothDevice.ACCESS_REJECTED) { 1041 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED); 1042 } 1043 } 1044 1045 SharedPreferences.Editor editor = preferences.edit(); 1046 editor.remove(mDevice.getAddress()); 1047 editor.commit(); 1048 } 1049 1050 // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth 1051 // app's shared preferences). migrateMessagePermissionChoice()1052 private void migrateMessagePermissionChoice() { 1053 SharedPreferences preferences = mContext.getSharedPreferences( 1054 "bluetooth_message_permission", Context.MODE_PRIVATE); 1055 if (!preferences.contains(mDevice.getAddress())) { 1056 return; 1057 } 1058 1059 if (mDevice.getMessageAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) { 1060 int oldPermission = 1061 preferences.getInt(mDevice.getAddress(), BluetoothDevice.ACCESS_UNKNOWN); 1062 if (oldPermission == BluetoothDevice.ACCESS_ALLOWED) { 1063 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_ALLOWED); 1064 } else if (oldPermission == BluetoothDevice.ACCESS_REJECTED) { 1065 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_REJECTED); 1066 } 1067 } 1068 1069 SharedPreferences.Editor editor = preferences.edit(); 1070 editor.remove(mDevice.getAddress()); 1071 editor.commit(); 1072 } 1073 processPhonebookAccess()1074 private void processPhonebookAccess() { 1075 if (mDevice.getBondState() != BluetoothDevice.BOND_BONDED) return; 1076 1077 ParcelUuid[] uuids = mDevice.getUuids(); 1078 if (BluetoothUuid.containsAnyUuid(uuids, PbapServerProfile.PBAB_CLIENT_UUIDS)) { 1079 // The pairing dialog now warns of phone-book access for paired devices. 1080 // No separate prompt is displayed after pairing. 1081 mDevice.getPhonebookAccessPermission(); 1082 } 1083 } 1084 getMaxConnectionState()1085 public int getMaxConnectionState() { 1086 int maxState = BluetoothProfile.STATE_DISCONNECTED; 1087 synchronized (mProfileLock) { 1088 for (LocalBluetoothProfile profile : getProfiles()) { 1089 int connectionStatus = getProfileConnectionState(profile); 1090 if (connectionStatus > maxState) { 1091 maxState = connectionStatus; 1092 } 1093 } 1094 } 1095 return maxState; 1096 } 1097 1098 /** 1099 * Return full summary that describes connection state of this device 1100 * 1101 * @see #getConnectionSummary(boolean shortSummary) 1102 */ getConnectionSummary()1103 public String getConnectionSummary() { 1104 return getConnectionSummary(false /* shortSummary */); 1105 } 1106 1107 /** 1108 * Return summary that describes connection state of this device. Summary depends on: 1109 * 1. Whether device has battery info 1110 * 2. Whether device is in active usage(or in phone call) 1111 * 1112 * @param shortSummary {@code true} if need to return short version summary 1113 */ getConnectionSummary(boolean shortSummary)1114 public String getConnectionSummary(boolean shortSummary) { 1115 boolean profileConnected = false; // Updated as long as BluetoothProfile is connected 1116 boolean a2dpConnected = true; // A2DP is connected 1117 boolean hfpConnected = true; // HFP is connected 1118 boolean hearingAidConnected = true; // Hearing Aid is connected 1119 boolean leAudioConnected = true; // LeAudio is connected 1120 int leftBattery = -1; 1121 int rightBattery = -1; 1122 1123 if (isProfileConnectedFail() && isConnected()) { 1124 return mContext.getString(R.string.profile_connect_timeout_subtext); 1125 } 1126 1127 synchronized (mProfileLock) { 1128 for (LocalBluetoothProfile profile : getProfiles()) { 1129 int connectionStatus = getProfileConnectionState(profile); 1130 1131 switch (connectionStatus) { 1132 case BluetoothProfile.STATE_CONNECTING: 1133 case BluetoothProfile.STATE_DISCONNECTING: 1134 return mContext.getString( 1135 BluetoothUtils.getConnectionStateSummary(connectionStatus)); 1136 1137 case BluetoothProfile.STATE_CONNECTED: 1138 profileConnected = true; 1139 break; 1140 1141 case BluetoothProfile.STATE_DISCONNECTED: 1142 if (profile.isProfileReady()) { 1143 if (profile instanceof A2dpProfile 1144 || profile instanceof A2dpSinkProfile) { 1145 a2dpConnected = false; 1146 } else if (profile instanceof HeadsetProfile 1147 || profile instanceof HfpClientProfile) { 1148 hfpConnected = false; 1149 } else if (profile instanceof HearingAidProfile) { 1150 hearingAidConnected = false; 1151 } else if (profile instanceof LeAudioProfile) { 1152 leAudioConnected = false; 1153 } 1154 } 1155 break; 1156 } 1157 } 1158 } 1159 1160 String batteryLevelPercentageString = null; 1161 // Android framework should only set mBatteryLevel to valid range [0-100], 1162 // BluetoothDevice.BATTERY_LEVEL_BLUETOOTH_OFF, or BluetoothDevice.BATTERY_LEVEL_UNKNOWN, 1163 // any other value should be a framework bug. Thus assume here that if value is greater 1164 // than BluetoothDevice.BATTERY_LEVEL_UNKNOWN, it must be valid 1165 final int batteryLevel = getMinBatteryLevelWithMemberDevices(); 1166 if (batteryLevel > BluetoothDevice.BATTERY_LEVEL_UNKNOWN) { 1167 // TODO: name com.android.settingslib.bluetooth.Utils something different 1168 batteryLevelPercentageString = 1169 com.android.settingslib.Utils.formatPercentage(batteryLevel); 1170 } 1171 1172 int stringRes = R.string.bluetooth_pairing; 1173 //when profile is connected, information would be available 1174 if (profileConnected) { 1175 // Update Meta data for connected device 1176 if (BluetoothUtils.getBooleanMetaData( 1177 mDevice, BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET)) { 1178 leftBattery = BluetoothUtils.getIntMetaData(mDevice, 1179 BluetoothDevice.METADATA_UNTETHERED_LEFT_BATTERY); 1180 rightBattery = BluetoothUtils.getIntMetaData(mDevice, 1181 BluetoothDevice.METADATA_UNTETHERED_RIGHT_BATTERY); 1182 } 1183 1184 // Set default string with battery level in device connected situation. 1185 if (isTwsBatteryAvailable(leftBattery, rightBattery)) { 1186 stringRes = R.string.bluetooth_battery_level_untethered; 1187 } else if (batteryLevelPercentageString != null) { 1188 stringRes = R.string.bluetooth_battery_level; 1189 } 1190 1191 // Set active string in following device connected situation, also show battery 1192 // information if they have. 1193 // 1. Hearing Aid device active. 1194 // 2. Headset device active with in-calling state. 1195 // 3. A2DP device active without in-calling state. 1196 // 4. Le Audio device active 1197 if (a2dpConnected || hfpConnected || hearingAidConnected || leAudioConnected) { 1198 final boolean isOnCall = Utils.isAudioModeOngoingCall(mContext); 1199 if ((mIsActiveDeviceHearingAid) 1200 || (mIsActiveDeviceHeadset && isOnCall) 1201 || (mIsActiveDeviceA2dp && !isOnCall) 1202 || mIsActiveDeviceLeAudio) { 1203 if (isTwsBatteryAvailable(leftBattery, rightBattery) && !shortSummary) { 1204 stringRes = R.string.bluetooth_active_battery_level_untethered; 1205 } else if (batteryLevelPercentageString != null && !shortSummary) { 1206 stringRes = R.string.bluetooth_active_battery_level; 1207 } else { 1208 stringRes = R.string.bluetooth_active_no_battery_level; 1209 } 1210 } 1211 1212 // Try to show left/right information if can not get it from battery for hearing 1213 // aids specifically. 1214 boolean isActiveAshaHearingAid = mIsActiveDeviceHearingAid; 1215 boolean isActiveLeAudioHearingAid = mIsActiveDeviceLeAudio 1216 && isConnectedHapClientDevice(); 1217 if ((isActiveAshaHearingAid || isActiveLeAudioHearingAid) 1218 && stringRes == R.string.bluetooth_active_no_battery_level) { 1219 final Set<CachedBluetoothDevice> memberDevices = getMemberDevice(); 1220 final CachedBluetoothDevice subDevice = getSubDevice(); 1221 if (memberDevices.stream().anyMatch(m -> m.isConnected())) { 1222 stringRes = R.string.bluetooth_hearing_aid_left_and_right_active; 1223 } else if (subDevice != null && subDevice.isConnected()) { 1224 stringRes = R.string.bluetooth_hearing_aid_left_and_right_active; 1225 } else { 1226 int deviceSide = getDeviceSide(); 1227 if (deviceSide == HearingAidInfo.DeviceSide.SIDE_LEFT_AND_RIGHT) { 1228 stringRes = R.string.bluetooth_hearing_aid_left_and_right_active; 1229 } else if (deviceSide == HearingAidInfo.DeviceSide.SIDE_LEFT) { 1230 stringRes = R.string.bluetooth_hearing_aid_left_active; 1231 } else if (deviceSide == HearingAidInfo.DeviceSide.SIDE_RIGHT) { 1232 stringRes = R.string.bluetooth_hearing_aid_right_active; 1233 } else { 1234 stringRes = R.string.bluetooth_active_no_battery_level; 1235 } 1236 } 1237 } 1238 } 1239 } 1240 1241 if (stringRes != R.string.bluetooth_pairing 1242 || getBondState() == BluetoothDevice.BOND_BONDING) { 1243 if (isTwsBatteryAvailable(leftBattery, rightBattery)) { 1244 return mContext.getString(stringRes, Utils.formatPercentage(leftBattery), 1245 Utils.formatPercentage(rightBattery)); 1246 } else { 1247 return mContext.getString(stringRes, batteryLevelPercentageString); 1248 } 1249 } else { 1250 return null; 1251 } 1252 } 1253 isTwsBatteryAvailable(int leftBattery, int rightBattery)1254 private boolean isTwsBatteryAvailable(int leftBattery, int rightBattery) { 1255 return leftBattery >= 0 && rightBattery >= 0; 1256 } 1257 isProfileConnectedFail()1258 private boolean isProfileConnectedFail() { 1259 Log.d(TAG, "anonymizedAddress=" + mDevice.getAnonymizedAddress() 1260 + " mIsA2dpProfileConnectedFail=" + mIsA2dpProfileConnectedFail 1261 + " mIsHearingAidProfileConnectedFail=" + mIsHearingAidProfileConnectedFail 1262 + " mIsLeAudioProfileConnectedFail=" + mIsLeAudioProfileConnectedFail 1263 + " mIsHeadsetProfileConnectedFail=" + mIsHeadsetProfileConnectedFail 1264 + " isConnectedSapDevice()=" + isConnectedSapDevice()); 1265 1266 return mIsA2dpProfileConnectedFail || mIsHearingAidProfileConnectedFail 1267 || (!isConnectedSapDevice() && mIsHeadsetProfileConnectedFail) 1268 || mIsLeAudioProfileConnectedFail; 1269 } 1270 1271 /** 1272 * See {@link #getCarConnectionSummary(boolean, boolean)} 1273 */ getCarConnectionSummary()1274 public String getCarConnectionSummary() { 1275 return getCarConnectionSummary(false /* shortSummary */); 1276 } 1277 1278 /** 1279 * See {@link #getCarConnectionSummary(boolean, boolean)} 1280 */ getCarConnectionSummary(boolean shortSummary)1281 public String getCarConnectionSummary(boolean shortSummary) { 1282 return getCarConnectionSummary(shortSummary, true /* useDisconnectedString */); 1283 } 1284 1285 /** 1286 * Returns android auto string that describes the connection state of this device. 1287 * 1288 * @param shortSummary {@code true} if need to return short version summary 1289 * @param useDisconnectedString {@code true} if need to return disconnected summary string 1290 */ getCarConnectionSummary(boolean shortSummary, boolean useDisconnectedString)1291 public String getCarConnectionSummary(boolean shortSummary, boolean useDisconnectedString) { 1292 boolean profileConnected = false; // at least one profile is connected 1293 boolean a2dpNotConnected = false; // A2DP is preferred but not connected 1294 boolean hfpNotConnected = false; // HFP is preferred but not connected 1295 boolean hearingAidNotConnected = false; // Hearing Aid is preferred but not connected 1296 boolean leAudioNotConnected = false; // LeAudio is preferred but not connected 1297 1298 synchronized (mProfileLock) { 1299 for (LocalBluetoothProfile profile : getProfiles()) { 1300 int connectionStatus = getProfileConnectionState(profile); 1301 1302 switch (connectionStatus) { 1303 case BluetoothProfile.STATE_CONNECTING: 1304 case BluetoothProfile.STATE_DISCONNECTING: 1305 return mContext.getString( 1306 BluetoothUtils.getConnectionStateSummary(connectionStatus)); 1307 1308 case BluetoothProfile.STATE_CONNECTED: 1309 if (shortSummary) { 1310 return mContext.getString(BluetoothUtils.getConnectionStateSummary( 1311 connectionStatus), /* formatArgs= */ ""); 1312 } 1313 profileConnected = true; 1314 break; 1315 1316 case BluetoothProfile.STATE_DISCONNECTED: 1317 if (profile.isProfileReady()) { 1318 if (profile instanceof A2dpProfile 1319 || profile instanceof A2dpSinkProfile) { 1320 a2dpNotConnected = true; 1321 } else if (profile instanceof HeadsetProfile 1322 || profile instanceof HfpClientProfile) { 1323 hfpNotConnected = true; 1324 } else if (profile instanceof HearingAidProfile) { 1325 hearingAidNotConnected = true; 1326 } else if (profile instanceof LeAudioProfile) { 1327 leAudioNotConnected = true; 1328 } 1329 } 1330 break; 1331 } 1332 } 1333 } 1334 1335 String batteryLevelPercentageString = null; 1336 // Android framework should only set mBatteryLevel to valid range [0-100], 1337 // BluetoothDevice.BATTERY_LEVEL_BLUETOOTH_OFF, or BluetoothDevice.BATTERY_LEVEL_UNKNOWN, 1338 // any other value should be a framework bug. Thus assume here that if value is greater 1339 // than BluetoothDevice.BATTERY_LEVEL_UNKNOWN, it must be valid 1340 final int batteryLevel = getMinBatteryLevelWithMemberDevices(); 1341 if (batteryLevel > BluetoothDevice.BATTERY_LEVEL_UNKNOWN) { 1342 // TODO: name com.android.settingslib.bluetooth.Utils something different 1343 batteryLevelPercentageString = 1344 com.android.settingslib.Utils.formatPercentage(batteryLevel); 1345 } 1346 1347 // Prepare the string for the Active Device summary 1348 String[] activeDeviceStringsArray = mContext.getResources().getStringArray( 1349 R.array.bluetooth_audio_active_device_summaries); 1350 String activeDeviceString = activeDeviceStringsArray[0]; // Default value: not active 1351 if (mIsActiveDeviceA2dp && mIsActiveDeviceHeadset) { 1352 activeDeviceString = activeDeviceStringsArray[1]; // Active for Media and Phone 1353 } else { 1354 if (mIsActiveDeviceA2dp) { 1355 activeDeviceString = activeDeviceStringsArray[2]; // Active for Media only 1356 } 1357 if (mIsActiveDeviceHeadset) { 1358 activeDeviceString = activeDeviceStringsArray[3]; // Active for Phone only 1359 } 1360 } 1361 if (!hearingAidNotConnected && mIsActiveDeviceHearingAid) { 1362 activeDeviceString = activeDeviceStringsArray[1]; 1363 return mContext.getString(R.string.bluetooth_connected, activeDeviceString); 1364 } 1365 1366 if (!leAudioNotConnected && mIsActiveDeviceLeAudio) { 1367 activeDeviceString = activeDeviceStringsArray[1]; 1368 return mContext.getString(R.string.bluetooth_connected, activeDeviceString); 1369 } 1370 1371 if (profileConnected) { 1372 if (a2dpNotConnected && hfpNotConnected) { 1373 if (batteryLevelPercentageString != null) { 1374 return mContext.getString( 1375 R.string.bluetooth_connected_no_headset_no_a2dp_battery_level, 1376 batteryLevelPercentageString, activeDeviceString); 1377 } else { 1378 return mContext.getString(R.string.bluetooth_connected_no_headset_no_a2dp, 1379 activeDeviceString); 1380 } 1381 1382 } else if (a2dpNotConnected) { 1383 if (batteryLevelPercentageString != null) { 1384 return mContext.getString(R.string.bluetooth_connected_no_a2dp_battery_level, 1385 batteryLevelPercentageString, activeDeviceString); 1386 } else { 1387 return mContext.getString(R.string.bluetooth_connected_no_a2dp, 1388 activeDeviceString); 1389 } 1390 1391 } else if (hfpNotConnected) { 1392 if (batteryLevelPercentageString != null) { 1393 return mContext.getString(R.string.bluetooth_connected_no_headset_battery_level, 1394 batteryLevelPercentageString, activeDeviceString); 1395 } else { 1396 return mContext.getString(R.string.bluetooth_connected_no_headset, 1397 activeDeviceString); 1398 } 1399 } else { 1400 if (batteryLevelPercentageString != null) { 1401 return mContext.getString(R.string.bluetooth_connected_battery_level, 1402 batteryLevelPercentageString, activeDeviceString); 1403 } else { 1404 return mContext.getString(R.string.bluetooth_connected, activeDeviceString); 1405 } 1406 } 1407 } 1408 1409 if (getBondState() == BluetoothDevice.BOND_BONDING) { 1410 return mContext.getString(R.string.bluetooth_pairing); 1411 } 1412 return useDisconnectedString ? mContext.getString(R.string.bluetooth_disconnected) : null; 1413 } 1414 1415 /** 1416 * @return {@code true} if {@code cachedBluetoothDevice} is a2dp device 1417 */ isConnectedA2dpDevice()1418 public boolean isConnectedA2dpDevice() { 1419 A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile(); 1420 return a2dpProfile != null && a2dpProfile.getConnectionStatus(mDevice) == 1421 BluetoothProfile.STATE_CONNECTED; 1422 } 1423 1424 /** 1425 * @return {@code true} if {@code cachedBluetoothDevice} is HFP device 1426 */ isConnectedHfpDevice()1427 public boolean isConnectedHfpDevice() { 1428 HeadsetProfile headsetProfile = mProfileManager.getHeadsetProfile(); 1429 return headsetProfile != null && headsetProfile.getConnectionStatus(mDevice) == 1430 BluetoothProfile.STATE_CONNECTED; 1431 } 1432 1433 /** 1434 * @return {@code true} if {@code cachedBluetoothDevice} is ASHA hearing aid device 1435 */ isConnectedAshaHearingAidDevice()1436 public boolean isConnectedAshaHearingAidDevice() { 1437 HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile(); 1438 return hearingAidProfile != null && hearingAidProfile.getConnectionStatus(mDevice) == 1439 BluetoothProfile.STATE_CONNECTED; 1440 } 1441 1442 /** 1443 * @return {@code true} if {@code cachedBluetoothDevice} is HAP device 1444 */ isConnectedHapClientDevice()1445 public boolean isConnectedHapClientDevice() { 1446 HapClientProfile hapClientProfile = mProfileManager.getHapClientProfile(); 1447 return hapClientProfile != null && hapClientProfile.getConnectionStatus(mDevice) 1448 == BluetoothProfile.STATE_CONNECTED; 1449 } 1450 1451 /** 1452 * @return {@code true} if {@code cachedBluetoothDevice} is LeAudio hearing aid device 1453 */ isConnectedLeAudioHearingAidDevice()1454 public boolean isConnectedLeAudioHearingAidDevice() { 1455 return isConnectedHapClientDevice() && isConnectedLeAudioDevice(); 1456 } 1457 1458 /** 1459 * @return {@code true} if {@code cachedBluetoothDevice} is hearing aid device 1460 * 1461 * The device may be an ASHA hearing aid that supports {@link HearingAidProfile} or a LeAudio 1462 * hearing aid that supports {@link HapClientProfile} and {@link LeAudioProfile}. 1463 */ isConnectedHearingAidDevice()1464 public boolean isConnectedHearingAidDevice() { 1465 return isConnectedAshaHearingAidDevice() || isConnectedLeAudioHearingAidDevice(); 1466 } 1467 1468 /** 1469 * @return {@code true} if {@code cachedBluetoothDevice} is LeAudio device 1470 */ isConnectedLeAudioDevice()1471 public boolean isConnectedLeAudioDevice() { 1472 LeAudioProfile leAudio = mProfileManager.getLeAudioProfile(); 1473 return leAudio != null && leAudio.getConnectionStatus(mDevice) == 1474 BluetoothProfile.STATE_CONNECTED; 1475 } 1476 isConnectedSapDevice()1477 private boolean isConnectedSapDevice() { 1478 SapProfile sapProfile = mProfileManager.getSapProfile(); 1479 return sapProfile != null && sapProfile.getConnectionStatus(mDevice) 1480 == BluetoothProfile.STATE_CONNECTED; 1481 } 1482 getSubDevice()1483 public CachedBluetoothDevice getSubDevice() { 1484 return mSubDevice; 1485 } 1486 setSubDevice(CachedBluetoothDevice subDevice)1487 public void setSubDevice(CachedBluetoothDevice subDevice) { 1488 mSubDevice = subDevice; 1489 } 1490 switchSubDeviceContent()1491 public void switchSubDeviceContent() { 1492 // Backup from main device 1493 BluetoothDevice tmpDevice = mDevice; 1494 final short tmpRssi = mRssi; 1495 final boolean tmpJustDiscovered = mJustDiscovered; 1496 final HearingAidInfo tmpHearingAidInfo = mHearingAidInfo; 1497 // Set main device from sub device 1498 release(); 1499 mDevice = mSubDevice.mDevice; 1500 mRssi = mSubDevice.mRssi; 1501 mJustDiscovered = mSubDevice.mJustDiscovered; 1502 mHearingAidInfo = mSubDevice.mHearingAidInfo; 1503 // Set sub device from backup 1504 mSubDevice.release(); 1505 mSubDevice.mDevice = tmpDevice; 1506 mSubDevice.mRssi = tmpRssi; 1507 mSubDevice.mJustDiscovered = tmpJustDiscovered; 1508 mSubDevice.mHearingAidInfo = tmpHearingAidInfo; 1509 fetchActiveDevices(); 1510 } 1511 1512 /** 1513 * @return a set of member devices that are in the same coordinated set with this device. 1514 */ getMemberDevice()1515 public Set<CachedBluetoothDevice> getMemberDevice() { 1516 return mMemberDevices; 1517 } 1518 1519 /** 1520 * Store the member devices that are in the same coordinated set. 1521 */ addMemberDevice(CachedBluetoothDevice memberDevice)1522 public void addMemberDevice(CachedBluetoothDevice memberDevice) { 1523 Log.d(TAG, this + " addMemberDevice = " + memberDevice); 1524 mMemberDevices.add(memberDevice); 1525 } 1526 1527 /** 1528 * Remove a device from the member device sets. 1529 */ removeMemberDevice(CachedBluetoothDevice memberDevice)1530 public void removeMemberDevice(CachedBluetoothDevice memberDevice) { 1531 memberDevice.release(); 1532 mMemberDevices.remove(memberDevice); 1533 } 1534 1535 /** 1536 * In order to show the preference for the whole group, we always set the main device as the 1537 * first connected device in the coordinated set, and then switch the content of the main 1538 * device and member devices. 1539 * 1540 * @param newMainDevice the new Main device which is from the previous main device's member 1541 * list. 1542 */ switchMemberDeviceContent(CachedBluetoothDevice newMainDevice)1543 public void switchMemberDeviceContent(CachedBluetoothDevice newMainDevice) { 1544 // Remove the sub device from mMemberDevices first to prevent hash mismatch problem due 1545 // to mDevice switch 1546 removeMemberDevice(newMainDevice); 1547 1548 // Backup from current main device 1549 final BluetoothDevice tmpDevice = mDevice; 1550 final short tmpRssi = mRssi; 1551 final boolean tmpJustDiscovered = mJustDiscovered; 1552 1553 // Set main device from sub device 1554 release(); 1555 mDevice = newMainDevice.mDevice; 1556 mRssi = newMainDevice.mRssi; 1557 mJustDiscovered = newMainDevice.mJustDiscovered; 1558 fillData(); 1559 1560 // Set sub device from backup 1561 newMainDevice.release(); 1562 newMainDevice.mDevice = tmpDevice; 1563 newMainDevice.mRssi = tmpRssi; 1564 newMainDevice.mJustDiscovered = tmpJustDiscovered; 1565 newMainDevice.fillData(); 1566 1567 // Add the sub device back into mMemberDevices with correct hash 1568 addMemberDevice(newMainDevice); 1569 } 1570 1571 /** 1572 * Get cached bluetooth icon with description 1573 */ getDrawableWithDescription()1574 public Pair<Drawable, String> getDrawableWithDescription() { 1575 Uri uri = BluetoothUtils.getUriMetaData(mDevice, BluetoothDevice.METADATA_MAIN_ICON); 1576 Pair<Drawable, String> pair = BluetoothUtils.getBtClassDrawableWithDescription( 1577 mContext, this); 1578 1579 if (BluetoothUtils.isAdvancedDetailsHeader(mDevice) && uri != null) { 1580 BitmapDrawable drawable = mDrawableCache.get(uri.toString()); 1581 if (drawable != null) { 1582 Resources resources = mContext.getResources(); 1583 return new Pair<>(new AdaptiveOutlineDrawable( 1584 resources, drawable.getBitmap()), pair.second); 1585 } 1586 1587 refresh(); 1588 } 1589 1590 return BluetoothUtils.getBtRainbowDrawableWithDescription(mContext, this); 1591 } 1592 releaseLruCache()1593 void releaseLruCache() { 1594 mDrawableCache.evictAll(); 1595 } 1596 getUnpairing()1597 boolean getUnpairing() { 1598 return mUnpairing; 1599 } 1600 } 1601