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