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 mPhonebookRejectedTimes;
     72 
     73     private int mMessageRejectedTimes;
     74 
     75     private final Collection<Callback> mCallbacks = new ArrayList<Callback>();
     76 
     77     // Following constants indicate the user's choices of Phone book/message access settings
     78     // User hasn't made any choice or settings app has wiped out the memory
     79     final static int ACCESS_UNKNOWN = 0;
     80     // User has accepted the connection and let Settings app remember the decision
     81     final static int ACCESS_ALLOWED = 1;
     82     // User has rejected the connection and let Settings app remember the decision
     83     final static int ACCESS_REJECTED = 2;
     84 
     85     // how many times did User reject the connection to make the rejected persist.
     86     final static int PERSIST_REJECTED_TIMES_LIMIT = 2;
     87 
     88     private final static String PHONEBOOK_PREFS_NAME = "bluetooth_phonebook_permission";
     89     private final static String MESSAGE_PREFS_NAME = "bluetooth_message_permission";
     90     private final static String PHONEBOOK_REJECT_TIMES = "bluetooth_phonebook_reject";
     91     private final static String MESSAGE_REJECT_TIMES = "bluetooth_message_reject";
     92 
     93     /**
     94      * When we connect to multiple profiles, we only want to display a single
     95      * error even if they all fail. This tracks that state.
     96      */
     97     private boolean mIsConnectingErrorPossible;
     98 
     99     /**
    100      * Last time a bt profile auto-connect was attempted.
    101      * If an ACTION_UUID intent comes in within
    102      * MAX_UUID_DELAY_FOR_AUTO_CONNECT milliseconds, we will try auto-connect
    103      * again with the new UUIDs
    104      */
    105     private long mConnectAttempted;
    106 
    107     // See mConnectAttempted
    108     private static final long MAX_UUID_DELAY_FOR_AUTO_CONNECT = 5000;
    109 
    110     /** Auto-connect after pairing only if locally initiated. */
    111     private boolean mConnectAfterPairing;
    112 
    113     /**
    114      * Describes the current device and profile for logging.
    115      *
    116      * @param profile Profile to describe
    117      * @return Description of the device and profile
    118      */
    119     private String describe(LocalBluetoothProfile profile) {
    120         StringBuilder sb = new StringBuilder();
    121         sb.append("Address:").append(mDevice);
    122         if (profile != null) {
    123             sb.append(" Profile:").append(profile);
    124         }
    125 
    126         return sb.toString();
    127     }
    128 
    129     void onProfileStateChanged(LocalBluetoothProfile profile, int newProfileState) {
    130         if (Utils.D) {
    131             Log.d(TAG, "onProfileStateChanged: profile " + profile +
    132                     " newProfileState " + newProfileState);
    133         }
    134         if (mLocalAdapter.getBluetoothState() == BluetoothAdapter.STATE_TURNING_OFF)
    135         {
    136             if (Utils.D) Log.d(TAG, " BT Turninig Off...Profile conn state change ignored...");
    137             return;
    138         }
    139         mProfileConnectionState.put(profile, newProfileState);
    140         if (newProfileState == BluetoothProfile.STATE_CONNECTED) {
    141             if (!mProfiles.contains(profile)) {
    142                 mRemovedProfiles.remove(profile);
    143                 mProfiles.add(profile);
    144                 if (profile instanceof PanProfile &&
    145                         ((PanProfile) profile).isLocalRoleNap(mDevice)) {
    146                     // Device doesn't support NAP, so remove PanProfile on disconnect
    147                     mLocalNapRoleConnected = true;
    148                 }
    149             }
    150             if (profile instanceof MapProfile) {
    151                 profile.setPreferred(mDevice, true);
    152             }
    153         } else if (profile instanceof MapProfile &&
    154                 newProfileState == BluetoothProfile.STATE_DISCONNECTED) {
    155             if (mProfiles.contains(profile)) {
    156                 mRemovedProfiles.add(profile);
    157                 mProfiles.remove(profile);
    158             }
    159             profile.setPreferred(mDevice, false);
    160         } else if (mLocalNapRoleConnected && profile instanceof PanProfile &&
    161                 ((PanProfile) profile).isLocalRoleNap(mDevice) &&
    162                 newProfileState == BluetoothProfile.STATE_DISCONNECTED) {
    163             Log.d(TAG, "Removing PanProfile from device after NAP disconnect");
    164             mProfiles.remove(profile);
    165             mRemovedProfiles.add(profile);
    166             mLocalNapRoleConnected = false;
    167         }
    168     }
    169 
    170     CachedBluetoothDevice(Context context,
    171                           LocalBluetoothAdapter adapter,
    172                           LocalBluetoothProfileManager profileManager,
    173                           BluetoothDevice device) {
    174         mContext = context;
    175         mLocalAdapter = adapter;
    176         mProfileManager = profileManager;
    177         mDevice = device;
    178         mProfileConnectionState = new HashMap<LocalBluetoothProfile, Integer>();
    179         fillData();
    180     }
    181 
    182     void disconnect() {
    183         for (LocalBluetoothProfile profile : mProfiles) {
    184             disconnect(profile);
    185         }
    186         // Disconnect  PBAP server in case its connected
    187         // This is to ensure all the profiles are disconnected as some CK/Hs do not
    188         // disconnect  PBAP connection when HF connection is brought down
    189         PbapServerProfile PbapProfile = mProfileManager.getPbapProfile();
    190         if (PbapProfile.getConnectionStatus(mDevice) == BluetoothProfile.STATE_CONNECTED)
    191         {
    192             PbapProfile.disconnect(mDevice);
    193         }
    194     }
    195 
    196     void disconnect(LocalBluetoothProfile profile) {
    197         if (profile.disconnect(mDevice)) {
    198             if (Utils.D) {
    199                 Log.d(TAG, "Command sent successfully:DISCONNECT " + describe(profile));
    200             }
    201         }
    202     }
    203 
    204     void connect(boolean connectAllProfiles) {
    205         if (!ensurePaired()) {
    206             return;
    207         }
    208 
    209         mConnectAttempted = SystemClock.elapsedRealtime();
    210         connectWithoutResettingTimer(connectAllProfiles);
    211     }
    212 
    213     void onBondingDockConnect() {
    214         // Attempt to connect if UUIDs are available. Otherwise,
    215         // we will connect when the ACTION_UUID intent arrives.
    216         connect(false);
    217     }
    218 
    219     private void connectWithoutResettingTimer(boolean connectAllProfiles) {
    220         // Try to initialize the profiles if they were not.
    221         if (mProfiles.isEmpty()) {
    222             // if mProfiles is empty, then do not invoke updateProfiles. This causes a race
    223             // condition with carkits during pairing, wherein RemoteDevice.UUIDs have been updated
    224             // from bluetooth stack but ACTION.uuid is not sent yet.
    225             // Eventually ACTION.uuid will be received which shall trigger the connection of the
    226             // various profiles
    227             // If UUIDs are not available yet, connect will be happen
    228             // upon arrival of the ACTION_UUID intent.
    229             Log.d(TAG, "No profiles. Maybe we will connect later");
    230             return;
    231         }
    232 
    233         // Reset the only-show-one-error-dialog tracking variable
    234         mIsConnectingErrorPossible = true;
    235 
    236         int preferredProfiles = 0;
    237         for (LocalBluetoothProfile profile : mProfiles) {
    238             if (connectAllProfiles ? profile.isConnectable() : profile.isAutoConnectable()) {
    239                 if (profile.isPreferred(mDevice)) {
    240                     ++preferredProfiles;
    241                     connectInt(profile);
    242                 }
    243             }
    244         }
    245         if (DEBUG) Log.d(TAG, "Preferred profiles = " + preferredProfiles);
    246 
    247         if (preferredProfiles == 0) {
    248             connectAutoConnectableProfiles();
    249         }
    250     }
    251 
    252     private void connectAutoConnectableProfiles() {
    253         if (!ensurePaired()) {
    254             return;
    255         }
    256         // Reset the only-show-one-error-dialog tracking variable
    257         mIsConnectingErrorPossible = true;
    258 
    259         for (LocalBluetoothProfile profile : mProfiles) {
    260             if (profile.isAutoConnectable()) {
    261                 profile.setPreferred(mDevice, true);
    262                 connectInt(profile);
    263             }
    264         }
    265     }
    266 
    267     /**
    268      * Connect this device to the specified profile.
    269      *
    270      * @param profile the profile to use with the remote device
    271      */
    272     void connectProfile(LocalBluetoothProfile profile) {
    273         mConnectAttempted = SystemClock.elapsedRealtime();
    274         // Reset the only-show-one-error-dialog tracking variable
    275         mIsConnectingErrorPossible = true;
    276         connectInt(profile);
    277         // Refresh the UI based on profile.connect() call
    278         refresh();
    279     }
    280 
    281     synchronized void connectInt(LocalBluetoothProfile profile) {
    282         if (!ensurePaired()) {
    283             return;
    284         }
    285         if (profile.connect(mDevice)) {
    286             if (Utils.D) {
    287                 Log.d(TAG, "Command sent successfully:CONNECT " + describe(profile));
    288             }
    289             return;
    290         }
    291         Log.i(TAG, "Failed to connect " + profile.toString() + " to " + mName);
    292     }
    293 
    294     private boolean ensurePaired() {
    295         if (getBondState() == BluetoothDevice.BOND_NONE) {
    296             startPairing();
    297             return false;
    298         } else {
    299             return true;
    300         }
    301     }
    302 
    303     boolean startPairing() {
    304         // Pairing is unreliable while scanning, so cancel discovery
    305         if (mLocalAdapter.isDiscovering()) {
    306             mLocalAdapter.cancelDiscovery();
    307         }
    308 
    309         if (!mDevice.createBond()) {
    310             return false;
    311         }
    312 
    313         mConnectAfterPairing = true;  // auto-connect after pairing
    314         return true;
    315     }
    316 
    317     /**
    318      * Return true if user initiated pairing on this device. The message text is
    319      * slightly different for local vs. remote initiated pairing dialogs.
    320      */
    321     boolean isUserInitiatedPairing() {
    322         return mConnectAfterPairing;
    323     }
    324 
    325     void unpair() {
    326         int state = getBondState();
    327 
    328         if (state == BluetoothDevice.BOND_BONDING) {
    329             mDevice.cancelBondProcess();
    330         }
    331 
    332         if (state != BluetoothDevice.BOND_NONE) {
    333             final BluetoothDevice dev = mDevice;
    334             if (dev != null) {
    335                 final boolean successful = dev.removeBond();
    336                 if (successful) {
    337                     if (Utils.D) {
    338                         Log.d(TAG, "Command sent successfully:REMOVE_BOND " + describe(null));
    339                     }
    340                 } else if (Utils.V) {
    341                     Log.v(TAG, "Framework rejected command immediately:REMOVE_BOND " +
    342                             describe(null));
    343                 }
    344             }
    345         }
    346     }
    347 
    348     int getProfileConnectionState(LocalBluetoothProfile profile) {
    349         if (mProfileConnectionState == null ||
    350                 mProfileConnectionState.get(profile) == null) {
    351             // If cache is empty make the binder call to get the state
    352             int state = profile.getConnectionStatus(mDevice);
    353             mProfileConnectionState.put(profile, state);
    354         }
    355         return mProfileConnectionState.get(profile);
    356     }
    357 
    358     public void clearProfileConnectionState ()
    359     {
    360         if (Utils.D) {
    361             Log.d(TAG," Clearing all connection state for dev:" + mDevice.getName());
    362         }
    363         for (LocalBluetoothProfile profile :getProfiles()) {
    364             mProfileConnectionState.put(profile, BluetoothProfile.STATE_DISCONNECTED);
    365         }
    366     }
    367 
    368     // TODO: do any of these need to run async on a background thread?
    369     private void fillData() {
    370         fetchName();
    371         fetchBtClass();
    372         updateProfiles();
    373         fetchPhonebookPermissionChoice();
    374         fetchMessagePermissionChoice();
    375         fetchPhonebookRejectTimes();
    376         fetchMessageRejectTimes();
    377 
    378         mVisible = false;
    379         dispatchAttributesChanged();
    380     }
    381 
    382     BluetoothDevice getDevice() {
    383         return mDevice;
    384     }
    385 
    386     String getName() {
    387         return mName;
    388     }
    389 
    390     void setName(String name) {
    391         if (!mName.equals(name)) {
    392             if (TextUtils.isEmpty(name)) {
    393                 // TODO: use friendly name for unknown device (bug 1181856)
    394                 mName = mDevice.getAddress();
    395             } else {
    396                 mName = name;
    397                 mDevice.setAlias(name);
    398             }
    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             mPhonebookRejectedTimes = 0;
    545             savePhonebookRejectTimes();
    546             mMessageRejectedTimes = 0;
    547             saveMessageRejectTimes();
    548         }
    549 
    550         refresh();
    551 
    552         if (bondState == BluetoothDevice.BOND_BONDED) {
    553             if (mDevice.isBluetoothDock()) {
    554                 onBondingDockConnect();
    555             } else if (mConnectAfterPairing) {
    556                 connect(false);
    557             }
    558             mConnectAfterPairing = false;
    559         }
    560     }
    561 
    562     void setBtClass(BluetoothClass btClass) {
    563         if (btClass != null && mBtClass != btClass) {
    564             mBtClass = btClass;
    565             dispatchAttributesChanged();
    566         }
    567     }
    568 
    569     BluetoothClass getBtClass() {
    570         return mBtClass;
    571     }
    572 
    573     List<LocalBluetoothProfile> getProfiles() {
    574         return Collections.unmodifiableList(mProfiles);
    575     }
    576 
    577     List<LocalBluetoothProfile> getConnectableProfiles() {
    578         List<LocalBluetoothProfile> connectableProfiles =
    579                 new ArrayList<LocalBluetoothProfile>();
    580         for (LocalBluetoothProfile profile : mProfiles) {
    581             if (profile.isConnectable()) {
    582                 connectableProfiles.add(profile);
    583             }
    584         }
    585         return connectableProfiles;
    586     }
    587 
    588     List<LocalBluetoothProfile> getRemovedProfiles() {
    589         return mRemovedProfiles;
    590     }
    591 
    592     void registerCallback(Callback callback) {
    593         synchronized (mCallbacks) {
    594             mCallbacks.add(callback);
    595         }
    596     }
    597 
    598     void unregisterCallback(Callback callback) {
    599         synchronized (mCallbacks) {
    600             mCallbacks.remove(callback);
    601         }
    602     }
    603 
    604     private void dispatchAttributesChanged() {
    605         synchronized (mCallbacks) {
    606             for (Callback callback : mCallbacks) {
    607                 callback.onDeviceAttributesChanged();
    608             }
    609         }
    610     }
    611 
    612     @Override
    613     public String toString() {
    614         return mDevice.toString();
    615     }
    616 
    617     @Override
    618     public boolean equals(Object o) {
    619         if ((o == null) || !(o instanceof CachedBluetoothDevice)) {
    620             return false;
    621         }
    622         return mDevice.equals(((CachedBluetoothDevice) o).mDevice);
    623     }
    624 
    625     @Override
    626     public int hashCode() {
    627         return mDevice.getAddress().hashCode();
    628     }
    629 
    630     // This comparison uses non-final fields so the sort order may change
    631     // when device attributes change (such as bonding state). Settings
    632     // will completely refresh the device list when this happens.
    633     public int compareTo(CachedBluetoothDevice another) {
    634         // Connected above not connected
    635         int comparison = (another.isConnected() ? 1 : 0) - (isConnected() ? 1 : 0);
    636         if (comparison != 0) return comparison;
    637 
    638         // Paired above not paired
    639         comparison = (another.getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0) -
    640             (getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0);
    641         if (comparison != 0) return comparison;
    642 
    643         // Visible above not visible
    644         comparison = (another.mVisible ? 1 : 0) - (mVisible ? 1 : 0);
    645         if (comparison != 0) return comparison;
    646 
    647         // Stronger signal above weaker signal
    648         comparison = another.mRssi - mRssi;
    649         if (comparison != 0) return comparison;
    650 
    651         // Fallback on name
    652         return mName.compareTo(another.mName);
    653     }
    654 
    655     public interface Callback {
    656         void onDeviceAttributesChanged();
    657     }
    658 
    659     int getPhonebookPermissionChoice() {
    660         return mPhonebookPermissionChoice;
    661     }
    662 
    663     void setPhonebookPermissionChoice(int permissionChoice) {
    664         // if user reject it, only save it when reject exceed limit.
    665         if (permissionChoice == ACCESS_REJECTED) {
    666             mPhonebookRejectedTimes++;
    667             savePhonebookRejectTimes();
    668             if (mPhonebookRejectedTimes < PERSIST_REJECTED_TIMES_LIMIT) {
    669                 return;
    670             }
    671         }
    672 
    673         mPhonebookPermissionChoice = permissionChoice;
    674 
    675         SharedPreferences.Editor editor =
    676             mContext.getSharedPreferences(PHONEBOOK_PREFS_NAME, Context.MODE_PRIVATE).edit();
    677         if (permissionChoice == ACCESS_UNKNOWN) {
    678             editor.remove(mDevice.getAddress());
    679         } else {
    680             editor.putInt(mDevice.getAddress(), permissionChoice);
    681         }
    682         editor.commit();
    683     }
    684 
    685     private void fetchPhonebookPermissionChoice() {
    686         SharedPreferences preference = mContext.getSharedPreferences(PHONEBOOK_PREFS_NAME,
    687                                                                      Context.MODE_PRIVATE);
    688         mPhonebookPermissionChoice = preference.getInt(mDevice.getAddress(),
    689                                                        ACCESS_UNKNOWN);
    690     }
    691 
    692     private void fetchPhonebookRejectTimes() {
    693         SharedPreferences preference = mContext.getSharedPreferences(PHONEBOOK_REJECT_TIMES,
    694                                                                      Context.MODE_PRIVATE);
    695         mPhonebookRejectedTimes = preference.getInt(mDevice.getAddress(), 0);
    696     }
    697 
    698     private void savePhonebookRejectTimes() {
    699         SharedPreferences.Editor editor =
    700             mContext.getSharedPreferences(PHONEBOOK_REJECT_TIMES,
    701                                           Context.MODE_PRIVATE).edit();
    702         if (mPhonebookRejectedTimes == 0) {
    703             editor.remove(mDevice.getAddress());
    704         } else {
    705             editor.putInt(mDevice.getAddress(), mPhonebookRejectedTimes);
    706         }
    707         editor.commit();
    708     }
    709 
    710     int getMessagePermissionChoice() {
    711         return mMessagePermissionChoice;
    712     }
    713 
    714     void setMessagePermissionChoice(int permissionChoice) {
    715         // if user reject it, only save it when reject exceed limit.
    716         if (permissionChoice == ACCESS_REJECTED) {
    717             mMessageRejectedTimes++;
    718             saveMessageRejectTimes();
    719             if (mMessageRejectedTimes < PERSIST_REJECTED_TIMES_LIMIT) {
    720                 return;
    721             }
    722         }
    723 
    724         mMessagePermissionChoice = permissionChoice;
    725 
    726         SharedPreferences.Editor editor =
    727             mContext.getSharedPreferences(MESSAGE_PREFS_NAME, Context.MODE_PRIVATE).edit();
    728         if (permissionChoice == ACCESS_UNKNOWN) {
    729             editor.remove(mDevice.getAddress());
    730         } else {
    731             editor.putInt(mDevice.getAddress(), permissionChoice);
    732         }
    733         editor.commit();
    734     }
    735 
    736     private void fetchMessagePermissionChoice() {
    737         SharedPreferences preference = mContext.getSharedPreferences(MESSAGE_PREFS_NAME,
    738                                                                      Context.MODE_PRIVATE);
    739         mMessagePermissionChoice = preference.getInt(mDevice.getAddress(),
    740                                                        ACCESS_UNKNOWN);
    741     }
    742 
    743     private void fetchMessageRejectTimes() {
    744         SharedPreferences preference = mContext.getSharedPreferences(MESSAGE_REJECT_TIMES,
    745                                                                      Context.MODE_PRIVATE);
    746         mMessageRejectedTimes = preference.getInt(mDevice.getAddress(), 0);
    747     }
    748 
    749     private void saveMessageRejectTimes() {
    750         SharedPreferences.Editor editor =
    751             mContext.getSharedPreferences(MESSAGE_REJECT_TIMES, Context.MODE_PRIVATE).edit();
    752         if (mMessageRejectedTimes == 0) {
    753             editor.remove(mDevice.getAddress());
    754         } else {
    755             editor.putInt(mDevice.getAddress(), mMessageRejectedTimes);
    756         }
    757         editor.commit();
    758     }
    759 
    760 }
    761