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.BluetoothDevice; 22 import android.bluetooth.BluetoothHearingAid; 23 import android.bluetooth.BluetoothProfile; 24 import android.bluetooth.BluetoothUuid; 25 import android.content.Context; 26 import android.content.SharedPreferences; 27 import android.os.ParcelUuid; 28 import android.os.SystemClock; 29 import android.text.TextUtils; 30 import android.util.Log; 31 32 import androidx.annotation.VisibleForTesting; 33 34 import com.android.settingslib.R; 35 import com.android.settingslib.Utils; 36 37 import java.util.ArrayList; 38 import java.util.Collection; 39 import java.util.Collections; 40 import java.util.List; 41 42 /** 43 * CachedBluetoothDevice represents a remote Bluetooth device. It contains 44 * attributes of the device (such as the address, name, RSSI, etc.) and 45 * functionality that can be performed on the device (connect, pair, disconnect, 46 * etc.). 47 */ 48 public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> { 49 private static final String TAG = "CachedBluetoothDevice"; 50 51 // See mConnectAttempted 52 private static final long MAX_UUID_DELAY_FOR_AUTO_CONNECT = 5000; 53 // Some Hearing Aids (especially the 2nd device) needs more time to do service discovery 54 private static final long MAX_HEARING_AIDS_DELAY_FOR_AUTO_CONNECT = 15000; 55 private static final long MAX_HOGP_DELAY_FOR_AUTO_CONNECT = 30000; 56 57 private final Context mContext; 58 private final BluetoothAdapter mLocalAdapter; 59 private final LocalBluetoothProfileManager mProfileManager; 60 private final Object mProfileLock = new Object(); 61 BluetoothDevice mDevice; 62 private long mHiSyncId; 63 // Need this since there is no method for getting RSSI 64 short mRssi; 65 // mProfiles and mRemovedProfiles does not do swap() between main and sub device. It is 66 // because current sub device is only for HearingAid and its profile is the same. 67 private final List<LocalBluetoothProfile> mProfiles = new ArrayList<>(); 68 69 // List of profiles that were previously in mProfiles, but have been removed 70 private final List<LocalBluetoothProfile> mRemovedProfiles = new ArrayList<>(); 71 72 // Device supports PANU but not NAP: remove PanProfile after device disconnects from NAP 73 private boolean mLocalNapRoleConnected; 74 75 boolean mJustDiscovered; 76 77 private final Collection<Callback> mCallbacks = new ArrayList<>(); 78 79 /** 80 * Last time a bt profile auto-connect was attempted. 81 * If an ACTION_UUID intent comes in within 82 * MAX_UUID_DELAY_FOR_AUTO_CONNECT milliseconds, we will try auto-connect 83 * again with the new UUIDs 84 */ 85 private long mConnectAttempted; 86 87 // Active device state 88 private boolean mIsActiveDeviceA2dp = false; 89 private boolean mIsActiveDeviceHeadset = false; 90 private boolean mIsActiveDeviceHearingAid = false; 91 // Group second device for Hearing Aid 92 private CachedBluetoothDevice mSubDevice; 93 CachedBluetoothDevice(Context context, LocalBluetoothProfileManager profileManager, BluetoothDevice device)94 CachedBluetoothDevice(Context context, LocalBluetoothProfileManager profileManager, 95 BluetoothDevice device) { 96 mContext = context; 97 mLocalAdapter = BluetoothAdapter.getDefaultAdapter(); 98 mProfileManager = profileManager; 99 mDevice = device; 100 fillData(); 101 mHiSyncId = BluetoothHearingAid.HI_SYNC_ID_INVALID; 102 } 103 104 /** 105 * Describes the current device and profile for logging. 106 * 107 * @param profile Profile to describe 108 * @return Description of the device and profile 109 */ describe(LocalBluetoothProfile profile)110 private String describe(LocalBluetoothProfile profile) { 111 StringBuilder sb = new StringBuilder(); 112 sb.append("Address:").append(mDevice); 113 if (profile != null) { 114 sb.append(" Profile:").append(profile); 115 } 116 117 return sb.toString(); 118 } 119 onProfileStateChanged(LocalBluetoothProfile profile, int newProfileState)120 void onProfileStateChanged(LocalBluetoothProfile profile, int newProfileState) { 121 if (BluetoothUtils.D) { 122 Log.d(TAG, "onProfileStateChanged: profile " + profile + ", device=" + mDevice 123 + ", newProfileState " + newProfileState); 124 } 125 if (mLocalAdapter.getState() == BluetoothAdapter.STATE_TURNING_OFF) 126 { 127 if (BluetoothUtils.D) { 128 Log.d(TAG, " BT Turninig Off...Profile conn state change ignored..."); 129 } 130 return; 131 } 132 133 synchronized (mProfileLock) { 134 if (newProfileState == BluetoothProfile.STATE_CONNECTED) { 135 if (profile instanceof MapProfile) { 136 profile.setPreferred(mDevice, true); 137 } 138 if (!mProfiles.contains(profile)) { 139 mRemovedProfiles.remove(profile); 140 mProfiles.add(profile); 141 if (profile instanceof PanProfile 142 && ((PanProfile) profile).isLocalRoleNap(mDevice)) { 143 // Device doesn't support NAP, so remove PanProfile on disconnect 144 mLocalNapRoleConnected = true; 145 } 146 } 147 } else if (profile instanceof MapProfile 148 && newProfileState == BluetoothProfile.STATE_DISCONNECTED) { 149 profile.setPreferred(mDevice, false); 150 } else if (mLocalNapRoleConnected && profile instanceof PanProfile 151 && ((PanProfile) profile).isLocalRoleNap(mDevice) 152 && newProfileState == BluetoothProfile.STATE_DISCONNECTED) { 153 Log.d(TAG, "Removing PanProfile from device after NAP disconnect"); 154 mProfiles.remove(profile); 155 mRemovedProfiles.add(profile); 156 mLocalNapRoleConnected = false; 157 } 158 } 159 160 fetchActiveDevices(); 161 } 162 disconnect()163 public void disconnect() { 164 synchronized (mProfileLock) { 165 for (LocalBluetoothProfile profile : mProfiles) { 166 disconnect(profile); 167 } 168 } 169 // Disconnect PBAP server in case its connected 170 // This is to ensure all the profiles are disconnected as some CK/Hs do not 171 // disconnect PBAP connection when HF connection is brought down 172 PbapServerProfile PbapProfile = mProfileManager.getPbapProfile(); 173 if (PbapProfile != null && isConnectedProfile(PbapProfile)) 174 { 175 PbapProfile.disconnect(mDevice); 176 } 177 } 178 disconnect(LocalBluetoothProfile profile)179 public void disconnect(LocalBluetoothProfile profile) { 180 if (profile.disconnect(mDevice)) { 181 if (BluetoothUtils.D) { 182 Log.d(TAG, "Command sent successfully:DISCONNECT " + describe(profile)); 183 } 184 } 185 } 186 connect(boolean connectAllProfiles)187 public void connect(boolean connectAllProfiles) { 188 if (!ensurePaired()) { 189 return; 190 } 191 192 mConnectAttempted = SystemClock.elapsedRealtime(); 193 connectWithoutResettingTimer(connectAllProfiles); 194 } 195 getHiSyncId()196 public long getHiSyncId() { 197 return mHiSyncId; 198 } 199 setHiSyncId(long id)200 public void setHiSyncId(long id) { 201 if (BluetoothUtils.D) { 202 Log.d(TAG, "setHiSyncId: mDevice " + mDevice + ", id " + id); 203 } 204 mHiSyncId = id; 205 } 206 isHearingAidDevice()207 public boolean isHearingAidDevice() { 208 return mHiSyncId != BluetoothHearingAid.HI_SYNC_ID_INVALID; 209 } 210 onBondingDockConnect()211 void onBondingDockConnect() { 212 // Attempt to connect if UUIDs are available. Otherwise, 213 // we will connect when the ACTION_UUID intent arrives. 214 connect(false); 215 } 216 connectWithoutResettingTimer(boolean connectAllProfiles)217 private void connectWithoutResettingTimer(boolean connectAllProfiles) { 218 synchronized (mProfileLock) { 219 // Try to initialize the profiles if they were not. 220 if (mProfiles.isEmpty()) { 221 // if mProfiles is empty, then do not invoke updateProfiles. This causes a race 222 // condition with carkits during pairing, wherein RemoteDevice.UUIDs have been 223 // updated from bluetooth stack but ACTION.uuid is not sent yet. 224 // Eventually ACTION.uuid will be received which shall trigger the connection of the 225 // various profiles 226 // If UUIDs are not available yet, connect will be happen 227 // upon arrival of the ACTION_UUID intent. 228 Log.d(TAG, "No profiles. Maybe we will connect later for device " + mDevice); 229 return; 230 } 231 232 int preferredProfiles = 0; 233 for (LocalBluetoothProfile profile : mProfiles) { 234 if (connectAllProfiles ? profile.accessProfileEnabled() 235 : profile.isAutoConnectable()) { 236 if (profile.isPreferred(mDevice)) { 237 ++preferredProfiles; 238 connectInt(profile); 239 } 240 } 241 } 242 if (BluetoothUtils.D) Log.d(TAG, "Preferred profiles = " + preferredProfiles); 243 244 if (preferredProfiles == 0) { 245 connectAutoConnectableProfiles(); 246 } 247 } 248 } 249 connectAutoConnectableProfiles()250 private void connectAutoConnectableProfiles() { 251 if (!ensurePaired()) { 252 return; 253 } 254 255 synchronized (mProfileLock) { 256 for (LocalBluetoothProfile profile : mProfiles) { 257 if (profile.isAutoConnectable()) { 258 profile.setPreferred(mDevice, true); 259 connectInt(profile); 260 } 261 } 262 } 263 } 264 265 /** 266 * Connect this device to the specified profile. 267 * 268 * @param profile the profile to use with the remote device 269 */ connectProfile(LocalBluetoothProfile profile)270 public void connectProfile(LocalBluetoothProfile profile) { 271 mConnectAttempted = SystemClock.elapsedRealtime(); 272 connectInt(profile); 273 // Refresh the UI based on profile.connect() call 274 refresh(); 275 } 276 connectInt(LocalBluetoothProfile profile)277 synchronized void connectInt(LocalBluetoothProfile profile) { 278 if (!ensurePaired()) { 279 return; 280 } 281 if (profile.connect(mDevice)) { 282 if (BluetoothUtils.D) { 283 Log.d(TAG, "Command sent successfully:CONNECT " + describe(profile)); 284 } 285 return; 286 } 287 Log.i(TAG, "Failed to connect " + profile.toString() + " to " + getName()); 288 } 289 ensurePaired()290 private boolean ensurePaired() { 291 if (getBondState() == BluetoothDevice.BOND_NONE) { 292 startPairing(); 293 return false; 294 } else { 295 return true; 296 } 297 } 298 startPairing()299 public boolean startPairing() { 300 // Pairing is unreliable while scanning, so cancel discovery 301 if (mLocalAdapter.isDiscovering()) { 302 mLocalAdapter.cancelDiscovery(); 303 } 304 305 if (!mDevice.createBond()) { 306 return false; 307 } 308 309 return true; 310 } 311 unpair()312 public void unpair() { 313 int state = getBondState(); 314 315 if (state == BluetoothDevice.BOND_BONDING) { 316 mDevice.cancelBondProcess(); 317 } 318 319 if (state != BluetoothDevice.BOND_NONE) { 320 final BluetoothDevice dev = mDevice; 321 if (dev != null) { 322 final boolean successful = dev.removeBond(); 323 if (successful) { 324 if (BluetoothUtils.D) { 325 Log.d(TAG, "Command sent successfully:REMOVE_BOND " + describe(null)); 326 } 327 } else if (BluetoothUtils.V) { 328 Log.v(TAG, "Framework rejected command immediately:REMOVE_BOND " + 329 describe(null)); 330 } 331 } 332 } 333 } 334 getProfileConnectionState(LocalBluetoothProfile profile)335 public int getProfileConnectionState(LocalBluetoothProfile profile) { 336 return profile != null 337 ? profile.getConnectionStatus(mDevice) 338 : BluetoothProfile.STATE_DISCONNECTED; 339 } 340 341 // TODO: do any of these need to run async on a background thread? fillData()342 private void fillData() { 343 updateProfiles(); 344 fetchActiveDevices(); 345 migratePhonebookPermissionChoice(); 346 migrateMessagePermissionChoice(); 347 348 dispatchAttributesChanged(); 349 } 350 getDevice()351 public BluetoothDevice getDevice() { 352 return mDevice; 353 } 354 355 /** 356 * Convenience method that can be mocked - it lets tests avoid having to call getDevice() which 357 * causes problems in tests since BluetoothDevice is final and cannot be mocked. 358 * @return the address of this device 359 */ getAddress()360 public String getAddress() { 361 return mDevice.getAddress(); 362 } 363 364 /** 365 * Get name from remote device 366 * @return {@link BluetoothDevice#getAliasName()} if 367 * {@link BluetoothDevice#getAliasName()} is not null otherwise return 368 * {@link BluetoothDevice#getAddress()} 369 */ getName()370 public String getName() { 371 final String aliasName = mDevice.getAliasName(); 372 return TextUtils.isEmpty(aliasName) ? getAddress() : aliasName; 373 } 374 375 /** 376 * User changes the device name 377 * @param name new alias name to be set, should never be null 378 */ setName(String name)379 public void setName(String name) { 380 // Prevent getName() to be set to null if setName(null) is called 381 if (name != null && !TextUtils.equals(name, getName())) { 382 mDevice.setAlias(name); 383 dispatchAttributesChanged(); 384 } 385 } 386 387 /** 388 * Set this device as active device 389 * @return true if at least one profile on this device is set to active, false otherwise 390 */ setActive()391 public boolean setActive() { 392 boolean result = false; 393 A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile(); 394 if (a2dpProfile != null && isConnectedProfile(a2dpProfile)) { 395 if (a2dpProfile.setActiveDevice(getDevice())) { 396 Log.i(TAG, "OnPreferenceClickListener: A2DP active device=" + this); 397 result = true; 398 } 399 } 400 HeadsetProfile headsetProfile = mProfileManager.getHeadsetProfile(); 401 if ((headsetProfile != null) && isConnectedProfile(headsetProfile)) { 402 if (headsetProfile.setActiveDevice(getDevice())) { 403 Log.i(TAG, "OnPreferenceClickListener: Headset active device=" + this); 404 result = true; 405 } 406 } 407 HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile(); 408 if ((hearingAidProfile != null) && isConnectedProfile(hearingAidProfile)) { 409 if (hearingAidProfile.setActiveDevice(getDevice())) { 410 Log.i(TAG, "OnPreferenceClickListener: Hearing Aid active device=" + this); 411 result = true; 412 } 413 } 414 return result; 415 } 416 refreshName()417 void refreshName() { 418 if (BluetoothUtils.D) { 419 Log.d(TAG, "Device name: " + getName()); 420 } 421 dispatchAttributesChanged(); 422 } 423 424 /** 425 * Checks if device has a human readable name besides MAC address 426 * @return true if device's alias name is not null nor empty, false otherwise 427 */ hasHumanReadableName()428 public boolean hasHumanReadableName() { 429 return !TextUtils.isEmpty(mDevice.getAliasName()); 430 } 431 432 /** 433 * Get battery level from remote device 434 * @return battery level in percentage [0-100], or {@link BluetoothDevice#BATTERY_LEVEL_UNKNOWN} 435 */ getBatteryLevel()436 public int getBatteryLevel() { 437 return mDevice.getBatteryLevel(); 438 } 439 refresh()440 void refresh() { 441 dispatchAttributesChanged(); 442 } 443 setJustDiscovered(boolean justDiscovered)444 public void setJustDiscovered(boolean justDiscovered) { 445 if (mJustDiscovered != justDiscovered) { 446 mJustDiscovered = justDiscovered; 447 dispatchAttributesChanged(); 448 } 449 } 450 getBondState()451 public int getBondState() { 452 return mDevice.getBondState(); 453 } 454 455 /** 456 * Update the device status as active or non-active per Bluetooth profile. 457 * 458 * @param isActive true if the device is active 459 * @param bluetoothProfile the Bluetooth profile 460 */ onActiveDeviceChanged(boolean isActive, int bluetoothProfile)461 public void onActiveDeviceChanged(boolean isActive, int bluetoothProfile) { 462 boolean changed = false; 463 switch (bluetoothProfile) { 464 case BluetoothProfile.A2DP: 465 changed = (mIsActiveDeviceA2dp != isActive); 466 mIsActiveDeviceA2dp = isActive; 467 break; 468 case BluetoothProfile.HEADSET: 469 changed = (mIsActiveDeviceHeadset != isActive); 470 mIsActiveDeviceHeadset = isActive; 471 break; 472 case BluetoothProfile.HEARING_AID: 473 changed = (mIsActiveDeviceHearingAid != isActive); 474 mIsActiveDeviceHearingAid = isActive; 475 break; 476 default: 477 Log.w(TAG, "onActiveDeviceChanged: unknown profile " + bluetoothProfile + 478 " isActive " + isActive); 479 break; 480 } 481 if (changed) { 482 dispatchAttributesChanged(); 483 } 484 } 485 486 /** 487 * Update the profile audio state. 488 */ onAudioModeChanged()489 void onAudioModeChanged() { 490 dispatchAttributesChanged(); 491 } 492 /** 493 * Get the device status as active or non-active per Bluetooth profile. 494 * 495 * @param bluetoothProfile the Bluetooth profile 496 * @return true if the device is active 497 */ 498 @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) isActiveDevice(int bluetoothProfile)499 public boolean isActiveDevice(int bluetoothProfile) { 500 switch (bluetoothProfile) { 501 case BluetoothProfile.A2DP: 502 return mIsActiveDeviceA2dp; 503 case BluetoothProfile.HEADSET: 504 return mIsActiveDeviceHeadset; 505 case BluetoothProfile.HEARING_AID: 506 return mIsActiveDeviceHearingAid; 507 default: 508 Log.w(TAG, "getActiveDevice: unknown profile " + bluetoothProfile); 509 break; 510 } 511 return false; 512 } 513 setRssi(short rssi)514 void setRssi(short rssi) { 515 if (mRssi != rssi) { 516 mRssi = rssi; 517 dispatchAttributesChanged(); 518 } 519 } 520 521 /** 522 * Checks whether we are connected to this device (any profile counts). 523 * 524 * @return Whether it is connected. 525 */ isConnected()526 public boolean isConnected() { 527 synchronized (mProfileLock) { 528 for (LocalBluetoothProfile profile : mProfiles) { 529 int status = getProfileConnectionState(profile); 530 if (status == BluetoothProfile.STATE_CONNECTED) { 531 return true; 532 } 533 } 534 535 return false; 536 } 537 } 538 isConnectedProfile(LocalBluetoothProfile profile)539 public boolean isConnectedProfile(LocalBluetoothProfile profile) { 540 int status = getProfileConnectionState(profile); 541 return status == BluetoothProfile.STATE_CONNECTED; 542 543 } 544 isBusy()545 public boolean isBusy() { 546 synchronized (mProfileLock) { 547 for (LocalBluetoothProfile profile : mProfiles) { 548 int status = getProfileConnectionState(profile); 549 if (status == BluetoothProfile.STATE_CONNECTING 550 || status == BluetoothProfile.STATE_DISCONNECTING) { 551 return true; 552 } 553 } 554 return getBondState() == BluetoothDevice.BOND_BONDING; 555 } 556 } 557 updateProfiles()558 private boolean updateProfiles() { 559 ParcelUuid[] uuids = mDevice.getUuids(); 560 if (uuids == null) return false; 561 562 ParcelUuid[] localUuids = mLocalAdapter.getUuids(); 563 if (localUuids == null) return false; 564 565 /* 566 * Now we know if the device supports PBAP, update permissions... 567 */ 568 processPhonebookAccess(); 569 570 synchronized (mProfileLock) { 571 mProfileManager.updateProfiles(uuids, localUuids, mProfiles, mRemovedProfiles, 572 mLocalNapRoleConnected, mDevice); 573 } 574 575 if (BluetoothUtils.D) { 576 Log.e(TAG, "updating profiles for " + mDevice.getAliasName() + ", " + mDevice); 577 BluetoothClass bluetoothClass = mDevice.getBluetoothClass(); 578 579 if (bluetoothClass != null) Log.v(TAG, "Class: " + bluetoothClass.toString()); 580 Log.v(TAG, "UUID:"); 581 for (ParcelUuid uuid : uuids) { 582 Log.v(TAG, " " + uuid); 583 } 584 } 585 return true; 586 } 587 fetchActiveDevices()588 private void fetchActiveDevices() { 589 A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile(); 590 if (a2dpProfile != null) { 591 mIsActiveDeviceA2dp = mDevice.equals(a2dpProfile.getActiveDevice()); 592 } 593 HeadsetProfile headsetProfile = mProfileManager.getHeadsetProfile(); 594 if (headsetProfile != null) { 595 mIsActiveDeviceHeadset = mDevice.equals(headsetProfile.getActiveDevice()); 596 } 597 HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile(); 598 if (hearingAidProfile != null) { 599 mIsActiveDeviceHearingAid = hearingAidProfile.getActiveDevices().contains(mDevice); 600 } 601 } 602 603 /** 604 * Refreshes the UI when framework alerts us of a UUID change. 605 */ onUuidChanged()606 void onUuidChanged() { 607 updateProfiles(); 608 ParcelUuid[] uuids = mDevice.getUuids(); 609 610 long timeout = MAX_UUID_DELAY_FOR_AUTO_CONNECT; 611 if (BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.Hogp)) { 612 timeout = MAX_HOGP_DELAY_FOR_AUTO_CONNECT; 613 } else if (BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.HearingAid)) { 614 timeout = MAX_HEARING_AIDS_DELAY_FOR_AUTO_CONNECT; 615 } 616 617 if (BluetoothUtils.D) { 618 Log.d(TAG, "onUuidChanged: Time since last connect=" 619 + (SystemClock.elapsedRealtime() - mConnectAttempted)); 620 } 621 622 /* 623 * If a connect was attempted earlier without any UUID, we will do the connect now. 624 * Otherwise, allow the connect on UUID change. 625 */ 626 if (!mProfiles.isEmpty() 627 && ((mConnectAttempted + timeout) > SystemClock.elapsedRealtime())) { 628 connectWithoutResettingTimer(false); 629 } 630 631 dispatchAttributesChanged(); 632 } 633 onBondingStateChanged(int bondState)634 void onBondingStateChanged(int bondState) { 635 if (bondState == BluetoothDevice.BOND_NONE) { 636 synchronized (mProfileLock) { 637 mProfiles.clear(); 638 } 639 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_UNKNOWN); 640 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_UNKNOWN); 641 mDevice.setSimAccessPermission(BluetoothDevice.ACCESS_UNKNOWN); 642 } 643 644 refresh(); 645 646 if (bondState == BluetoothDevice.BOND_BONDED) { 647 if (mDevice.isBluetoothDock()) { 648 onBondingDockConnect(); 649 } else if (mDevice.isBondingInitiatedLocally()) { 650 connect(false); 651 } 652 } 653 } 654 getBtClass()655 public BluetoothClass getBtClass() { 656 return mDevice.getBluetoothClass(); 657 } 658 getProfiles()659 public List<LocalBluetoothProfile> getProfiles() { 660 return Collections.unmodifiableList(mProfiles); 661 } 662 getConnectableProfiles()663 public List<LocalBluetoothProfile> getConnectableProfiles() { 664 List<LocalBluetoothProfile> connectableProfiles = 665 new ArrayList<LocalBluetoothProfile>(); 666 synchronized (mProfileLock) { 667 for (LocalBluetoothProfile profile : mProfiles) { 668 if (profile.accessProfileEnabled()) { 669 connectableProfiles.add(profile); 670 } 671 } 672 } 673 return connectableProfiles; 674 } 675 getRemovedProfiles()676 public List<LocalBluetoothProfile> getRemovedProfiles() { 677 return mRemovedProfiles; 678 } 679 registerCallback(Callback callback)680 public void registerCallback(Callback callback) { 681 synchronized (mCallbacks) { 682 mCallbacks.add(callback); 683 } 684 } 685 unregisterCallback(Callback callback)686 public void unregisterCallback(Callback callback) { 687 synchronized (mCallbacks) { 688 mCallbacks.remove(callback); 689 } 690 } 691 dispatchAttributesChanged()692 void dispatchAttributesChanged() { 693 synchronized (mCallbacks) { 694 for (Callback callback : mCallbacks) { 695 callback.onDeviceAttributesChanged(); 696 } 697 } 698 } 699 700 @Override toString()701 public String toString() { 702 return mDevice.toString(); 703 } 704 705 @Override equals(Object o)706 public boolean equals(Object o) { 707 if ((o == null) || !(o instanceof CachedBluetoothDevice)) { 708 return false; 709 } 710 return mDevice.equals(((CachedBluetoothDevice) o).mDevice); 711 } 712 713 @Override hashCode()714 public int hashCode() { 715 return mDevice.getAddress().hashCode(); 716 } 717 718 // This comparison uses non-final fields so the sort order may change 719 // when device attributes change (such as bonding state). Settings 720 // will completely refresh the device list when this happens. compareTo(CachedBluetoothDevice another)721 public int compareTo(CachedBluetoothDevice another) { 722 // Connected above not connected 723 int comparison = (another.isConnected() ? 1 : 0) - (isConnected() ? 1 : 0); 724 if (comparison != 0) return comparison; 725 726 // Paired above not paired 727 comparison = (another.getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0) - 728 (getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0); 729 if (comparison != 0) return comparison; 730 731 // Just discovered above discovered in the past 732 comparison = (another.mJustDiscovered ? 1 : 0) - (mJustDiscovered ? 1 : 0); 733 if (comparison != 0) return comparison; 734 735 // Stronger signal above weaker signal 736 comparison = another.mRssi - mRssi; 737 if (comparison != 0) return comparison; 738 739 // Fallback on name 740 return getName().compareTo(another.getName()); 741 } 742 743 public interface Callback { onDeviceAttributesChanged()744 void onDeviceAttributesChanged(); 745 } 746 747 // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth 748 // app's shared preferences). migratePhonebookPermissionChoice()749 private void migratePhonebookPermissionChoice() { 750 SharedPreferences preferences = mContext.getSharedPreferences( 751 "bluetooth_phonebook_permission", Context.MODE_PRIVATE); 752 if (!preferences.contains(mDevice.getAddress())) { 753 return; 754 } 755 756 if (mDevice.getPhonebookAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) { 757 int oldPermission = 758 preferences.getInt(mDevice.getAddress(), BluetoothDevice.ACCESS_UNKNOWN); 759 if (oldPermission == BluetoothDevice.ACCESS_ALLOWED) { 760 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED); 761 } else if (oldPermission == BluetoothDevice.ACCESS_REJECTED) { 762 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED); 763 } 764 } 765 766 SharedPreferences.Editor editor = preferences.edit(); 767 editor.remove(mDevice.getAddress()); 768 editor.commit(); 769 } 770 771 // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth 772 // app's shared preferences). migrateMessagePermissionChoice()773 private void migrateMessagePermissionChoice() { 774 SharedPreferences preferences = mContext.getSharedPreferences( 775 "bluetooth_message_permission", Context.MODE_PRIVATE); 776 if (!preferences.contains(mDevice.getAddress())) { 777 return; 778 } 779 780 if (mDevice.getMessageAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) { 781 int oldPermission = 782 preferences.getInt(mDevice.getAddress(), BluetoothDevice.ACCESS_UNKNOWN); 783 if (oldPermission == BluetoothDevice.ACCESS_ALLOWED) { 784 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_ALLOWED); 785 } else if (oldPermission == BluetoothDevice.ACCESS_REJECTED) { 786 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_REJECTED); 787 } 788 } 789 790 SharedPreferences.Editor editor = preferences.edit(); 791 editor.remove(mDevice.getAddress()); 792 editor.commit(); 793 } 794 processPhonebookAccess()795 private void processPhonebookAccess() { 796 if (mDevice.getBondState() != BluetoothDevice.BOND_BONDED) return; 797 798 ParcelUuid[] uuids = mDevice.getUuids(); 799 if (BluetoothUuid.containsAnyUuid(uuids, PbapServerProfile.PBAB_CLIENT_UUIDS)) { 800 // The pairing dialog now warns of phone-book access for paired devices. 801 // No separate prompt is displayed after pairing. 802 if (mDevice.getPhonebookAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) { 803 if (mDevice.getBluetoothClass().getDeviceClass() 804 == BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE || 805 mDevice.getBluetoothClass().getDeviceClass() 806 == BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET) { 807 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED); 808 } else { 809 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED); 810 } 811 } 812 } 813 } 814 getMaxConnectionState()815 public int getMaxConnectionState() { 816 int maxState = BluetoothProfile.STATE_DISCONNECTED; 817 synchronized (mProfileLock) { 818 for (LocalBluetoothProfile profile : getProfiles()) { 819 int connectionStatus = getProfileConnectionState(profile); 820 if (connectionStatus > maxState) { 821 maxState = connectionStatus; 822 } 823 } 824 } 825 return maxState; 826 } 827 828 /** 829 * Return full summary that describes connection state of this device 830 * 831 * @see #getConnectionSummary(boolean shortSummary) 832 */ getConnectionSummary()833 public String getConnectionSummary() { 834 return getConnectionSummary(false /* shortSummary */); 835 } 836 837 /** 838 * Return summary that describes connection state of this device. Summary depends on: 839 * 1. Whether device has battery info 840 * 2. Whether device is in active usage(or in phone call) 841 * 842 * @param shortSummary {@code true} if need to return short version summary 843 */ getConnectionSummary(boolean shortSummary)844 public String getConnectionSummary(boolean shortSummary) { 845 boolean profileConnected = false; // Updated as long as BluetoothProfile is connected 846 boolean a2dpConnected = true; // A2DP is connected 847 boolean hfpConnected = true; // HFP is connected 848 boolean hearingAidConnected = true; // Hearing Aid is connected 849 int leftBattery = -1; 850 int rightBattery = -1; 851 852 synchronized (mProfileLock) { 853 for (LocalBluetoothProfile profile : getProfiles()) { 854 int connectionStatus = getProfileConnectionState(profile); 855 856 switch (connectionStatus) { 857 case BluetoothProfile.STATE_CONNECTING: 858 case BluetoothProfile.STATE_DISCONNECTING: 859 return mContext.getString( 860 BluetoothUtils.getConnectionStateSummary(connectionStatus)); 861 862 case BluetoothProfile.STATE_CONNECTED: 863 profileConnected = true; 864 break; 865 866 case BluetoothProfile.STATE_DISCONNECTED: 867 if (profile.isProfileReady()) { 868 if (profile instanceof A2dpProfile 869 || profile instanceof A2dpSinkProfile) { 870 a2dpConnected = false; 871 } else if (profile instanceof HeadsetProfile 872 || profile instanceof HfpClientProfile) { 873 hfpConnected = false; 874 } else if (profile instanceof HearingAidProfile) { 875 hearingAidConnected = false; 876 } 877 } 878 break; 879 } 880 } 881 } 882 883 String batteryLevelPercentageString = null; 884 // Android framework should only set mBatteryLevel to valid range [0-100] or 885 // BluetoothDevice.BATTERY_LEVEL_UNKNOWN, any other value should be a framework bug. 886 // Thus assume here that if value is not BluetoothDevice.BATTERY_LEVEL_UNKNOWN, it must 887 // be valid 888 final int batteryLevel = getBatteryLevel(); 889 if (batteryLevel != BluetoothDevice.BATTERY_LEVEL_UNKNOWN) { 890 // TODO: name com.android.settingslib.bluetooth.Utils something different 891 batteryLevelPercentageString = 892 com.android.settingslib.Utils.formatPercentage(batteryLevel); 893 } 894 895 int stringRes = R.string.bluetooth_pairing; 896 //when profile is connected, information would be available 897 if (profileConnected) { 898 // Update Meta data for connected device 899 if (BluetoothUtils.getBooleanMetaData( 900 mDevice, BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET)) { 901 leftBattery = BluetoothUtils.getIntMetaData(mDevice, 902 BluetoothDevice.METADATA_UNTETHERED_LEFT_BATTERY); 903 rightBattery = BluetoothUtils.getIntMetaData(mDevice, 904 BluetoothDevice.METADATA_UNTETHERED_RIGHT_BATTERY); 905 } 906 907 // Set default string with battery level in device connected situation. 908 if (isTwsBatteryAvailable(leftBattery, rightBattery)) { 909 stringRes = R.string.bluetooth_battery_level_untethered; 910 } else if (batteryLevelPercentageString != null) { 911 stringRes = R.string.bluetooth_battery_level; 912 } 913 914 // Set active string in following device connected situation. 915 // 1. Hearing Aid device active. 916 // 2. Headset device active with in-calling state. 917 // 3. A2DP device active without in-calling state. 918 if (a2dpConnected || hfpConnected || hearingAidConnected) { 919 final boolean isOnCall = Utils.isAudioModeOngoingCall(mContext); 920 if ((mIsActiveDeviceHearingAid) 921 || (mIsActiveDeviceHeadset && isOnCall) 922 || (mIsActiveDeviceA2dp && !isOnCall)) { 923 if (isTwsBatteryAvailable(leftBattery, rightBattery) && !shortSummary) { 924 stringRes = R.string.bluetooth_active_battery_level_untethered; 925 } else if (batteryLevelPercentageString != null && !shortSummary) { 926 stringRes = R.string.bluetooth_active_battery_level; 927 } else { 928 stringRes = R.string.bluetooth_active_no_battery_level; 929 } 930 } 931 } 932 } 933 934 if (stringRes != R.string.bluetooth_pairing 935 || getBondState() == BluetoothDevice.BOND_BONDING) { 936 if (isTwsBatteryAvailable(leftBattery, rightBattery)) { 937 return mContext.getString(stringRes, Utils.formatPercentage(leftBattery), 938 Utils.formatPercentage(rightBattery)); 939 } else { 940 return mContext.getString(stringRes, batteryLevelPercentageString); 941 } 942 } else { 943 return null; 944 } 945 } 946 isTwsBatteryAvailable(int leftBattery, int rightBattery)947 private boolean isTwsBatteryAvailable(int leftBattery, int rightBattery) { 948 return leftBattery >= 0 && rightBattery >= 0; 949 } 950 951 /** 952 * @return resource for android auto string that describes the connection state of this device. 953 */ getCarConnectionSummary()954 public String getCarConnectionSummary() { 955 boolean profileConnected = false; // at least one profile is connected 956 boolean a2dpNotConnected = false; // A2DP is preferred but not connected 957 boolean hfpNotConnected = false; // HFP is preferred but not connected 958 boolean hearingAidNotConnected = false; // Hearing Aid is preferred but not connected 959 960 synchronized (mProfileLock) { 961 for (LocalBluetoothProfile profile : getProfiles()) { 962 int connectionStatus = getProfileConnectionState(profile); 963 964 switch (connectionStatus) { 965 case BluetoothProfile.STATE_CONNECTING: 966 case BluetoothProfile.STATE_DISCONNECTING: 967 return mContext.getString( 968 BluetoothUtils.getConnectionStateSummary(connectionStatus)); 969 970 case BluetoothProfile.STATE_CONNECTED: 971 profileConnected = true; 972 break; 973 974 case BluetoothProfile.STATE_DISCONNECTED: 975 if (profile.isProfileReady()) { 976 if (profile instanceof A2dpProfile 977 || profile instanceof A2dpSinkProfile) { 978 a2dpNotConnected = true; 979 } else if (profile instanceof HeadsetProfile 980 || profile instanceof HfpClientProfile) { 981 hfpNotConnected = true; 982 } else if (profile instanceof HearingAidProfile) { 983 hearingAidNotConnected = true; 984 } 985 } 986 break; 987 } 988 } 989 } 990 991 String batteryLevelPercentageString = null; 992 // Android framework should only set mBatteryLevel to valid range [0-100] or 993 // BluetoothDevice.BATTERY_LEVEL_UNKNOWN, any other value should be a framework bug. 994 // Thus assume here that if value is not BluetoothDevice.BATTERY_LEVEL_UNKNOWN, it must 995 // be valid 996 final int batteryLevel = getBatteryLevel(); 997 if (batteryLevel != BluetoothDevice.BATTERY_LEVEL_UNKNOWN) { 998 // TODO: name com.android.settingslib.bluetooth.Utils something different 999 batteryLevelPercentageString = 1000 com.android.settingslib.Utils.formatPercentage(batteryLevel); 1001 } 1002 1003 // Prepare the string for the Active Device summary 1004 String[] activeDeviceStringsArray = mContext.getResources().getStringArray( 1005 R.array.bluetooth_audio_active_device_summaries); 1006 String activeDeviceString = activeDeviceStringsArray[0]; // Default value: not active 1007 if (mIsActiveDeviceA2dp && mIsActiveDeviceHeadset) { 1008 activeDeviceString = activeDeviceStringsArray[1]; // Active for Media and Phone 1009 } else { 1010 if (mIsActiveDeviceA2dp) { 1011 activeDeviceString = activeDeviceStringsArray[2]; // Active for Media only 1012 } 1013 if (mIsActiveDeviceHeadset) { 1014 activeDeviceString = activeDeviceStringsArray[3]; // Active for Phone only 1015 } 1016 } 1017 if (!hearingAidNotConnected && mIsActiveDeviceHearingAid) { 1018 activeDeviceString = activeDeviceStringsArray[1]; 1019 return mContext.getString(R.string.bluetooth_connected, activeDeviceString); 1020 } 1021 1022 if (profileConnected) { 1023 if (a2dpNotConnected && hfpNotConnected) { 1024 if (batteryLevelPercentageString != null) { 1025 return mContext.getString( 1026 R.string.bluetooth_connected_no_headset_no_a2dp_battery_level, 1027 batteryLevelPercentageString, activeDeviceString); 1028 } else { 1029 return mContext.getString(R.string.bluetooth_connected_no_headset_no_a2dp, 1030 activeDeviceString); 1031 } 1032 1033 } else if (a2dpNotConnected) { 1034 if (batteryLevelPercentageString != null) { 1035 return mContext.getString(R.string.bluetooth_connected_no_a2dp_battery_level, 1036 batteryLevelPercentageString, activeDeviceString); 1037 } else { 1038 return mContext.getString(R.string.bluetooth_connected_no_a2dp, 1039 activeDeviceString); 1040 } 1041 1042 } else if (hfpNotConnected) { 1043 if (batteryLevelPercentageString != null) { 1044 return mContext.getString(R.string.bluetooth_connected_no_headset_battery_level, 1045 batteryLevelPercentageString, activeDeviceString); 1046 } else { 1047 return mContext.getString(R.string.bluetooth_connected_no_headset, 1048 activeDeviceString); 1049 } 1050 } else { 1051 if (batteryLevelPercentageString != null) { 1052 return mContext.getString(R.string.bluetooth_connected_battery_level, 1053 batteryLevelPercentageString, activeDeviceString); 1054 } else { 1055 return mContext.getString(R.string.bluetooth_connected, activeDeviceString); 1056 } 1057 } 1058 } 1059 1060 return getBondState() == BluetoothDevice.BOND_BONDING ? 1061 mContext.getString(R.string.bluetooth_pairing) : null; 1062 } 1063 1064 /** 1065 * @return {@code true} if {@code cachedBluetoothDevice} is a2dp device 1066 */ isConnectedA2dpDevice()1067 public boolean isConnectedA2dpDevice() { 1068 A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile(); 1069 return a2dpProfile != null && a2dpProfile.getConnectionStatus(mDevice) == 1070 BluetoothProfile.STATE_CONNECTED; 1071 } 1072 1073 /** 1074 * @return {@code true} if {@code cachedBluetoothDevice} is HFP device 1075 */ isConnectedHfpDevice()1076 public boolean isConnectedHfpDevice() { 1077 HeadsetProfile headsetProfile = mProfileManager.getHeadsetProfile(); 1078 return headsetProfile != null && headsetProfile.getConnectionStatus(mDevice) == 1079 BluetoothProfile.STATE_CONNECTED; 1080 } 1081 1082 /** 1083 * @return {@code true} if {@code cachedBluetoothDevice} is Hearing Aid device 1084 */ isConnectedHearingAidDevice()1085 public boolean isConnectedHearingAidDevice() { 1086 HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile(); 1087 return hearingAidProfile != null && hearingAidProfile.getConnectionStatus(mDevice) == 1088 BluetoothProfile.STATE_CONNECTED; 1089 } 1090 getSubDevice()1091 public CachedBluetoothDevice getSubDevice() { 1092 return mSubDevice; 1093 } 1094 setSubDevice(CachedBluetoothDevice subDevice)1095 public void setSubDevice(CachedBluetoothDevice subDevice) { 1096 mSubDevice = subDevice; 1097 } 1098 switchSubDeviceContent()1099 public void switchSubDeviceContent() { 1100 // Backup from main device 1101 BluetoothDevice tmpDevice = mDevice; 1102 short tmpRssi = mRssi; 1103 boolean tmpJustDiscovered = mJustDiscovered; 1104 // Set main device from sub device 1105 mDevice = mSubDevice.mDevice; 1106 mRssi = mSubDevice.mRssi; 1107 mJustDiscovered = mSubDevice.mJustDiscovered; 1108 // Set sub device from backup 1109 mSubDevice.mDevice = tmpDevice; 1110 mSubDevice.mRssi = tmpRssi; 1111 mSubDevice.mJustDiscovered = tmpJustDiscovered; 1112 fetchActiveDevices(); 1113 } 1114 } 1115