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.settings.bluetooth;
     18 
     19 import android.bluetooth.BluetoothClass;
     20 import android.bluetooth.BluetoothDevice;
     21 import android.bluetooth.BluetoothProfile;
     22 import android.content.Context;
     23 import android.content.SharedPreferences;
     24 import android.os.ParcelUuid;
     25 import android.os.SystemClock;
     26 import android.text.TextUtils;
     27 import android.util.Log;
     28 import android.bluetooth.BluetoothAdapter;
     29 
     30 import java.util.ArrayList;
     31 import java.util.Collection;
     32 import java.util.Collections;
     33 import java.util.HashMap;
     34 import 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  */
     42 final 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