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