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