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.settings.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 java.util.ArrayList; 32 import java.util.Collection; 33 import java.util.Collections; 34 import java.util.HashMap; 35 import java.util.List; 36 37 /** 38 * CachedBluetoothDevice represents a remote Bluetooth device. It contains 39 * attributes of the device (such as the address, name, RSSI, etc.) and 40 * functionality that can be performed on the device (connect, pair, disconnect, 41 * etc.). 42 */ 43 final class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> { 44 private static final String TAG = "CachedBluetoothDevice"; 45 private static final boolean DEBUG = Utils.V; 46 47 private final Context mContext; 48 private final LocalBluetoothAdapter mLocalAdapter; 49 private final LocalBluetoothProfileManager mProfileManager; 50 private final BluetoothDevice mDevice; 51 private String mName; 52 private short mRssi; 53 private BluetoothClass mBtClass; 54 private HashMap<LocalBluetoothProfile, Integer> mProfileConnectionState; 55 56 private final List<LocalBluetoothProfile> mProfiles = 57 new ArrayList<LocalBluetoothProfile>(); 58 59 // List of profiles that were previously in mProfiles, but have been removed 60 private final List<LocalBluetoothProfile> mRemovedProfiles = 61 new ArrayList<LocalBluetoothProfile>(); 62 63 // Device supports PANU but not NAP: remove PanProfile after device disconnects from NAP 64 private boolean mLocalNapRoleConnected; 65 66 private boolean mVisible; 67 68 private int mPhonebookPermissionChoice; 69 70 private int mMessagePermissionChoice; 71 72 private int mMessageRejectionCount; 73 74 private final Collection<Callback> mCallbacks = new ArrayList<Callback>(); 75 76 // Following constants indicate the user's choices of Phone book/message access settings 77 // User hasn't made any choice or settings app has wiped out the memory 78 public final static int ACCESS_UNKNOWN = 0; 79 // User has accepted the connection and let Settings app remember the decision 80 public final static int ACCESS_ALLOWED = 1; 81 // User has rejected the connection and let Settings app remember the decision 82 public final static int ACCESS_REJECTED = 2; 83 84 // How many times user should reject the connection to make the choice persist. 85 private final static int MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST = 2; 86 87 private final static String MESSAGE_REJECTION_COUNT_PREFS_NAME = "bluetooth_message_reject"; 88 89 /** 90 * When we connect to multiple profiles, we only want to display a single 91 * error even if they all fail. This tracks that state. 92 */ 93 private boolean mIsConnectingErrorPossible; 94 95 /** 96 * Last time a bt profile auto-connect was attempted. 97 * If an ACTION_UUID intent comes in within 98 * MAX_UUID_DELAY_FOR_AUTO_CONNECT milliseconds, we will try auto-connect 99 * again with the new UUIDs 100 */ 101 private long mConnectAttempted; 102 103 // See mConnectAttempted 104 private static final long MAX_UUID_DELAY_FOR_AUTO_CONNECT = 5000; 105 106 /** Auto-connect after pairing only if locally initiated. */ 107 private boolean mConnectAfterPairing; 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 */ 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 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 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 173 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 187 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 195 void connect(boolean connectAllProfiles) { 196 if (!ensurePaired()) { 197 return; 198 } 199 200 mConnectAttempted = SystemClock.elapsedRealtime(); 201 connectWithoutResettingTimer(connectAllProfiles); 202 } 203 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 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 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 */ 263 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 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 285 private boolean ensurePaired() { 286 if (getBondState() == BluetoothDevice.BOND_NONE) { 287 startPairing(); 288 return false; 289 } else { 290 return true; 291 } 292 } 293 294 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 mConnectAfterPairing = true; // auto-connect after pairing 305 return true; 306 } 307 308 /** 309 * Return true if user initiated pairing on this device. The message text is 310 * slightly different for local vs. remote initiated pairing dialogs. 311 */ 312 boolean isUserInitiatedPairing() { 313 return mConnectAfterPairing; 314 } 315 316 void unpair() { 317 int state = getBondState(); 318 319 if (state == BluetoothDevice.BOND_BONDING) { 320 mDevice.cancelBondProcess(); 321 } 322 323 if (state != BluetoothDevice.BOND_NONE) { 324 final BluetoothDevice dev = mDevice; 325 if (dev != null) { 326 final boolean successful = dev.removeBond(); 327 if (successful) { 328 if (Utils.D) { 329 Log.d(TAG, "Command sent successfully:REMOVE_BOND " + describe(null)); 330 } 331 } else if (Utils.V) { 332 Log.v(TAG, "Framework rejected command immediately:REMOVE_BOND " + 333 describe(null)); 334 } 335 } 336 } 337 } 338 339 int getProfileConnectionState(LocalBluetoothProfile profile) { 340 if (mProfileConnectionState == null || 341 mProfileConnectionState.get(profile) == null) { 342 // If cache is empty make the binder call to get the state 343 int state = profile.getConnectionStatus(mDevice); 344 mProfileConnectionState.put(profile, state); 345 } 346 return mProfileConnectionState.get(profile); 347 } 348 349 public void clearProfileConnectionState () 350 { 351 if (Utils.D) { 352 Log.d(TAG," Clearing all connection state for dev:" + mDevice.getName()); 353 } 354 for (LocalBluetoothProfile profile :getProfiles()) { 355 mProfileConnectionState.put(profile, BluetoothProfile.STATE_DISCONNECTED); 356 } 357 } 358 359 // TODO: do any of these need to run async on a background thread? 360 private void fillData() { 361 fetchName(); 362 fetchBtClass(); 363 updateProfiles(); 364 migratePhonebookPermissionChoice(); 365 migrateMessagePermissionChoice(); 366 fetchMessageRejectionCount(); 367 368 mVisible = false; 369 dispatchAttributesChanged(); 370 } 371 372 BluetoothDevice getDevice() { 373 return mDevice; 374 } 375 376 String getName() { 377 return mName; 378 } 379 380 /** 381 * Populate name from BluetoothDevice.ACTION_FOUND intent 382 */ 383 void setNewName(String name) { 384 if (mName == null) { 385 mName = name; 386 if (mName == null || TextUtils.isEmpty(mName)) { 387 mName = mDevice.getAddress(); 388 } 389 dispatchAttributesChanged(); 390 } 391 } 392 393 /** 394 * user changes the device name 395 */ 396 void setName(String name) { 397 if (!mName.equals(name)) { 398 mName = name; 399 mDevice.setAlias(name); 400 dispatchAttributesChanged(); 401 } 402 } 403 404 void refreshName() { 405 fetchName(); 406 dispatchAttributesChanged(); 407 } 408 409 private void fetchName() { 410 mName = mDevice.getAliasName(); 411 412 if (TextUtils.isEmpty(mName)) { 413 mName = mDevice.getAddress(); 414 if (DEBUG) Log.d(TAG, "Device has no name (yet), use address: " + mName); 415 } 416 } 417 418 void refresh() { 419 dispatchAttributesChanged(); 420 } 421 422 boolean isVisible() { 423 return mVisible; 424 } 425 426 void setVisible(boolean visible) { 427 if (mVisible != visible) { 428 mVisible = visible; 429 dispatchAttributesChanged(); 430 } 431 } 432 433 int getBondState() { 434 return mDevice.getBondState(); 435 } 436 437 void setRssi(short rssi) { 438 if (mRssi != rssi) { 439 mRssi = rssi; 440 dispatchAttributesChanged(); 441 } 442 } 443 444 /** 445 * Checks whether we are connected to this device (any profile counts). 446 * 447 * @return Whether it is connected. 448 */ 449 boolean isConnected() { 450 for (LocalBluetoothProfile profile : mProfiles) { 451 int status = getProfileConnectionState(profile); 452 if (status == BluetoothProfile.STATE_CONNECTED) { 453 return true; 454 } 455 } 456 457 return false; 458 } 459 460 boolean isConnectedProfile(LocalBluetoothProfile profile) { 461 int status = getProfileConnectionState(profile); 462 return status == BluetoothProfile.STATE_CONNECTED; 463 464 } 465 466 boolean isBusy() { 467 for (LocalBluetoothProfile profile : mProfiles) { 468 int status = getProfileConnectionState(profile); 469 if (status == BluetoothProfile.STATE_CONNECTING 470 || status == BluetoothProfile.STATE_DISCONNECTING) { 471 return true; 472 } 473 } 474 return getBondState() == BluetoothDevice.BOND_BONDING; 475 } 476 477 /** 478 * Fetches a new value for the cached BT class. 479 */ 480 private void fetchBtClass() { 481 mBtClass = mDevice.getBluetoothClass(); 482 } 483 484 private boolean updateProfiles() { 485 ParcelUuid[] uuids = mDevice.getUuids(); 486 if (uuids == null) return false; 487 488 ParcelUuid[] localUuids = mLocalAdapter.getUuids(); 489 if (localUuids == null) return false; 490 491 /** 492 * Now we know if the device supports PBAP, update permissions... 493 */ 494 processPhonebookAccess(); 495 496 mProfileManager.updateProfiles(uuids, localUuids, mProfiles, mRemovedProfiles, 497 mLocalNapRoleConnected, mDevice); 498 499 if (DEBUG) { 500 Log.e(TAG, "updating profiles for " + mDevice.getAliasName()); 501 BluetoothClass bluetoothClass = mDevice.getBluetoothClass(); 502 503 if (bluetoothClass != null) Log.v(TAG, "Class: " + bluetoothClass.toString()); 504 Log.v(TAG, "UUID:"); 505 for (ParcelUuid uuid : uuids) { 506 Log.v(TAG, " " + uuid); 507 } 508 } 509 return true; 510 } 511 512 /** 513 * Refreshes the UI for the BT class, including fetching the latest value 514 * for the class. 515 */ 516 void refreshBtClass() { 517 fetchBtClass(); 518 dispatchAttributesChanged(); 519 } 520 521 /** 522 * Refreshes the UI when framework alerts us of a UUID change. 523 */ 524 void onUuidChanged() { 525 updateProfiles(); 526 527 if (DEBUG) { 528 Log.e(TAG, "onUuidChanged: Time since last connect" 529 + (SystemClock.elapsedRealtime() - mConnectAttempted)); 530 } 531 532 /* 533 * If a connect was attempted earlier without any UUID, we will do the 534 * connect now. 535 */ 536 if (!mProfiles.isEmpty() 537 && (mConnectAttempted + MAX_UUID_DELAY_FOR_AUTO_CONNECT) > SystemClock 538 .elapsedRealtime()) { 539 connectWithoutResettingTimer(false); 540 } 541 dispatchAttributesChanged(); 542 } 543 544 void onBondingStateChanged(int bondState) { 545 if (bondState == BluetoothDevice.BOND_NONE) { 546 mProfiles.clear(); 547 mConnectAfterPairing = false; // cancel auto-connect 548 setPhonebookPermissionChoice(ACCESS_UNKNOWN); 549 setMessagePermissionChoice(ACCESS_UNKNOWN); 550 mMessageRejectionCount = 0; 551 saveMessageRejectionCount(); 552 } 553 554 refresh(); 555 556 if (bondState == BluetoothDevice.BOND_BONDED) { 557 if (mDevice.isBluetoothDock()) { 558 onBondingDockConnect(); 559 } else if (mConnectAfterPairing) { 560 connect(false); 561 } 562 mConnectAfterPairing = false; 563 } 564 } 565 566 void setBtClass(BluetoothClass btClass) { 567 if (btClass != null && mBtClass != btClass) { 568 mBtClass = btClass; 569 dispatchAttributesChanged(); 570 } 571 } 572 573 BluetoothClass getBtClass() { 574 return mBtClass; 575 } 576 577 List<LocalBluetoothProfile> getProfiles() { 578 return Collections.unmodifiableList(mProfiles); 579 } 580 581 List<LocalBluetoothProfile> getConnectableProfiles() { 582 List<LocalBluetoothProfile> connectableProfiles = 583 new ArrayList<LocalBluetoothProfile>(); 584 for (LocalBluetoothProfile profile : mProfiles) { 585 if (profile.isConnectable()) { 586 connectableProfiles.add(profile); 587 } 588 } 589 return connectableProfiles; 590 } 591 592 List<LocalBluetoothProfile> getRemovedProfiles() { 593 return mRemovedProfiles; 594 } 595 596 void registerCallback(Callback callback) { 597 synchronized (mCallbacks) { 598 mCallbacks.add(callback); 599 } 600 } 601 602 void unregisterCallback(Callback callback) { 603 synchronized (mCallbacks) { 604 mCallbacks.remove(callback); 605 } 606 } 607 608 private void dispatchAttributesChanged() { 609 synchronized (mCallbacks) { 610 for (Callback callback : mCallbacks) { 611 callback.onDeviceAttributesChanged(); 612 } 613 } 614 } 615 616 @Override 617 public String toString() { 618 return mDevice.toString(); 619 } 620 621 @Override 622 public boolean equals(Object o) { 623 if ((o == null) || !(o instanceof CachedBluetoothDevice)) { 624 return false; 625 } 626 return mDevice.equals(((CachedBluetoothDevice) o).mDevice); 627 } 628 629 @Override 630 public int hashCode() { 631 return mDevice.getAddress().hashCode(); 632 } 633 634 // This comparison uses non-final fields so the sort order may change 635 // when device attributes change (such as bonding state). Settings 636 // will completely refresh the device list when this happens. 637 public int compareTo(CachedBluetoothDevice another) { 638 // Connected above not connected 639 int comparison = (another.isConnected() ? 1 : 0) - (isConnected() ? 1 : 0); 640 if (comparison != 0) return comparison; 641 642 // Paired above not paired 643 comparison = (another.getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0) - 644 (getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0); 645 if (comparison != 0) return comparison; 646 647 // Visible above not visible 648 comparison = (another.mVisible ? 1 : 0) - (mVisible ? 1 : 0); 649 if (comparison != 0) return comparison; 650 651 // Stronger signal above weaker signal 652 comparison = another.mRssi - mRssi; 653 if (comparison != 0) return comparison; 654 655 // Fallback on name 656 return mName.compareTo(another.mName); 657 } 658 659 public interface Callback { 660 void onDeviceAttributesChanged(); 661 } 662 663 int getPhonebookPermissionChoice() { 664 int permission = mDevice.getPhonebookAccessPermission(); 665 if (permission == BluetoothDevice.ACCESS_ALLOWED) { 666 return ACCESS_ALLOWED; 667 } else if (permission == BluetoothDevice.ACCESS_REJECTED) { 668 return ACCESS_REJECTED; 669 } 670 return ACCESS_UNKNOWN; 671 } 672 673 void setPhonebookPermissionChoice(int permissionChoice) { 674 int permission = BluetoothDevice.ACCESS_UNKNOWN; 675 if (permissionChoice == ACCESS_ALLOWED) { 676 permission = BluetoothDevice.ACCESS_ALLOWED; 677 } else if (permissionChoice == ACCESS_REJECTED) { 678 permission = BluetoothDevice.ACCESS_REJECTED; 679 } 680 mDevice.setPhonebookAccessPermission(permission); 681 } 682 683 // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth 684 // app's shared preferences). 685 private void migratePhonebookPermissionChoice() { 686 SharedPreferences preferences = mContext.getSharedPreferences( 687 "bluetooth_phonebook_permission", Context.MODE_PRIVATE); 688 if (!preferences.contains(mDevice.getAddress())) { 689 return; 690 } 691 692 if (mDevice.getPhonebookAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) { 693 int oldPermission = preferences.getInt(mDevice.getAddress(), ACCESS_UNKNOWN); 694 if (oldPermission == ACCESS_ALLOWED) { 695 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED); 696 } else if (oldPermission == ACCESS_REJECTED) { 697 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED); 698 } 699 } 700 701 SharedPreferences.Editor editor = preferences.edit(); 702 editor.remove(mDevice.getAddress()); 703 editor.commit(); 704 } 705 706 int getMessagePermissionChoice() { 707 int permission = mDevice.getMessageAccessPermission(); 708 if (permission == BluetoothDevice.ACCESS_ALLOWED) { 709 return ACCESS_ALLOWED; 710 } else if (permission == BluetoothDevice.ACCESS_REJECTED) { 711 return ACCESS_REJECTED; 712 } 713 return ACCESS_UNKNOWN; 714 } 715 716 void setMessagePermissionChoice(int permissionChoice) { 717 int permission = BluetoothDevice.ACCESS_UNKNOWN; 718 if (permissionChoice == ACCESS_ALLOWED) { 719 permission = BluetoothDevice.ACCESS_ALLOWED; 720 } else if (permissionChoice == ACCESS_REJECTED) { 721 permission = BluetoothDevice.ACCESS_REJECTED; 722 } 723 mDevice.setMessageAccessPermission(permission); 724 } 725 726 // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth 727 // app's shared preferences). 728 private void migrateMessagePermissionChoice() { 729 SharedPreferences preferences = mContext.getSharedPreferences( 730 "bluetooth_message_permission", Context.MODE_PRIVATE); 731 if (!preferences.contains(mDevice.getAddress())) { 732 return; 733 } 734 735 if (mDevice.getMessageAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) { 736 int oldPermission = preferences.getInt(mDevice.getAddress(), ACCESS_UNKNOWN); 737 if (oldPermission == ACCESS_ALLOWED) { 738 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_ALLOWED); 739 } else if (oldPermission == ACCESS_REJECTED) { 740 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_REJECTED); 741 } 742 } 743 744 SharedPreferences.Editor editor = preferences.edit(); 745 editor.remove(mDevice.getAddress()); 746 editor.commit(); 747 } 748 749 /** 750 * @return Whether this rejection should persist. 751 */ 752 boolean checkAndIncreaseMessageRejectionCount() { 753 if (mMessageRejectionCount < MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST) { 754 mMessageRejectionCount++; 755 saveMessageRejectionCount(); 756 } 757 return mMessageRejectionCount >= MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST; 758 } 759 760 private void fetchMessageRejectionCount() { 761 SharedPreferences preference = mContext.getSharedPreferences( 762 MESSAGE_REJECTION_COUNT_PREFS_NAME, Context.MODE_PRIVATE); 763 mMessageRejectionCount = preference.getInt(mDevice.getAddress(), 0); 764 } 765 766 private void saveMessageRejectionCount() { 767 SharedPreferences.Editor editor = mContext.getSharedPreferences( 768 MESSAGE_REJECTION_COUNT_PREFS_NAME, Context.MODE_PRIVATE).edit(); 769 if (mMessageRejectionCount == 0) { 770 editor.remove(mDevice.getAddress()); 771 } else { 772 editor.putInt(mDevice.getAddress(), mMessageRejectionCount); 773 } 774 editor.commit(); 775 } 776 777 private void processPhonebookAccess() { 778 if (mDevice.getBondState() != BluetoothDevice.BOND_BONDED) return; 779 780 ParcelUuid[] uuids = mDevice.getUuids(); 781 if (BluetoothUuid.containsAnyUuid(uuids, PbapServerProfile.PBAB_CLIENT_UUIDS)) { 782 // The pairing dialog now warns of phone-book access for paired devices. 783 // No separate prompt is displayed after pairing. 784 setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_ALLOWED); 785 } 786 } 787 } 788