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.EventLog; 31 import android.util.Log; 32 33 import androidx.annotation.VisibleForTesting; 34 35 import com.android.settingslib.R; 36 import com.android.settingslib.Utils; 37 38 import java.util.ArrayList; 39 import java.util.Collection; 40 import java.util.Collections; 41 import java.util.List; 42 43 /** 44 * CachedBluetoothDevice represents a remote Bluetooth device. It contains 45 * attributes of the device (such as the address, name, RSSI, etc.) and 46 * functionality that can be performed on the device (connect, pair, disconnect, 47 * etc.). 48 */ 49 public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> { 50 private static final String TAG = "CachedBluetoothDevice"; 51 52 // See mConnectAttempted 53 private static final long MAX_UUID_DELAY_FOR_AUTO_CONNECT = 5000; 54 // Some Hearing Aids (especially the 2nd device) needs more time to do service discovery 55 private static final long MAX_HEARING_AIDS_DELAY_FOR_AUTO_CONNECT = 15000; 56 private static final long MAX_HOGP_DELAY_FOR_AUTO_CONNECT = 30000; 57 58 private final Context mContext; 59 private final BluetoothAdapter mLocalAdapter; 60 private final LocalBluetoothProfileManager mProfileManager; 61 private final Object mProfileLock = new Object(); 62 BluetoothDevice mDevice; 63 private long mHiSyncId; 64 // Need this since there is no method for getting RSSI 65 short mRssi; 66 // mProfiles and mRemovedProfiles does not do swap() between main and sub device. It is 67 // because current sub device is only for HearingAid and its profile is the same. 68 private final List<LocalBluetoothProfile> mProfiles = new ArrayList<>(); 69 70 // List of profiles that were previously in mProfiles, but have been removed 71 private final List<LocalBluetoothProfile> mRemovedProfiles = new ArrayList<>(); 72 73 // Device supports PANU but not NAP: remove PanProfile after device disconnects from NAP 74 private boolean mLocalNapRoleConnected; 75 76 boolean mJustDiscovered; 77 78 private final Collection<Callback> mCallbacks = new ArrayList<>(); 79 80 /** 81 * Last time a bt profile auto-connect was attempted. 82 * If an ACTION_UUID intent comes in within 83 * MAX_UUID_DELAY_FOR_AUTO_CONNECT milliseconds, we will try auto-connect 84 * again with the new UUIDs 85 */ 86 private long mConnectAttempted; 87 88 // Active device state 89 private boolean mIsActiveDeviceA2dp = false; 90 private boolean mIsActiveDeviceHeadset = false; 91 private boolean mIsActiveDeviceHearingAid = false; 92 // Group second device for Hearing Aid 93 private CachedBluetoothDevice mSubDevice; 94 CachedBluetoothDevice(Context context, LocalBluetoothProfileManager profileManager, BluetoothDevice device)95 CachedBluetoothDevice(Context context, LocalBluetoothProfileManager profileManager, 96 BluetoothDevice device) { 97 mContext = context; 98 mLocalAdapter = BluetoothAdapter.getDefaultAdapter(); 99 mProfileManager = profileManager; 100 mDevice = device; 101 fillData(); 102 mHiSyncId = BluetoothHearingAid.HI_SYNC_ID_INVALID; 103 } 104 105 /** 106 * Describes the current device and profile for logging. 107 * 108 * @param profile Profile to describe 109 * @return Description of the device and profile 110 */ describe(LocalBluetoothProfile profile)111 private String describe(LocalBluetoothProfile profile) { 112 StringBuilder sb = new StringBuilder(); 113 sb.append("Address:").append(mDevice); 114 if (profile != null) { 115 sb.append(" Profile:").append(profile); 116 } 117 118 return sb.toString(); 119 } 120 onProfileStateChanged(LocalBluetoothProfile profile, int newProfileState)121 void onProfileStateChanged(LocalBluetoothProfile profile, int newProfileState) { 122 if (BluetoothUtils.D) { 123 Log.d(TAG, "onProfileStateChanged: profile " + profile + ", device=" + mDevice 124 + ", newProfileState " + newProfileState); 125 } 126 if (mLocalAdapter.getState() == BluetoothAdapter.STATE_TURNING_OFF) 127 { 128 if (BluetoothUtils.D) { 129 Log.d(TAG, " BT Turninig Off...Profile conn state change ignored..."); 130 } 131 return; 132 } 133 134 synchronized (mProfileLock) { 135 if (newProfileState == BluetoothProfile.STATE_CONNECTED) { 136 if (profile instanceof MapProfile) { 137 profile.setPreferred(mDevice, true); 138 } 139 if (!mProfiles.contains(profile)) { 140 mRemovedProfiles.remove(profile); 141 mProfiles.add(profile); 142 if (profile instanceof PanProfile 143 && ((PanProfile) profile).isLocalRoleNap(mDevice)) { 144 // Device doesn't support NAP, so remove PanProfile on disconnect 145 mLocalNapRoleConnected = true; 146 } 147 } 148 } else if (profile instanceof MapProfile 149 && newProfileState == BluetoothProfile.STATE_DISCONNECTED) { 150 profile.setPreferred(mDevice, false); 151 } else if (mLocalNapRoleConnected && profile instanceof PanProfile 152 && ((PanProfile) profile).isLocalRoleNap(mDevice) 153 && newProfileState == BluetoothProfile.STATE_DISCONNECTED) { 154 Log.d(TAG, "Removing PanProfile from device after NAP disconnect"); 155 mProfiles.remove(profile); 156 mRemovedProfiles.add(profile); 157 mLocalNapRoleConnected = false; 158 } 159 } 160 161 fetchActiveDevices(); 162 } 163 disconnect()164 public void disconnect() { 165 synchronized (mProfileLock) { 166 for (LocalBluetoothProfile profile : mProfiles) { 167 disconnect(profile); 168 } 169 } 170 // Disconnect PBAP server in case its connected 171 // This is to ensure all the profiles are disconnected as some CK/Hs do not 172 // disconnect PBAP connection when HF connection is brought down 173 PbapServerProfile PbapProfile = mProfileManager.getPbapProfile(); 174 if (PbapProfile != null && isConnectedProfile(PbapProfile)) 175 { 176 PbapProfile.disconnect(mDevice); 177 } 178 } 179 disconnect(LocalBluetoothProfile profile)180 public void disconnect(LocalBluetoothProfile profile) { 181 if (profile.disconnect(mDevice)) { 182 if (BluetoothUtils.D) { 183 Log.d(TAG, "Command sent successfully:DISCONNECT " + describe(profile)); 184 } 185 } 186 } 187 connect(boolean connectAllProfiles)188 public void connect(boolean connectAllProfiles) { 189 if (!ensurePaired()) { 190 return; 191 } 192 193 mConnectAttempted = SystemClock.elapsedRealtime(); 194 connectWithoutResettingTimer(connectAllProfiles); 195 } 196 getHiSyncId()197 public long getHiSyncId() { 198 return mHiSyncId; 199 } 200 setHiSyncId(long id)201 public void setHiSyncId(long id) { 202 if (BluetoothUtils.D) { 203 Log.d(TAG, "setHiSyncId: mDevice " + mDevice + ", id " + id); 204 } 205 mHiSyncId = id; 206 } 207 isHearingAidDevice()208 public boolean isHearingAidDevice() { 209 return mHiSyncId != BluetoothHearingAid.HI_SYNC_ID_INVALID; 210 } 211 onBondingDockConnect()212 void onBondingDockConnect() { 213 // Attempt to connect if UUIDs are available. Otherwise, 214 // we will connect when the ACTION_UUID intent arrives. 215 connect(false); 216 } 217 connectWithoutResettingTimer(boolean connectAllProfiles)218 private void connectWithoutResettingTimer(boolean connectAllProfiles) { 219 synchronized (mProfileLock) { 220 // Try to initialize the profiles if they were not. 221 if (mProfiles.isEmpty()) { 222 // if mProfiles is empty, then do not invoke updateProfiles. This causes a race 223 // condition with carkits during pairing, wherein RemoteDevice.UUIDs have been 224 // updated from bluetooth stack but ACTION.uuid is not sent yet. 225 // Eventually ACTION.uuid will be received which shall trigger the connection of the 226 // various profiles 227 // If UUIDs are not available yet, connect will be happen 228 // upon arrival of the ACTION_UUID intent. 229 Log.d(TAG, "No profiles. Maybe we will connect later for device " + mDevice); 230 return; 231 } 232 233 int preferredProfiles = 0; 234 for (LocalBluetoothProfile profile : mProfiles) { 235 if (connectAllProfiles ? profile.accessProfileEnabled() 236 : profile.isAutoConnectable()) { 237 if (profile.isPreferred(mDevice)) { 238 ++preferredProfiles; 239 connectInt(profile); 240 } 241 } 242 } 243 if (BluetoothUtils.D) Log.d(TAG, "Preferred profiles = " + preferredProfiles); 244 245 if (preferredProfiles == 0) { 246 connectAutoConnectableProfiles(); 247 } 248 } 249 } 250 connectAutoConnectableProfiles()251 private void connectAutoConnectableProfiles() { 252 if (!ensurePaired()) { 253 return; 254 } 255 256 synchronized (mProfileLock) { 257 for (LocalBluetoothProfile profile : mProfiles) { 258 if (profile.isAutoConnectable()) { 259 profile.setPreferred(mDevice, true); 260 connectInt(profile); 261 } 262 } 263 } 264 } 265 266 /** 267 * Connect this device to the specified profile. 268 * 269 * @param profile the profile to use with the remote device 270 */ connectProfile(LocalBluetoothProfile profile)271 public void connectProfile(LocalBluetoothProfile profile) { 272 mConnectAttempted = SystemClock.elapsedRealtime(); 273 connectInt(profile); 274 // Refresh the UI based on profile.connect() call 275 refresh(); 276 } 277 connectInt(LocalBluetoothProfile profile)278 synchronized void connectInt(LocalBluetoothProfile profile) { 279 if (!ensurePaired()) { 280 return; 281 } 282 if (profile.connect(mDevice)) { 283 if (BluetoothUtils.D) { 284 Log.d(TAG, "Command sent successfully:CONNECT " + describe(profile)); 285 } 286 return; 287 } 288 Log.i(TAG, "Failed to connect " + profile.toString() + " to " + getName()); 289 } 290 ensurePaired()291 private boolean ensurePaired() { 292 if (getBondState() == BluetoothDevice.BOND_NONE) { 293 startPairing(); 294 return false; 295 } else { 296 return true; 297 } 298 } 299 startPairing()300 public boolean startPairing() { 301 // Pairing is unreliable while scanning, so cancel discovery 302 if (mLocalAdapter.isDiscovering()) { 303 mLocalAdapter.cancelDiscovery(); 304 } 305 306 if (!mDevice.createBond()) { 307 return false; 308 } 309 310 return true; 311 } 312 unpair()313 public void unpair() { 314 int state = getBondState(); 315 316 if (state == BluetoothDevice.BOND_BONDING) { 317 mDevice.cancelBondProcess(); 318 } 319 320 if (state != BluetoothDevice.BOND_NONE) { 321 final BluetoothDevice dev = mDevice; 322 if (dev != null) { 323 final boolean successful = dev.removeBond(); 324 if (successful) { 325 if (BluetoothUtils.D) { 326 Log.d(TAG, "Command sent successfully:REMOVE_BOND " + describe(null)); 327 } 328 } else if (BluetoothUtils.V) { 329 Log.v(TAG, "Framework rejected command immediately:REMOVE_BOND " + 330 describe(null)); 331 } 332 } 333 } 334 } 335 getProfileConnectionState(LocalBluetoothProfile profile)336 public int getProfileConnectionState(LocalBluetoothProfile profile) { 337 return profile != null 338 ? profile.getConnectionStatus(mDevice) 339 : BluetoothProfile.STATE_DISCONNECTED; 340 } 341 342 // TODO: do any of these need to run async on a background thread? fillData()343 private void fillData() { 344 updateProfiles(); 345 fetchActiveDevices(); 346 migratePhonebookPermissionChoice(); 347 migrateMessagePermissionChoice(); 348 349 dispatchAttributesChanged(); 350 } 351 getDevice()352 public BluetoothDevice getDevice() { 353 return mDevice; 354 } 355 356 /** 357 * Convenience method that can be mocked - it lets tests avoid having to call getDevice() which 358 * causes problems in tests since BluetoothDevice is final and cannot be mocked. 359 * @return the address of this device 360 */ getAddress()361 public String getAddress() { 362 return mDevice.getAddress(); 363 } 364 365 /** 366 * Get name from remote device 367 * @return {@link BluetoothDevice#getAliasName()} if 368 * {@link BluetoothDevice#getAliasName()} is not null otherwise return 369 * {@link BluetoothDevice#getAddress()} 370 */ getName()371 public String getName() { 372 final String aliasName = mDevice.getAliasName(); 373 return TextUtils.isEmpty(aliasName) ? getAddress() : aliasName; 374 } 375 376 /** 377 * User changes the device name 378 * @param name new alias name to be set, should never be null 379 */ setName(String name)380 public void setName(String name) { 381 // Prevent getName() to be set to null if setName(null) is called 382 if (name != null && !TextUtils.equals(name, getName())) { 383 mDevice.setAlias(name); 384 dispatchAttributesChanged(); 385 } 386 } 387 388 /** 389 * Set this device as active device 390 * @return true if at least one profile on this device is set to active, false otherwise 391 */ setActive()392 public boolean setActive() { 393 boolean result = false; 394 A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile(); 395 if (a2dpProfile != null && isConnectedProfile(a2dpProfile)) { 396 if (a2dpProfile.setActiveDevice(getDevice())) { 397 Log.i(TAG, "OnPreferenceClickListener: A2DP active device=" + this); 398 result = true; 399 } 400 } 401 HeadsetProfile headsetProfile = mProfileManager.getHeadsetProfile(); 402 if ((headsetProfile != null) && isConnectedProfile(headsetProfile)) { 403 if (headsetProfile.setActiveDevice(getDevice())) { 404 Log.i(TAG, "OnPreferenceClickListener: Headset active device=" + this); 405 result = true; 406 } 407 } 408 HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile(); 409 if ((hearingAidProfile != null) && isConnectedProfile(hearingAidProfile)) { 410 if (hearingAidProfile.setActiveDevice(getDevice())) { 411 Log.i(TAG, "OnPreferenceClickListener: Hearing Aid active device=" + this); 412 result = true; 413 } 414 } 415 return result; 416 } 417 refreshName()418 void refreshName() { 419 if (BluetoothUtils.D) { 420 Log.d(TAG, "Device name: " + getName()); 421 } 422 dispatchAttributesChanged(); 423 } 424 425 /** 426 * Checks if device has a human readable name besides MAC address 427 * @return true if device's alias name is not null nor empty, false otherwise 428 */ hasHumanReadableName()429 public boolean hasHumanReadableName() { 430 return !TextUtils.isEmpty(mDevice.getAliasName()); 431 } 432 433 /** 434 * Get battery level from remote device 435 * @return battery level in percentage [0-100], or {@link BluetoothDevice#BATTERY_LEVEL_UNKNOWN} 436 */ getBatteryLevel()437 public int getBatteryLevel() { 438 return mDevice.getBatteryLevel(); 439 } 440 refresh()441 void refresh() { 442 dispatchAttributesChanged(); 443 } 444 setJustDiscovered(boolean justDiscovered)445 public void setJustDiscovered(boolean justDiscovered) { 446 if (mJustDiscovered != justDiscovered) { 447 mJustDiscovered = justDiscovered; 448 dispatchAttributesChanged(); 449 } 450 } 451 getBondState()452 public int getBondState() { 453 return mDevice.getBondState(); 454 } 455 456 /** 457 * Update the device status as active or non-active per Bluetooth profile. 458 * 459 * @param isActive true if the device is active 460 * @param bluetoothProfile the Bluetooth profile 461 */ onActiveDeviceChanged(boolean isActive, int bluetoothProfile)462 public void onActiveDeviceChanged(boolean isActive, int bluetoothProfile) { 463 boolean changed = false; 464 switch (bluetoothProfile) { 465 case BluetoothProfile.A2DP: 466 changed = (mIsActiveDeviceA2dp != isActive); 467 mIsActiveDeviceA2dp = isActive; 468 break; 469 case BluetoothProfile.HEADSET: 470 changed = (mIsActiveDeviceHeadset != isActive); 471 mIsActiveDeviceHeadset = isActive; 472 break; 473 case BluetoothProfile.HEARING_AID: 474 changed = (mIsActiveDeviceHearingAid != isActive); 475 mIsActiveDeviceHearingAid = isActive; 476 break; 477 default: 478 Log.w(TAG, "onActiveDeviceChanged: unknown profile " + bluetoothProfile + 479 " isActive " + isActive); 480 break; 481 } 482 if (changed) { 483 dispatchAttributesChanged(); 484 } 485 } 486 487 /** 488 * Update the profile audio state. 489 */ onAudioModeChanged()490 void onAudioModeChanged() { 491 dispatchAttributesChanged(); 492 } 493 /** 494 * Get the device status as active or non-active per Bluetooth profile. 495 * 496 * @param bluetoothProfile the Bluetooth profile 497 * @return true if the device is active 498 */ 499 @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) isActiveDevice(int bluetoothProfile)500 public boolean isActiveDevice(int bluetoothProfile) { 501 switch (bluetoothProfile) { 502 case BluetoothProfile.A2DP: 503 return mIsActiveDeviceA2dp; 504 case BluetoothProfile.HEADSET: 505 return mIsActiveDeviceHeadset; 506 case BluetoothProfile.HEARING_AID: 507 return mIsActiveDeviceHearingAid; 508 default: 509 Log.w(TAG, "getActiveDevice: unknown profile " + bluetoothProfile); 510 break; 511 } 512 return false; 513 } 514 setRssi(short rssi)515 void setRssi(short rssi) { 516 if (mRssi != rssi) { 517 mRssi = rssi; 518 dispatchAttributesChanged(); 519 } 520 } 521 522 /** 523 * Checks whether we are connected to this device (any profile counts). 524 * 525 * @return Whether it is connected. 526 */ isConnected()527 public boolean isConnected() { 528 synchronized (mProfileLock) { 529 for (LocalBluetoothProfile profile : mProfiles) { 530 int status = getProfileConnectionState(profile); 531 if (status == BluetoothProfile.STATE_CONNECTED) { 532 return true; 533 } 534 } 535 536 return false; 537 } 538 } 539 isConnectedProfile(LocalBluetoothProfile profile)540 public boolean isConnectedProfile(LocalBluetoothProfile profile) { 541 int status = getProfileConnectionState(profile); 542 return status == BluetoothProfile.STATE_CONNECTED; 543 544 } 545 isBusy()546 public boolean isBusy() { 547 synchronized (mProfileLock) { 548 for (LocalBluetoothProfile profile : mProfiles) { 549 int status = getProfileConnectionState(profile); 550 if (status == BluetoothProfile.STATE_CONNECTING 551 || status == BluetoothProfile.STATE_DISCONNECTING) { 552 return true; 553 } 554 } 555 return getBondState() == BluetoothDevice.BOND_BONDING; 556 } 557 } 558 updateProfiles()559 private boolean updateProfiles() { 560 ParcelUuid[] uuids = mDevice.getUuids(); 561 if (uuids == null) return false; 562 563 ParcelUuid[] localUuids = mLocalAdapter.getUuids(); 564 if (localUuids == null) return false; 565 566 /* 567 * Now we know if the device supports PBAP, update permissions... 568 */ 569 processPhonebookAccess(); 570 571 synchronized (mProfileLock) { 572 mProfileManager.updateProfiles(uuids, localUuids, mProfiles, mRemovedProfiles, 573 mLocalNapRoleConnected, mDevice); 574 } 575 576 if (BluetoothUtils.D) { 577 Log.e(TAG, "updating profiles for " + mDevice.getAliasName() + ", " + mDevice); 578 BluetoothClass bluetoothClass = mDevice.getBluetoothClass(); 579 580 if (bluetoothClass != null) Log.v(TAG, "Class: " + bluetoothClass.toString()); 581 Log.v(TAG, "UUID:"); 582 for (ParcelUuid uuid : uuids) { 583 Log.v(TAG, " " + uuid); 584 } 585 } 586 return true; 587 } 588 fetchActiveDevices()589 private void fetchActiveDevices() { 590 A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile(); 591 if (a2dpProfile != null) { 592 mIsActiveDeviceA2dp = mDevice.equals(a2dpProfile.getActiveDevice()); 593 } 594 HeadsetProfile headsetProfile = mProfileManager.getHeadsetProfile(); 595 if (headsetProfile != null) { 596 mIsActiveDeviceHeadset = mDevice.equals(headsetProfile.getActiveDevice()); 597 } 598 HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile(); 599 if (hearingAidProfile != null) { 600 mIsActiveDeviceHearingAid = hearingAidProfile.getActiveDevices().contains(mDevice); 601 } 602 } 603 604 /** 605 * Refreshes the UI when framework alerts us of a UUID change. 606 */ onUuidChanged()607 void onUuidChanged() { 608 updateProfiles(); 609 ParcelUuid[] uuids = mDevice.getUuids(); 610 611 long timeout = MAX_UUID_DELAY_FOR_AUTO_CONNECT; 612 if (BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.Hogp)) { 613 timeout = MAX_HOGP_DELAY_FOR_AUTO_CONNECT; 614 } else if (BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.HearingAid)) { 615 timeout = MAX_HEARING_AIDS_DELAY_FOR_AUTO_CONNECT; 616 } 617 618 if (BluetoothUtils.D) { 619 Log.d(TAG, "onUuidChanged: Time since last connect=" 620 + (SystemClock.elapsedRealtime() - mConnectAttempted)); 621 } 622 623 /* 624 * If a connect was attempted earlier without any UUID, we will do the connect now. 625 * Otherwise, allow the connect on UUID change. 626 */ 627 if (!mProfiles.isEmpty() 628 && ((mConnectAttempted + timeout) > SystemClock.elapsedRealtime())) { 629 connectWithoutResettingTimer(false); 630 } 631 632 dispatchAttributesChanged(); 633 } 634 onBondingStateChanged(int bondState)635 void onBondingStateChanged(int bondState) { 636 if (bondState == BluetoothDevice.BOND_NONE) { 637 synchronized (mProfileLock) { 638 mProfiles.clear(); 639 } 640 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_UNKNOWN); 641 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_UNKNOWN); 642 mDevice.setSimAccessPermission(BluetoothDevice.ACCESS_UNKNOWN); 643 } 644 645 refresh(); 646 647 if (bondState == BluetoothDevice.BOND_BONDED) { 648 if (mDevice.isBluetoothDock()) { 649 onBondingDockConnect(); 650 } else if (mDevice.isBondingInitiatedLocally()) { 651 connect(false); 652 } 653 } 654 } 655 getBtClass()656 public BluetoothClass getBtClass() { 657 return mDevice.getBluetoothClass(); 658 } 659 getProfiles()660 public List<LocalBluetoothProfile> getProfiles() { 661 return Collections.unmodifiableList(mProfiles); 662 } 663 getConnectableProfiles()664 public List<LocalBluetoothProfile> getConnectableProfiles() { 665 List<LocalBluetoothProfile> connectableProfiles = 666 new ArrayList<LocalBluetoothProfile>(); 667 synchronized (mProfileLock) { 668 for (LocalBluetoothProfile profile : mProfiles) { 669 if (profile.accessProfileEnabled()) { 670 connectableProfiles.add(profile); 671 } 672 } 673 } 674 return connectableProfiles; 675 } 676 getRemovedProfiles()677 public List<LocalBluetoothProfile> getRemovedProfiles() { 678 return mRemovedProfiles; 679 } 680 registerCallback(Callback callback)681 public void registerCallback(Callback callback) { 682 synchronized (mCallbacks) { 683 mCallbacks.add(callback); 684 } 685 } 686 unregisterCallback(Callback callback)687 public void unregisterCallback(Callback callback) { 688 synchronized (mCallbacks) { 689 mCallbacks.remove(callback); 690 } 691 } 692 dispatchAttributesChanged()693 void dispatchAttributesChanged() { 694 synchronized (mCallbacks) { 695 for (Callback callback : mCallbacks) { 696 callback.onDeviceAttributesChanged(); 697 } 698 } 699 } 700 701 @Override toString()702 public String toString() { 703 return mDevice.toString(); 704 } 705 706 @Override equals(Object o)707 public boolean equals(Object o) { 708 if ((o == null) || !(o instanceof CachedBluetoothDevice)) { 709 return false; 710 } 711 return mDevice.equals(((CachedBluetoothDevice) o).mDevice); 712 } 713 714 @Override hashCode()715 public int hashCode() { 716 return mDevice.getAddress().hashCode(); 717 } 718 719 // This comparison uses non-final fields so the sort order may change 720 // when device attributes change (such as bonding state). Settings 721 // will completely refresh the device list when this happens. compareTo(CachedBluetoothDevice another)722 public int compareTo(CachedBluetoothDevice another) { 723 // Connected above not connected 724 int comparison = (another.isConnected() ? 1 : 0) - (isConnected() ? 1 : 0); 725 if (comparison != 0) return comparison; 726 727 // Paired above not paired 728 comparison = (another.getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0) - 729 (getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0); 730 if (comparison != 0) return comparison; 731 732 // Just discovered above discovered in the past 733 comparison = (another.mJustDiscovered ? 1 : 0) - (mJustDiscovered ? 1 : 0); 734 if (comparison != 0) return comparison; 735 736 // Stronger signal above weaker signal 737 comparison = another.mRssi - mRssi; 738 if (comparison != 0) return comparison; 739 740 // Fallback on name 741 return getName().compareTo(another.getName()); 742 } 743 744 public interface Callback { onDeviceAttributesChanged()745 void onDeviceAttributesChanged(); 746 } 747 748 // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth 749 // app's shared preferences). migratePhonebookPermissionChoice()750 private void migratePhonebookPermissionChoice() { 751 SharedPreferences preferences = mContext.getSharedPreferences( 752 "bluetooth_phonebook_permission", Context.MODE_PRIVATE); 753 if (!preferences.contains(mDevice.getAddress())) { 754 return; 755 } 756 757 if (mDevice.getPhonebookAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) { 758 int oldPermission = 759 preferences.getInt(mDevice.getAddress(), BluetoothDevice.ACCESS_UNKNOWN); 760 if (oldPermission == BluetoothDevice.ACCESS_ALLOWED) { 761 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED); 762 } else if (oldPermission == BluetoothDevice.ACCESS_REJECTED) { 763 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED); 764 } 765 } 766 767 SharedPreferences.Editor editor = preferences.edit(); 768 editor.remove(mDevice.getAddress()); 769 editor.commit(); 770 } 771 772 // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth 773 // app's shared preferences). migrateMessagePermissionChoice()774 private void migrateMessagePermissionChoice() { 775 SharedPreferences preferences = mContext.getSharedPreferences( 776 "bluetooth_message_permission", Context.MODE_PRIVATE); 777 if (!preferences.contains(mDevice.getAddress())) { 778 return; 779 } 780 781 if (mDevice.getMessageAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) { 782 int oldPermission = 783 preferences.getInt(mDevice.getAddress(), BluetoothDevice.ACCESS_UNKNOWN); 784 if (oldPermission == BluetoothDevice.ACCESS_ALLOWED) { 785 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_ALLOWED); 786 } else if (oldPermission == BluetoothDevice.ACCESS_REJECTED) { 787 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_REJECTED); 788 } 789 } 790 791 SharedPreferences.Editor editor = preferences.edit(); 792 editor.remove(mDevice.getAddress()); 793 editor.commit(); 794 } 795 processPhonebookAccess()796 private void processPhonebookAccess() { 797 if (mDevice.getBondState() != BluetoothDevice.BOND_BONDED) return; 798 799 ParcelUuid[] uuids = mDevice.getUuids(); 800 if (BluetoothUuid.containsAnyUuid(uuids, PbapServerProfile.PBAB_CLIENT_UUIDS)) { 801 // The pairing dialog now warns of phone-book access for paired devices. 802 // No separate prompt is displayed after pairing. 803 if (mDevice.getPhonebookAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) { 804 if (mDevice.getBluetoothClass().getDeviceClass() 805 == BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE || 806 mDevice.getBluetoothClass().getDeviceClass() 807 == BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET) { 808 EventLog.writeEvent(0x534e4554, "138529441", -1, ""); 809 } 810 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED); 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