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 17package com.android.settings.bluetooth; 18 19import android.bluetooth.BluetoothClass; 20import android.bluetooth.BluetoothDevice; 21import android.bluetooth.BluetoothProfile; 22import android.content.Context; 23import android.content.SharedPreferences; 24import android.os.ParcelUuid; 25import android.os.SystemClock; 26import android.text.TextUtils; 27import android.util.Log; 28import android.bluetooth.BluetoothAdapter; 29 30import java.util.ArrayList; 31import java.util.Collection; 32import java.util.Collections; 33import java.util.HashMap; 34import java.util.List; 35 36/** 37 * CachedBluetoothDevice represents a remote Bluetooth device. It contains 38 * attributes of the device (such as the address, name, RSSI, etc.) and 39 * functionality that can be performed on the device (connect, pair, disconnect, 40 * etc.). 41 */ 42final class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> { 43 private static final String TAG = "CachedBluetoothDevice"; 44 private static final boolean DEBUG = Utils.V; 45 46 private final Context mContext; 47 private final LocalBluetoothAdapter mLocalAdapter; 48 private final LocalBluetoothProfileManager mProfileManager; 49 private final BluetoothDevice mDevice; 50 private String mName; 51 private short mRssi; 52 private BluetoothClass mBtClass; 53 private HashMap<LocalBluetoothProfile, Integer> mProfileConnectionState; 54 55 private final List<LocalBluetoothProfile> mProfiles = 56 new ArrayList<LocalBluetoothProfile>(); 57 58 // List of profiles that were previously in mProfiles, but have been removed 59 private final List<LocalBluetoothProfile> mRemovedProfiles = 60 new ArrayList<LocalBluetoothProfile>(); 61 62 // Device supports PANU but not NAP: remove PanProfile after device disconnects from NAP 63 private boolean mLocalNapRoleConnected; 64 65 private boolean mVisible; 66 67 private int mPhonebookPermissionChoice; 68 69 private int mMessagePermissionChoice; 70 71 private int mMessageRejectionCount; 72 73 private final Collection<Callback> mCallbacks = new ArrayList<Callback>(); 74 75 // Following constants indicate the user's choices of Phone book/message access settings 76 // User hasn't made any choice or settings app has wiped out the memory 77 public final static int ACCESS_UNKNOWN = 0; 78 // User has accepted the connection and let Settings app remember the decision 79 public final static int ACCESS_ALLOWED = 1; 80 // User has rejected the connection and let Settings app remember the decision 81 public final static int ACCESS_REJECTED = 2; 82 83 // How many times user should reject the connection to make the choice persist. 84 private final static int MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST = 2; 85 86 private final static String MESSAGE_REJECTION_COUNT_PREFS_NAME = "bluetooth_message_reject"; 87 88 /** 89 * When we connect to multiple profiles, we only want to display a single 90 * error even if they all fail. This tracks that state. 91 */ 92 private boolean mIsConnectingErrorPossible; 93 94 /** 95 * Last time a bt profile auto-connect was attempted. 96 * If an ACTION_UUID intent comes in within 97 * MAX_UUID_DELAY_FOR_AUTO_CONNECT milliseconds, we will try auto-connect 98 * again with the new UUIDs 99 */ 100 private long mConnectAttempted; 101 102 // See mConnectAttempted 103 private static final long MAX_UUID_DELAY_FOR_AUTO_CONNECT = 5000; 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 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 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 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 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 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 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 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 BluetoothDevice getDevice() { 372 return mDevice; 373 } 374 375 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 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 boolean isVisible() { 422 return mVisible; 423 } 424 425 void setVisible(boolean visible) { 426 if (mVisible != visible) { 427 mVisible = visible; 428 dispatchAttributesChanged(); 429 } 430 } 431 432 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 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 boolean isConnectedProfile(LocalBluetoothProfile profile) { 460 int status = getProfileConnectionState(profile); 461 return status == BluetoothProfile.STATE_CONNECTED; 462 463 } 464 465 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 mProfileManager.updateProfiles(uuids, localUuids, mProfiles, mRemovedProfiles, 491 mLocalNapRoleConnected, mDevice); 492 493 if (DEBUG) { 494 Log.e(TAG, "updating profiles for " + mDevice.getAliasName()); 495 BluetoothClass bluetoothClass = mDevice.getBluetoothClass(); 496 497 if (bluetoothClass != null) Log.v(TAG, "Class: " + bluetoothClass.toString()); 498 Log.v(TAG, "UUID:"); 499 for (ParcelUuid uuid : uuids) { 500 Log.v(TAG, " " + uuid); 501 } 502 } 503 return true; 504 } 505 506 /** 507 * Refreshes the UI for the BT class, including fetching the latest value 508 * for the class. 509 */ 510 void refreshBtClass() { 511 fetchBtClass(); 512 dispatchAttributesChanged(); 513 } 514 515 /** 516 * Refreshes the UI when framework alerts us of a UUID change. 517 */ 518 void onUuidChanged() { 519 updateProfiles(); 520 521 if (DEBUG) { 522 Log.e(TAG, "onUuidChanged: Time since last connect" 523 + (SystemClock.elapsedRealtime() - mConnectAttempted)); 524 } 525 526 /* 527 * If a connect was attempted earlier without any UUID, we will do the 528 * connect now. 529 */ 530 if (!mProfiles.isEmpty() 531 && (mConnectAttempted + MAX_UUID_DELAY_FOR_AUTO_CONNECT) > SystemClock 532 .elapsedRealtime()) { 533 connectWithoutResettingTimer(false); 534 } 535 dispatchAttributesChanged(); 536 } 537 538 void onBondingStateChanged(int bondState) { 539 if (bondState == BluetoothDevice.BOND_NONE) { 540 mProfiles.clear(); 541 mConnectAfterPairing = false; // cancel auto-connect 542 setPhonebookPermissionChoice(ACCESS_UNKNOWN); 543 setMessagePermissionChoice(ACCESS_UNKNOWN); 544 mMessageRejectionCount = 0; 545 saveMessageRejectionCount(); 546 } 547 548 refresh(); 549 550 if (bondState == BluetoothDevice.BOND_BONDED) { 551 if (mDevice.isBluetoothDock()) { 552 onBondingDockConnect(); 553 } else if (mConnectAfterPairing) { 554 connect(false); 555 } 556 mConnectAfterPairing = false; 557 } 558 } 559 560 void setBtClass(BluetoothClass btClass) { 561 if (btClass != null && mBtClass != btClass) { 562 mBtClass = btClass; 563 dispatchAttributesChanged(); 564 } 565 } 566 567 BluetoothClass getBtClass() { 568 return mBtClass; 569 } 570 571 List<LocalBluetoothProfile> getProfiles() { 572 return Collections.unmodifiableList(mProfiles); 573 } 574 575 List<LocalBluetoothProfile> getConnectableProfiles() { 576 List<LocalBluetoothProfile> connectableProfiles = 577 new ArrayList<LocalBluetoothProfile>(); 578 for (LocalBluetoothProfile profile : mProfiles) { 579 if (profile.isConnectable()) { 580 connectableProfiles.add(profile); 581 } 582 } 583 return connectableProfiles; 584 } 585 586 List<LocalBluetoothProfile> getRemovedProfiles() { 587 return mRemovedProfiles; 588 } 589 590 void registerCallback(Callback callback) { 591 synchronized (mCallbacks) { 592 mCallbacks.add(callback); 593 } 594 } 595 596 void unregisterCallback(Callback callback) { 597 synchronized (mCallbacks) { 598 mCallbacks.remove(callback); 599 } 600 } 601 602 private void dispatchAttributesChanged() { 603 synchronized (mCallbacks) { 604 for (Callback callback : mCallbacks) { 605 callback.onDeviceAttributesChanged(); 606 } 607 } 608 } 609 610 @Override 611 public String toString() { 612 return mDevice.toString(); 613 } 614 615 @Override 616 public boolean equals(Object o) { 617 if ((o == null) || !(o instanceof CachedBluetoothDevice)) { 618 return false; 619 } 620 return mDevice.equals(((CachedBluetoothDevice) o).mDevice); 621 } 622 623 @Override 624 public int hashCode() { 625 return mDevice.getAddress().hashCode(); 626 } 627 628 // This comparison uses non-final fields so the sort order may change 629 // when device attributes change (such as bonding state). Settings 630 // will completely refresh the device list when this happens. 631 public int compareTo(CachedBluetoothDevice another) { 632 // Connected above not connected 633 int comparison = (another.isConnected() ? 1 : 0) - (isConnected() ? 1 : 0); 634 if (comparison != 0) return comparison; 635 636 // Paired above not paired 637 comparison = (another.getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0) - 638 (getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0); 639 if (comparison != 0) return comparison; 640 641 // Visible above not visible 642 comparison = (another.mVisible ? 1 : 0) - (mVisible ? 1 : 0); 643 if (comparison != 0) return comparison; 644 645 // Stronger signal above weaker signal 646 comparison = another.mRssi - mRssi; 647 if (comparison != 0) return comparison; 648 649 // Fallback on name 650 return mName.compareTo(another.mName); 651 } 652 653 public interface Callback { 654 void onDeviceAttributesChanged(); 655 } 656 657 int getPhonebookPermissionChoice() { 658 int permission = mDevice.getPhonebookAccessPermission(); 659 if (permission == BluetoothDevice.ACCESS_ALLOWED) { 660 return ACCESS_ALLOWED; 661 } else if (permission == BluetoothDevice.ACCESS_REJECTED) { 662 return ACCESS_REJECTED; 663 } 664 return ACCESS_UNKNOWN; 665 } 666 667 void setPhonebookPermissionChoice(int permissionChoice) { 668 int permission = BluetoothDevice.ACCESS_UNKNOWN; 669 if (permissionChoice == ACCESS_ALLOWED) { 670 permission = BluetoothDevice.ACCESS_ALLOWED; 671 } else if (permissionChoice == ACCESS_REJECTED) { 672 permission = BluetoothDevice.ACCESS_REJECTED; 673 } 674 mDevice.setPhonebookAccessPermission(permission); 675 } 676 677 // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth 678 // app's shared preferences). 679 private void migratePhonebookPermissionChoice() { 680 SharedPreferences preferences = mContext.getSharedPreferences( 681 "bluetooth_phonebook_permission", Context.MODE_PRIVATE); 682 if (!preferences.contains(mDevice.getAddress())) { 683 return; 684 } 685 686 if (mDevice.getPhonebookAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) { 687 int oldPermission = preferences.getInt(mDevice.getAddress(), ACCESS_UNKNOWN); 688 if (oldPermission == ACCESS_ALLOWED) { 689 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED); 690 } else if (oldPermission == ACCESS_REJECTED) { 691 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED); 692 } 693 } 694 695 SharedPreferences.Editor editor = preferences.edit(); 696 editor.remove(mDevice.getAddress()); 697 editor.commit(); 698 } 699 700 int getMessagePermissionChoice() { 701 int permission = mDevice.getMessageAccessPermission(); 702 if (permission == BluetoothDevice.ACCESS_ALLOWED) { 703 return ACCESS_ALLOWED; 704 } else if (permission == BluetoothDevice.ACCESS_REJECTED) { 705 return ACCESS_REJECTED; 706 } 707 return ACCESS_UNKNOWN; 708 } 709 710 void setMessagePermissionChoice(int permissionChoice) { 711 int permission = BluetoothDevice.ACCESS_UNKNOWN; 712 if (permissionChoice == ACCESS_ALLOWED) { 713 permission = BluetoothDevice.ACCESS_ALLOWED; 714 } else if (permissionChoice == ACCESS_REJECTED) { 715 permission = BluetoothDevice.ACCESS_REJECTED; 716 } 717 mDevice.setMessageAccessPermission(permission); 718 } 719 720 // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth 721 // app's shared preferences). 722 private void migrateMessagePermissionChoice() { 723 SharedPreferences preferences = mContext.getSharedPreferences( 724 "bluetooth_message_permission", Context.MODE_PRIVATE); 725 if (!preferences.contains(mDevice.getAddress())) { 726 return; 727 } 728 729 if (mDevice.getMessageAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) { 730 int oldPermission = preferences.getInt(mDevice.getAddress(), ACCESS_UNKNOWN); 731 if (oldPermission == ACCESS_ALLOWED) { 732 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_ALLOWED); 733 } else if (oldPermission == ACCESS_REJECTED) { 734 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_REJECTED); 735 } 736 } 737 738 SharedPreferences.Editor editor = preferences.edit(); 739 editor.remove(mDevice.getAddress()); 740 editor.commit(); 741 } 742 743 /** 744 * @return Whether this rejection should persist. 745 */ 746 boolean checkAndIncreaseMessageRejectionCount() { 747 if (mMessageRejectionCount < MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST) { 748 mMessageRejectionCount++; 749 saveMessageRejectionCount(); 750 } 751 return mMessageRejectionCount >= MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST; 752 } 753 754 private void fetchMessageRejectionCount() { 755 SharedPreferences preference = mContext.getSharedPreferences( 756 MESSAGE_REJECTION_COUNT_PREFS_NAME, Context.MODE_PRIVATE); 757 mMessageRejectionCount = preference.getInt(mDevice.getAddress(), 0); 758 } 759 760 private void saveMessageRejectionCount() { 761 SharedPreferences.Editor editor = mContext.getSharedPreferences( 762 MESSAGE_REJECTION_COUNT_PREFS_NAME, Context.MODE_PRIVATE).edit(); 763 if (mMessageRejectionCount == 0) { 764 editor.remove(mDevice.getAddress()); 765 } else { 766 editor.putInt(mDevice.getAddress(), mMessageRejectionCount); 767 } 768 editor.commit(); 769 } 770} 771