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