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