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