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