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