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 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 /** 106 * Describes the current device and profile for logging. 107 * 108 * @param profile Profile to describe 109 * @return Description of the device and profile 110 */ 111 private String describe(LocalBluetoothProfile profile) { 112 StringBuilder sb = new StringBuilder(); 113 sb.append("Address:").append(mDevice); 114 if (profile != null) { 115 sb.append(" Profile:").append(profile); 116 } 117 118 return sb.toString(); 119 } 120 121 void onProfileStateChanged(LocalBluetoothProfile profile, int newProfileState) { 122 if (Utils.D) { 123 Log.d(TAG, "onProfileStateChanged: profile " + profile + 124 " newProfileState " + newProfileState); 125 } 126 if (mLocalAdapter.getBluetoothState() == BluetoothAdapter.STATE_TURNING_OFF) 127 { 128 if (Utils.D) Log.d(TAG, " BT Turninig Off...Profile conn state change ignored..."); 129 return; 130 } 131 mProfileConnectionState.put(profile, newProfileState); 132 if (newProfileState == BluetoothProfile.STATE_CONNECTED) { 133 if (profile instanceof MapProfile) { 134 profile.setPreferred(mDevice, true); 135 } else if (!mProfiles.contains(profile)) { 136 mRemovedProfiles.remove(profile); 137 mProfiles.add(profile); 138 if (profile instanceof PanProfile && 139 ((PanProfile) profile).isLocalRoleNap(mDevice)) { 140 // Device doesn't support NAP, so remove PanProfile on disconnect 141 mLocalNapRoleConnected = true; 142 } 143 } 144 } else if (profile instanceof MapProfile && 145 newProfileState == BluetoothProfile.STATE_DISCONNECTED) { 146 profile.setPreferred(mDevice, false); 147 } else if (mLocalNapRoleConnected && profile instanceof PanProfile && 148 ((PanProfile) profile).isLocalRoleNap(mDevice) && 149 newProfileState == BluetoothProfile.STATE_DISCONNECTED) { 150 Log.d(TAG, "Removing PanProfile from device after NAP disconnect"); 151 mProfiles.remove(profile); 152 mRemovedProfiles.add(profile); 153 mLocalNapRoleConnected = false; 154 } 155 } 156 157 CachedBluetoothDevice(Context context, 158 LocalBluetoothAdapter adapter, 159 LocalBluetoothProfileManager profileManager, 160 BluetoothDevice device) { 161 mContext = context; 162 mLocalAdapter = adapter; 163 mProfileManager = profileManager; 164 mDevice = device; 165 mProfileConnectionState = new HashMap<LocalBluetoothProfile, Integer>(); 166 fillData(); 167 } 168 169 public void disconnect() { 170 for (LocalBluetoothProfile profile : mProfiles) { 171 disconnect(profile); 172 } 173 // Disconnect PBAP server in case its connected 174 // This is to ensure all the profiles are disconnected as some CK/Hs do not 175 // disconnect PBAP connection when HF connection is brought down 176 PbapServerProfile PbapProfile = mProfileManager.getPbapProfile(); 177 if (PbapProfile.getConnectionStatus(mDevice) == BluetoothProfile.STATE_CONNECTED) 178 { 179 PbapProfile.disconnect(mDevice); 180 } 181 } 182 183 public void disconnect(LocalBluetoothProfile profile) { 184 if (profile.disconnect(mDevice)) { 185 if (Utils.D) { 186 Log.d(TAG, "Command sent successfully:DISCONNECT " + describe(profile)); 187 } 188 } 189 } 190 191 public void connect(boolean connectAllProfiles) { 192 if (!ensurePaired()) { 193 return; 194 } 195 196 mConnectAttempted = SystemClock.elapsedRealtime(); 197 connectWithoutResettingTimer(connectAllProfiles); 198 } 199 200 void onBondingDockConnect() { 201 // Attempt to connect if UUIDs are available. Otherwise, 202 // we will connect when the ACTION_UUID intent arrives. 203 connect(false); 204 } 205 206 private void connectWithoutResettingTimer(boolean connectAllProfiles) { 207 // Try to initialize the profiles if they were not. 208 if (mProfiles.isEmpty()) { 209 // if mProfiles is empty, then do not invoke updateProfiles. This causes a race 210 // condition with carkits during pairing, wherein RemoteDevice.UUIDs have been updated 211 // from bluetooth stack but ACTION.uuid is not sent yet. 212 // Eventually ACTION.uuid will be received which shall trigger the connection of the 213 // various profiles 214 // If UUIDs are not available yet, connect will be happen 215 // upon arrival of the ACTION_UUID intent. 216 Log.d(TAG, "No profiles. Maybe we will connect later"); 217 return; 218 } 219 220 // Reset the only-show-one-error-dialog tracking variable 221 mIsConnectingErrorPossible = true; 222 223 int preferredProfiles = 0; 224 for (LocalBluetoothProfile profile : mProfiles) { 225 if (connectAllProfiles ? profile.isConnectable() : profile.isAutoConnectable()) { 226 if (profile.isPreferred(mDevice)) { 227 ++preferredProfiles; 228 connectInt(profile); 229 } 230 } 231 } 232 if (DEBUG) Log.d(TAG, "Preferred profiles = " + preferredProfiles); 233 234 if (preferredProfiles == 0) { 235 connectAutoConnectableProfiles(); 236 } 237 } 238 239 private void connectAutoConnectableProfiles() { 240 if (!ensurePaired()) { 241 return; 242 } 243 // Reset the only-show-one-error-dialog tracking variable 244 mIsConnectingErrorPossible = true; 245 246 for (LocalBluetoothProfile profile : mProfiles) { 247 if (profile.isAutoConnectable()) { 248 profile.setPreferred(mDevice, true); 249 connectInt(profile); 250 } 251 } 252 } 253 254 /** 255 * Connect this device to the specified profile. 256 * 257 * @param profile the profile to use with the remote device 258 */ 259 public void connectProfile(LocalBluetoothProfile profile) { 260 mConnectAttempted = SystemClock.elapsedRealtime(); 261 // Reset the only-show-one-error-dialog tracking variable 262 mIsConnectingErrorPossible = true; 263 connectInt(profile); 264 // Refresh the UI based on profile.connect() call 265 refresh(); 266 } 267 268 synchronized void connectInt(LocalBluetoothProfile profile) { 269 if (!ensurePaired()) { 270 return; 271 } 272 if (profile.connect(mDevice)) { 273 if (Utils.D) { 274 Log.d(TAG, "Command sent successfully:CONNECT " + describe(profile)); 275 } 276 return; 277 } 278 Log.i(TAG, "Failed to connect " + profile.toString() + " to " + mName); 279 } 280 281 private boolean ensurePaired() { 282 if (getBondState() == BluetoothDevice.BOND_NONE) { 283 startPairing(); 284 return false; 285 } else { 286 return true; 287 } 288 } 289 290 public boolean startPairing() { 291 // Pairing is unreliable while scanning, so cancel discovery 292 if (mLocalAdapter.isDiscovering()) { 293 mLocalAdapter.cancelDiscovery(); 294 } 295 296 if (!mDevice.createBond()) { 297 return false; 298 } 299 300 return true; 301 } 302 303 /** 304 * Return true if user initiated pairing on this device. The message text is 305 * slightly different for local vs. remote initiated pairing dialogs. 306 */ 307 boolean isUserInitiatedPairing() { 308 return mDevice.isBondingInitiatedLocally(); 309 } 310 311 public void unpair() { 312 int state = getBondState(); 313 314 if (state == BluetoothDevice.BOND_BONDING) { 315 mDevice.cancelBondProcess(); 316 } 317 318 if (state != BluetoothDevice.BOND_NONE) { 319 final BluetoothDevice dev = mDevice; 320 if (dev != null) { 321 final boolean successful = dev.removeBond(); 322 if (successful) { 323 if (Utils.D) { 324 Log.d(TAG, "Command sent successfully:REMOVE_BOND " + describe(null)); 325 } 326 } else if (Utils.V) { 327 Log.v(TAG, "Framework rejected command immediately:REMOVE_BOND " + 328 describe(null)); 329 } 330 } 331 } 332 } 333 334 public int getProfileConnectionState(LocalBluetoothProfile profile) { 335 if (mProfileConnectionState == null || 336 mProfileConnectionState.get(profile) == null) { 337 // If cache is empty make the binder call to get the state 338 int state = profile.getConnectionStatus(mDevice); 339 mProfileConnectionState.put(profile, state); 340 } 341 return mProfileConnectionState.get(profile); 342 } 343 344 public void clearProfileConnectionState () 345 { 346 if (Utils.D) { 347 Log.d(TAG," Clearing all connection state for dev:" + mDevice.getName()); 348 } 349 for (LocalBluetoothProfile profile :getProfiles()) { 350 mProfileConnectionState.put(profile, BluetoothProfile.STATE_DISCONNECTED); 351 } 352 } 353 354 // TODO: do any of these need to run async on a background thread? 355 private void fillData() { 356 fetchName(); 357 fetchBtClass(); 358 updateProfiles(); 359 migratePhonebookPermissionChoice(); 360 migrateMessagePermissionChoice(); 361 fetchMessageRejectionCount(); 362 363 mVisible = false; 364 dispatchAttributesChanged(); 365 } 366 367 public BluetoothDevice getDevice() { 368 return mDevice; 369 } 370 371 public String getName() { 372 return mName; 373 } 374 375 /** 376 * Populate name from BluetoothDevice.ACTION_FOUND intent 377 */ 378 void setNewName(String name) { 379 if (mName == null) { 380 mName = name; 381 if (mName == null || TextUtils.isEmpty(mName)) { 382 mName = mDevice.getAddress(); 383 } 384 dispatchAttributesChanged(); 385 } 386 } 387 388 /** 389 * user changes the device name 390 */ 391 public void setName(String name) { 392 if (!mName.equals(name)) { 393 mName = name; 394 mDevice.setAlias(name); 395 dispatchAttributesChanged(); 396 } 397 } 398 399 void refreshName() { 400 fetchName(); 401 dispatchAttributesChanged(); 402 } 403 404 private void fetchName() { 405 mName = mDevice.getAliasName(); 406 407 if (TextUtils.isEmpty(mName)) { 408 mName = mDevice.getAddress(); 409 if (DEBUG) Log.d(TAG, "Device has no name (yet), use address: " + mName); 410 } 411 } 412 413 void refresh() { 414 dispatchAttributesChanged(); 415 } 416 417 public boolean isVisible() { 418 return mVisible; 419 } 420 421 public void setVisible(boolean visible) { 422 if (mVisible != visible) { 423 mVisible = visible; 424 dispatchAttributesChanged(); 425 } 426 } 427 428 public int getBondState() { 429 return mDevice.getBondState(); 430 } 431 432 void setRssi(short rssi) { 433 if (mRssi != rssi) { 434 mRssi = rssi; 435 dispatchAttributesChanged(); 436 } 437 } 438 439 /** 440 * Checks whether we are connected to this device (any profile counts). 441 * 442 * @return Whether it is connected. 443 */ 444 public boolean isConnected() { 445 for (LocalBluetoothProfile profile : mProfiles) { 446 int status = getProfileConnectionState(profile); 447 if (status == BluetoothProfile.STATE_CONNECTED) { 448 return true; 449 } 450 } 451 452 return false; 453 } 454 455 public boolean isConnectedProfile(LocalBluetoothProfile profile) { 456 int status = getProfileConnectionState(profile); 457 return status == BluetoothProfile.STATE_CONNECTED; 458 459 } 460 461 public boolean isBusy() { 462 for (LocalBluetoothProfile profile : mProfiles) { 463 int status = getProfileConnectionState(profile); 464 if (status == BluetoothProfile.STATE_CONNECTING 465 || status == BluetoothProfile.STATE_DISCONNECTING) { 466 return true; 467 } 468 } 469 return getBondState() == BluetoothDevice.BOND_BONDING; 470 } 471 472 /** 473 * Fetches a new value for the cached BT class. 474 */ 475 private void fetchBtClass() { 476 mBtClass = mDevice.getBluetoothClass(); 477 } 478 479 private boolean updateProfiles() { 480 ParcelUuid[] uuids = mDevice.getUuids(); 481 if (uuids == null) return false; 482 483 ParcelUuid[] localUuids = mLocalAdapter.getUuids(); 484 if (localUuids == null) return false; 485 486 /** 487 * Now we know if the device supports PBAP, update permissions... 488 */ 489 processPhonebookAccess(); 490 491 mProfileManager.updateProfiles(uuids, localUuids, mProfiles, mRemovedProfiles, 492 mLocalNapRoleConnected, mDevice); 493 494 if (DEBUG) { 495 Log.e(TAG, "updating profiles for " + mDevice.getAliasName()); 496 BluetoothClass bluetoothClass = mDevice.getBluetoothClass(); 497 498 if (bluetoothClass != null) Log.v(TAG, "Class: " + bluetoothClass.toString()); 499 Log.v(TAG, "UUID:"); 500 for (ParcelUuid uuid : uuids) { 501 Log.v(TAG, " " + uuid); 502 } 503 } 504 return true; 505 } 506 507 /** 508 * Refreshes the UI for the BT class, including fetching the latest value 509 * for the class. 510 */ 511 void refreshBtClass() { 512 fetchBtClass(); 513 dispatchAttributesChanged(); 514 } 515 516 /** 517 * Refreshes the UI when framework alerts us of a UUID change. 518 */ 519 void onUuidChanged() { 520 updateProfiles(); 521 ParcelUuid[] uuids = mDevice.getUuids(); 522 523 long timeout = MAX_UUID_DELAY_FOR_AUTO_CONNECT; 524 if (BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.Hogp)) { 525 timeout = MAX_HOGP_DELAY_FOR_AUTO_CONNECT; 526 } 527 528 if (DEBUG) { 529 Log.d(TAG, "onUuidChanged: Time since last connect" 530 + (SystemClock.elapsedRealtime() - mConnectAttempted)); 531 } 532 533 /* 534 * If a connect was attempted earlier without any UUID, we will do the connect now. 535 * Otherwise, allow the connect on UUID change. 536 */ 537 if (!mProfiles.isEmpty() 538 && ((mConnectAttempted + timeout) > SystemClock.elapsedRealtime())) { 539 connectWithoutResettingTimer(false); 540 } 541 542 dispatchAttributesChanged(); 543 } 544 545 void onBondingStateChanged(int bondState) { 546 if (bondState == BluetoothDevice.BOND_NONE) { 547 mProfiles.clear(); 548 setPhonebookPermissionChoice(ACCESS_UNKNOWN); 549 setMessagePermissionChoice(ACCESS_UNKNOWN); 550 setSimPermissionChoice(ACCESS_UNKNOWN); 551 mMessageRejectionCount = 0; 552 saveMessageRejectionCount(); 553 } 554 555 refresh(); 556 557 if (bondState == BluetoothDevice.BOND_BONDED) { 558 if (mDevice.isBluetoothDock()) { 559 onBondingDockConnect(); 560 } else if (mDevice.isBondingInitiatedLocally()) { 561 connect(false); 562 } 563 } 564 } 565 566 void setBtClass(BluetoothClass btClass) { 567 if (btClass != null && mBtClass != btClass) { 568 mBtClass = btClass; 569 dispatchAttributesChanged(); 570 } 571 } 572 573 public BluetoothClass getBtClass() { 574 return mBtClass; 575 } 576 577 public List<LocalBluetoothProfile> getProfiles() { 578 return Collections.unmodifiableList(mProfiles); 579 } 580 581 public 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 public List<LocalBluetoothProfile> getRemovedProfiles() { 593 return mRemovedProfiles; 594 } 595 596 public void registerCallback(Callback callback) { 597 synchronized (mCallbacks) { 598 mCallbacks.add(callback); 599 } 600 } 601 602 public 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 public 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 public 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 public 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 public 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 public int getSimPermissionChoice() { 727 int permission = mDevice.getSimAccessPermission(); 728 if (permission == BluetoothDevice.ACCESS_ALLOWED) { 729 return ACCESS_ALLOWED; 730 } else if (permission == BluetoothDevice.ACCESS_REJECTED) { 731 return ACCESS_REJECTED; 732 } 733 return ACCESS_UNKNOWN; 734 } 735 736 void setSimPermissionChoice(int permissionChoice) { 737 int permission = BluetoothDevice.ACCESS_UNKNOWN; 738 if (permissionChoice == ACCESS_ALLOWED) { 739 permission = BluetoothDevice.ACCESS_ALLOWED; 740 } else if (permissionChoice == ACCESS_REJECTED) { 741 permission = BluetoothDevice.ACCESS_REJECTED; 742 } 743 mDevice.setSimAccessPermission(permission); 744 } 745 746 // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth 747 // app's shared preferences). 748 private void migrateMessagePermissionChoice() { 749 SharedPreferences preferences = mContext.getSharedPreferences( 750 "bluetooth_message_permission", Context.MODE_PRIVATE); 751 if (!preferences.contains(mDevice.getAddress())) { 752 return; 753 } 754 755 if (mDevice.getMessageAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) { 756 int oldPermission = preferences.getInt(mDevice.getAddress(), ACCESS_UNKNOWN); 757 if (oldPermission == ACCESS_ALLOWED) { 758 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_ALLOWED); 759 } else if (oldPermission == ACCESS_REJECTED) { 760 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_REJECTED); 761 } 762 } 763 764 SharedPreferences.Editor editor = preferences.edit(); 765 editor.remove(mDevice.getAddress()); 766 editor.commit(); 767 } 768 769 /** 770 * @return Whether this rejection should persist. 771 */ 772 public boolean checkAndIncreaseMessageRejectionCount() { 773 if (mMessageRejectionCount < MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST) { 774 mMessageRejectionCount++; 775 saveMessageRejectionCount(); 776 } 777 return mMessageRejectionCount >= MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST; 778 } 779 780 private void fetchMessageRejectionCount() { 781 SharedPreferences preference = mContext.getSharedPreferences( 782 MESSAGE_REJECTION_COUNT_PREFS_NAME, Context.MODE_PRIVATE); 783 mMessageRejectionCount = preference.getInt(mDevice.getAddress(), 0); 784 } 785 786 private void saveMessageRejectionCount() { 787 SharedPreferences.Editor editor = mContext.getSharedPreferences( 788 MESSAGE_REJECTION_COUNT_PREFS_NAME, Context.MODE_PRIVATE).edit(); 789 if (mMessageRejectionCount == 0) { 790 editor.remove(mDevice.getAddress()); 791 } else { 792 editor.putInt(mDevice.getAddress(), mMessageRejectionCount); 793 } 794 editor.commit(); 795 } 796 797 private void processPhonebookAccess() { 798 if (mDevice.getBondState() != BluetoothDevice.BOND_BONDED) return; 799 800 ParcelUuid[] uuids = mDevice.getUuids(); 801 if (BluetoothUuid.containsAnyUuid(uuids, PbapServerProfile.PBAB_CLIENT_UUIDS)) { 802 // The pairing dialog now warns of phone-book access for paired devices. 803 // No separate prompt is displayed after pairing. 804 if (getPhonebookPermissionChoice() == CachedBluetoothDevice.ACCESS_UNKNOWN) { 805 if (mDevice.getBluetoothClass().getDeviceClass() 806 == BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE || 807 mDevice.getBluetoothClass().getDeviceClass() 808 == BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET) { 809 setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_ALLOWED); 810 } else { 811 setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_REJECTED); 812 } 813 } 814 } 815 } 816 817 public int getMaxConnectionState() { 818 int maxState = BluetoothProfile.STATE_DISCONNECTED; 819 for (LocalBluetoothProfile profile : getProfiles()) { 820 int connectionStatus = getProfileConnectionState(profile); 821 if (connectionStatus > maxState) { 822 maxState = connectionStatus; 823 } 824 } 825 return maxState; 826 } 827 828 /** 829 * @return resource for string that discribes the connection state of this device. 830 */ 831 public int getConnectionSummary() { 832 boolean profileConnected = false; // at least one profile is connected 833 boolean a2dpNotConnected = false; // A2DP is preferred but not connected 834 boolean hfpNotConnected = false; // HFP is preferred but not connected 835 836 for (LocalBluetoothProfile profile : getProfiles()) { 837 int connectionStatus = getProfileConnectionState(profile); 838 839 switch (connectionStatus) { 840 case BluetoothProfile.STATE_CONNECTING: 841 case BluetoothProfile.STATE_DISCONNECTING: 842 return Utils.getConnectionStateSummary(connectionStatus); 843 844 case BluetoothProfile.STATE_CONNECTED: 845 profileConnected = true; 846 break; 847 848 case BluetoothProfile.STATE_DISCONNECTED: 849 if (profile.isProfileReady()) { 850 if ((profile instanceof A2dpProfile) || 851 (profile instanceof A2dpSinkProfile)){ 852 a2dpNotConnected = true; 853 } else if ((profile instanceof HeadsetProfile) || 854 (profile instanceof HfpClientProfile)) { 855 hfpNotConnected = true; 856 } 857 } 858 break; 859 } 860 } 861 862 if (profileConnected) { 863 if (a2dpNotConnected && hfpNotConnected) { 864 return R.string.bluetooth_connected_no_headset_no_a2dp; 865 } else if (a2dpNotConnected) { 866 return R.string.bluetooth_connected_no_a2dp; 867 } else if (hfpNotConnected) { 868 return R.string.bluetooth_connected_no_headset; 869 } else { 870 return R.string.bluetooth_connected; 871 } 872 } 873 874 return getBondState() == BluetoothDevice.BOND_BONDING ? R.string.bluetooth_pairing : 0; 875 } 876 } 877