Home | History | Annotate | Download | only in bluetooth
      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                 || (mConnectAttempted == 0))) {
    544             connectWithoutResettingTimer(false);
    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                 setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_ALLOWED);
    812             }
    813         }
    814     }
    815 
    816     public int getMaxConnectionState() {
    817         int maxState = BluetoothProfile.STATE_DISCONNECTED;
    818         for (LocalBluetoothProfile profile : getProfiles()) {
    819             int connectionStatus = getProfileConnectionState(profile);
    820             if (connectionStatus > maxState) {
    821                 maxState = connectionStatus;
    822             }
    823         }
    824         return maxState;
    825     }
    826 
    827     /**
    828      * @return resource for string that discribes the connection state of this device.
    829      */
    830     public int getConnectionSummary() {
    831         boolean profileConnected = false;       // at least one profile is connected
    832         boolean a2dpNotConnected = false;       // A2DP is preferred but not connected
    833         boolean headsetNotConnected = false;    // Headset is preferred but not connected
    834 
    835         for (LocalBluetoothProfile profile : getProfiles()) {
    836             int connectionStatus = getProfileConnectionState(profile);
    837 
    838             switch (connectionStatus) {
    839                 case BluetoothProfile.STATE_CONNECTING:
    840                 case BluetoothProfile.STATE_DISCONNECTING:
    841                     return Utils.getConnectionStateSummary(connectionStatus);
    842 
    843                 case BluetoothProfile.STATE_CONNECTED:
    844                     profileConnected = true;
    845                     break;
    846 
    847                 case BluetoothProfile.STATE_DISCONNECTED:
    848                     if (profile.isProfileReady()) {
    849                         if (profile instanceof A2dpProfile) {
    850                             a2dpNotConnected = true;
    851                         } else if (profile instanceof HeadsetProfile) {
    852                             headsetNotConnected = true;
    853                         }
    854                     }
    855                     break;
    856             }
    857         }
    858 
    859         if (profileConnected) {
    860             if (a2dpNotConnected && headsetNotConnected) {
    861                 return R.string.bluetooth_connected_no_headset_no_a2dp;
    862             } else if (a2dpNotConnected) {
    863                 return R.string.bluetooth_connected_no_a2dp;
    864             } else if (headsetNotConnected) {
    865                 return R.string.bluetooth_connected_no_headset;
    866             } else {
    867                 return R.string.bluetooth_connected;
    868             }
    869         }
    870 
    871         return getBondState() == BluetoothDevice.BOND_BONDING ? R.string.bluetooth_pairing : 0;
    872     }
    873 }
    874