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                 refresh();
    153             }
    154         } else if (profile instanceof MapProfile &&
    155                 newProfileState == BluetoothProfile.STATE_DISCONNECTED) {
    156             if (mProfiles.contains(profile)) {
    157                 mRemovedProfiles.add(profile);
    158                 mProfiles.remove(profile);
    159             }
    160             profile.setPreferred(mDevice, false);
    161             refresh();
    162         } else if (mLocalNapRoleConnected && profile instanceof PanProfile &&
    163                 ((PanProfile) profile).isLocalRoleNap(mDevice) &&
    164                 newProfileState == BluetoothProfile.STATE_DISCONNECTED) {
    165             Log.d(TAG, "Removing PanProfile from device after NAP disconnect");
    166             mProfiles.remove(profile);
    167             mRemovedProfiles.add(profile);
    168             mLocalNapRoleConnected = false;
    169         }
    170     }
    171 
    172     CachedBluetoothDevice(Context context,
    173                           LocalBluetoothAdapter adapter,
    174                           LocalBluetoothProfileManager profileManager,
    175                           BluetoothDevice device) {
    176         mContext = context;
    177         mLocalAdapter = adapter;
    178         mProfileManager = profileManager;
    179         mDevice = device;
    180         mProfileConnectionState = new HashMap<LocalBluetoothProfile, Integer>();
    181         fillData();
    182     }
    183 
    184     void disconnect() {
    185         for (LocalBluetoothProfile profile : mProfiles) {
    186             disconnect(profile);
    187         }
    188         // Disconnect  PBAP server in case its connected
    189         // This is to ensure all the profiles are disconnected as some CK/Hs do not
    190         // disconnect  PBAP connection when HF connection is brought down
    191         PbapServerProfile PbapProfile = mProfileManager.getPbapProfile();
    192         if (PbapProfile.getConnectionStatus(mDevice) == BluetoothProfile.STATE_CONNECTED)
    193         {
    194             PbapProfile.disconnect(mDevice);
    195         }
    196     }
    197 
    198     void disconnect(LocalBluetoothProfile profile) {
    199         if (profile.disconnect(mDevice)) {
    200             if (Utils.D) {
    201                 Log.d(TAG, "Command sent successfully:DISCONNECT " + describe(profile));
    202             }
    203         }
    204     }
    205 
    206     void connect(boolean connectAllProfiles) {
    207         if (!ensurePaired()) {
    208             return;
    209         }
    210 
    211         mConnectAttempted = SystemClock.elapsedRealtime();
    212         connectWithoutResettingTimer(connectAllProfiles);
    213     }
    214 
    215     void onBondingDockConnect() {
    216         // Attempt to connect if UUIDs are available. Otherwise,
    217         // we will connect when the ACTION_UUID intent arrives.
    218         connect(false);
    219     }
    220 
    221     private void connectWithoutResettingTimer(boolean connectAllProfiles) {
    222         // Try to initialize the profiles if they were not.
    223         if (mProfiles.isEmpty()) {
    224             // if mProfiles is empty, then do not invoke updateProfiles. This causes a race
    225             // condition with carkits during pairing, wherein RemoteDevice.UUIDs have been updated
    226             // from bluetooth stack but ACTION.uuid is not sent yet.
    227             // Eventually ACTION.uuid will be received which shall trigger the connection of the
    228             // various profiles
    229             // If UUIDs are not available yet, connect will be happen
    230             // upon arrival of the ACTION_UUID intent.
    231             Log.d(TAG, "No profiles. Maybe we will connect later");
    232             return;
    233         }
    234 
    235         // Reset the only-show-one-error-dialog tracking variable
    236         mIsConnectingErrorPossible = true;
    237 
    238         int preferredProfiles = 0;
    239         for (LocalBluetoothProfile profile : mProfiles) {
    240             if (connectAllProfiles ? profile.isConnectable() : profile.isAutoConnectable()) {
    241                 if (profile.isPreferred(mDevice)) {
    242                     ++preferredProfiles;
    243                     connectInt(profile);
    244                 }
    245             }
    246         }
    247         if (DEBUG) Log.d(TAG, "Preferred profiles = " + preferredProfiles);
    248 
    249         if (preferredProfiles == 0) {
    250             connectAutoConnectableProfiles();
    251         }
    252     }
    253 
    254     private void connectAutoConnectableProfiles() {
    255         if (!ensurePaired()) {
    256             return;
    257         }
    258         // Reset the only-show-one-error-dialog tracking variable
    259         mIsConnectingErrorPossible = true;
    260 
    261         for (LocalBluetoothProfile profile : mProfiles) {
    262             if (profile.isAutoConnectable()) {
    263                 profile.setPreferred(mDevice, true);
    264                 connectInt(profile);
    265             }
    266         }
    267     }
    268 
    269     /**
    270      * Connect this device to the specified profile.
    271      *
    272      * @param profile the profile to use with the remote device
    273      */
    274     void connectProfile(LocalBluetoothProfile profile) {
    275         mConnectAttempted = SystemClock.elapsedRealtime();
    276         // Reset the only-show-one-error-dialog tracking variable
    277         mIsConnectingErrorPossible = true;
    278         connectInt(profile);
    279         // Refresh the UI based on profile.connect() call
    280         refresh();
    281     }
    282 
    283     synchronized void connectInt(LocalBluetoothProfile profile) {
    284         if (!ensurePaired()) {
    285             return;
    286         }
    287         if (profile.connect(mDevice)) {
    288             if (Utils.D) {
    289                 Log.d(TAG, "Command sent successfully:CONNECT " + describe(profile));
    290             }
    291             return;
    292         }
    293         Log.i(TAG, "Failed to connect " + profile.toString() + " to " + mName);
    294     }
    295 
    296     private boolean ensurePaired() {
    297         if (getBondState() == BluetoothDevice.BOND_NONE) {
    298             startPairing();
    299             return false;
    300         } else {
    301             return true;
    302         }
    303     }
    304 
    305     boolean startPairing() {
    306         // Pairing is unreliable while scanning, so cancel discovery
    307         if (mLocalAdapter.isDiscovering()) {
    308             mLocalAdapter.cancelDiscovery();
    309         }
    310 
    311         if (!mDevice.createBond()) {
    312             return false;
    313         }
    314 
    315         mConnectAfterPairing = true;  // auto-connect after pairing
    316         return true;
    317     }
    318 
    319     /**
    320      * Return true if user initiated pairing on this device. The message text is
    321      * slightly different for local vs. remote initiated pairing dialogs.
    322      */
    323     boolean isUserInitiatedPairing() {
    324         return mConnectAfterPairing;
    325     }
    326 
    327     void unpair() {
    328         int state = getBondState();
    329 
    330         if (state == BluetoothDevice.BOND_BONDING) {
    331             mDevice.cancelBondProcess();
    332         }
    333 
    334         if (state != BluetoothDevice.BOND_NONE) {
    335             final BluetoothDevice dev = mDevice;
    336             if (dev != null) {
    337                 final boolean successful = dev.removeBond();
    338                 if (successful) {
    339                     if (Utils.D) {
    340                         Log.d(TAG, "Command sent successfully:REMOVE_BOND " + describe(null));
    341                     }
    342                 } else if (Utils.V) {
    343                     Log.v(TAG, "Framework rejected command immediately:REMOVE_BOND " +
    344                             describe(null));
    345                 }
    346             }
    347         }
    348     }
    349 
    350     int getProfileConnectionState(LocalBluetoothProfile profile) {
    351         if (mProfileConnectionState == null ||
    352                 mProfileConnectionState.get(profile) == null) {
    353             // If cache is empty make the binder call to get the state
    354             int state = profile.getConnectionStatus(mDevice);
    355             mProfileConnectionState.put(profile, state);
    356         }
    357         return mProfileConnectionState.get(profile);
    358     }
    359 
    360     public void clearProfileConnectionState ()
    361     {
    362         if (Utils.D) {
    363             Log.d(TAG," Clearing all connection state for dev:" + mDevice.getName());
    364         }
    365         for (LocalBluetoothProfile profile :getProfiles()) {
    366             mProfileConnectionState.put(profile, BluetoothProfile.STATE_DISCONNECTED);
    367         }
    368     }
    369 
    370     // TODO: do any of these need to run async on a background thread?
    371     private void fillData() {
    372         fetchName();
    373         fetchBtClass();
    374         updateProfiles();
    375         fetchPhonebookPermissionChoice();
    376         fetchMessagePermissionChoice();
    377         fetchPhonebookRejectTimes();
    378         fetchMessageRejectTimes();
    379 
    380         mVisible = false;
    381         dispatchAttributesChanged();
    382     }
    383 
    384     BluetoothDevice getDevice() {
    385         return mDevice;
    386     }
    387 
    388     String getName() {
    389         return mName;
    390     }
    391 
    392     void setName(String name) {
    393         if (!mName.equals(name)) {
    394             if (TextUtils.isEmpty(name)) {
    395                 // TODO: use friendly name for unknown device (bug 1181856)
    396                 mName = mDevice.getAddress();
    397             } else {
    398                 mName = name;
    399                 mDevice.setAlias(name);
    400             }
    401             dispatchAttributesChanged();
    402         }
    403     }
    404 
    405     void refreshName() {
    406         fetchName();
    407         dispatchAttributesChanged();
    408     }
    409 
    410     private void fetchName() {
    411         mName = mDevice.getAliasName();
    412 
    413         if (TextUtils.isEmpty(mName)) {
    414             mName = mDevice.getAddress();
    415             if (DEBUG) Log.d(TAG, "Device has no name (yet), use address: " + mName);
    416         }
    417     }
    418 
    419     void refresh() {
    420         dispatchAttributesChanged();
    421     }
    422 
    423     boolean isVisible() {
    424         return mVisible;
    425     }
    426 
    427     void setVisible(boolean visible) {
    428         if (mVisible != visible) {
    429             mVisible = visible;
    430             dispatchAttributesChanged();
    431         }
    432     }
    433 
    434     int getBondState() {
    435         return mDevice.getBondState();
    436     }
    437 
    438     void setRssi(short rssi) {
    439         if (mRssi != rssi) {
    440             mRssi = rssi;
    441             dispatchAttributesChanged();
    442         }
    443     }
    444 
    445     /**
    446      * Checks whether we are connected to this device (any profile counts).
    447      *
    448      * @return Whether it is connected.
    449      */
    450     boolean isConnected() {
    451         for (LocalBluetoothProfile profile : mProfiles) {
    452             int status = getProfileConnectionState(profile);
    453             if (status == BluetoothProfile.STATE_CONNECTED) {
    454                 return true;
    455             }
    456         }
    457 
    458         return false;
    459     }
    460 
    461     boolean isConnectedProfile(LocalBluetoothProfile profile) {
    462         int status = getProfileConnectionState(profile);
    463         return status == BluetoothProfile.STATE_CONNECTED;
    464 
    465     }
    466 
    467     boolean isBusy() {
    468         for (LocalBluetoothProfile profile : mProfiles) {
    469             int status = getProfileConnectionState(profile);
    470             if (status == BluetoothProfile.STATE_CONNECTING
    471                     || status == BluetoothProfile.STATE_DISCONNECTING) {
    472                 return true;
    473             }
    474         }
    475         return getBondState() == BluetoothDevice.BOND_BONDING;
    476     }
    477 
    478     /**
    479      * Fetches a new value for the cached BT class.
    480      */
    481     private void fetchBtClass() {
    482         mBtClass = mDevice.getBluetoothClass();
    483     }
    484 
    485     private boolean updateProfiles() {
    486         ParcelUuid[] uuids = mDevice.getUuids();
    487         if (uuids == null) return false;
    488 
    489         ParcelUuid[] localUuids = mLocalAdapter.getUuids();
    490         if (localUuids == null) return false;
    491 
    492         mProfileManager.updateProfiles(uuids, localUuids, mProfiles, mRemovedProfiles, mLocalNapRoleConnected);
    493 
    494         if (DEBUG) {
    495             Log.e(TAG, "updating profiles for " + mDevice.getAliasName());
    496             BluetoothClass bluetoothClass = mDevice.getBluetoothClass();
    497 
    498             if (bluetoothClass != null) Log.v(TAG, "Class: " + bluetoothClass.toString());
    499             Log.v(TAG, "UUID:");
    500             for (ParcelUuid uuid : uuids) {
    501                 Log.v(TAG, "  " + uuid);
    502             }
    503         }
    504         return true;
    505     }
    506 
    507     /**
    508      * Refreshes the UI for the BT class, including fetching the latest value
    509      * for the class.
    510      */
    511     void refreshBtClass() {
    512         fetchBtClass();
    513         dispatchAttributesChanged();
    514     }
    515 
    516     /**
    517      * Refreshes the UI when framework alerts us of a UUID change.
    518      */
    519     void onUuidChanged() {
    520         updateProfiles();
    521 
    522         if (DEBUG) {
    523             Log.e(TAG, "onUuidChanged: Time since last connect"
    524                     + (SystemClock.elapsedRealtime() - mConnectAttempted));
    525         }
    526 
    527         /*
    528          * If a connect was attempted earlier without any UUID, we will do the
    529          * connect now.
    530          */
    531         if (!mProfiles.isEmpty()
    532                 && (mConnectAttempted + MAX_UUID_DELAY_FOR_AUTO_CONNECT) > SystemClock
    533                         .elapsedRealtime()) {
    534             connectWithoutResettingTimer(false);
    535         }
    536         dispatchAttributesChanged();
    537     }
    538 
    539     void onBondingStateChanged(int bondState) {
    540         if (bondState == BluetoothDevice.BOND_NONE) {
    541             mProfiles.clear();
    542             mConnectAfterPairing = false;  // cancel auto-connect
    543             setPhonebookPermissionChoice(ACCESS_UNKNOWN);
    544             setMessagePermissionChoice(ACCESS_UNKNOWN);
    545             mPhonebookRejectedTimes = 0;
    546             savePhonebookRejectTimes();
    547             mMessageRejectedTimes = 0;
    548             saveMessageRejectTimes();
    549         }
    550 
    551         refresh();
    552 
    553         if (bondState == BluetoothDevice.BOND_BONDED) {
    554             if (mDevice.isBluetoothDock()) {
    555                 onBondingDockConnect();
    556             } else if (mConnectAfterPairing) {
    557                 connect(false);
    558             }
    559             mConnectAfterPairing = false;
    560         }
    561     }
    562 
    563     void setBtClass(BluetoothClass btClass) {
    564         if (btClass != null && mBtClass != btClass) {
    565             mBtClass = btClass;
    566             dispatchAttributesChanged();
    567         }
    568     }
    569 
    570     BluetoothClass getBtClass() {
    571         return mBtClass;
    572     }
    573 
    574     List<LocalBluetoothProfile> getProfiles() {
    575         return Collections.unmodifiableList(mProfiles);
    576     }
    577 
    578     List<LocalBluetoothProfile> getConnectableProfiles() {
    579         List<LocalBluetoothProfile> connectableProfiles =
    580                 new ArrayList<LocalBluetoothProfile>();
    581         for (LocalBluetoothProfile profile : mProfiles) {
    582             if (profile.isConnectable()) {
    583                 connectableProfiles.add(profile);
    584             }
    585         }
    586         return connectableProfiles;
    587     }
    588 
    589     List<LocalBluetoothProfile> getRemovedProfiles() {
    590         return mRemovedProfiles;
    591     }
    592 
    593     void registerCallback(Callback callback) {
    594         synchronized (mCallbacks) {
    595             mCallbacks.add(callback);
    596         }
    597     }
    598 
    599     void unregisterCallback(Callback callback) {
    600         synchronized (mCallbacks) {
    601             mCallbacks.remove(callback);
    602         }
    603     }
    604 
    605     private void dispatchAttributesChanged() {
    606         synchronized (mCallbacks) {
    607             for (Callback callback : mCallbacks) {
    608                 callback.onDeviceAttributesChanged();
    609             }
    610         }
    611     }
    612 
    613     @Override
    614     public String toString() {
    615         return mDevice.toString();
    616     }
    617 
    618     @Override
    619     public boolean equals(Object o) {
    620         if ((o == null) || !(o instanceof CachedBluetoothDevice)) {
    621             return false;
    622         }
    623         return mDevice.equals(((CachedBluetoothDevice) o).mDevice);
    624     }
    625 
    626     @Override
    627     public int hashCode() {
    628         return mDevice.getAddress().hashCode();
    629     }
    630 
    631     // This comparison uses non-final fields so the sort order may change
    632     // when device attributes change (such as bonding state). Settings
    633     // will completely refresh the device list when this happens.
    634     public int compareTo(CachedBluetoothDevice another) {
    635         // Connected above not connected
    636         int comparison = (another.isConnected() ? 1 : 0) - (isConnected() ? 1 : 0);
    637         if (comparison != 0) return comparison;
    638 
    639         // Paired above not paired
    640         comparison = (another.getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0) -
    641             (getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0);
    642         if (comparison != 0) return comparison;
    643 
    644         // Visible above not visible
    645         comparison = (another.mVisible ? 1 : 0) - (mVisible ? 1 : 0);
    646         if (comparison != 0) return comparison;
    647 
    648         // Stronger signal above weaker signal
    649         comparison = another.mRssi - mRssi;
    650         if (comparison != 0) return comparison;
    651 
    652         // Fallback on name
    653         return mName.compareTo(another.mName);
    654     }
    655 
    656     public interface Callback {
    657         void onDeviceAttributesChanged();
    658     }
    659 
    660     int getPhonebookPermissionChoice() {
    661         return mPhonebookPermissionChoice;
    662     }
    663 
    664     void setPhonebookPermissionChoice(int permissionChoice) {
    665         // if user reject it, only save it when reject exceed limit.
    666         if (permissionChoice == ACCESS_REJECTED) {
    667             mPhonebookRejectedTimes++;
    668             savePhonebookRejectTimes();
    669             if (mPhonebookRejectedTimes < PERSIST_REJECTED_TIMES_LIMIT) {
    670                 return;
    671             }
    672         }
    673 
    674         mPhonebookPermissionChoice = permissionChoice;
    675 
    676         SharedPreferences.Editor editor =
    677             mContext.getSharedPreferences(PHONEBOOK_PREFS_NAME, Context.MODE_PRIVATE).edit();
    678         if (permissionChoice == ACCESS_UNKNOWN) {
    679             editor.remove(mDevice.getAddress());
    680         } else {
    681             editor.putInt(mDevice.getAddress(), permissionChoice);
    682         }
    683         editor.commit();
    684     }
    685 
    686     private void fetchPhonebookPermissionChoice() {
    687         SharedPreferences preference = mContext.getSharedPreferences(PHONEBOOK_PREFS_NAME,
    688                                                                      Context.MODE_PRIVATE);
    689         mPhonebookPermissionChoice = preference.getInt(mDevice.getAddress(),
    690                                                        ACCESS_UNKNOWN);
    691     }
    692 
    693     private void fetchPhonebookRejectTimes() {
    694         SharedPreferences preference = mContext.getSharedPreferences(PHONEBOOK_REJECT_TIMES,
    695                                                                      Context.MODE_PRIVATE);
    696         mPhonebookRejectedTimes = preference.getInt(mDevice.getAddress(), 0);
    697     }
    698 
    699     private void savePhonebookRejectTimes() {
    700         SharedPreferences.Editor editor =
    701             mContext.getSharedPreferences(PHONEBOOK_REJECT_TIMES,
    702                                           Context.MODE_PRIVATE).edit();
    703         if (mPhonebookRejectedTimes == 0) {
    704             editor.remove(mDevice.getAddress());
    705         } else {
    706             editor.putInt(mDevice.getAddress(), mPhonebookRejectedTimes);
    707         }
    708         editor.commit();
    709     }
    710 
    711     int getMessagePermissionChoice() {
    712         return mMessagePermissionChoice;
    713     }
    714 
    715     void setMessagePermissionChoice(int permissionChoice) {
    716         // if user reject it, only save it when reject exceed limit.
    717         if (permissionChoice == ACCESS_REJECTED) {
    718             mMessageRejectedTimes++;
    719             saveMessageRejectTimes();
    720             if (mMessageRejectedTimes < PERSIST_REJECTED_TIMES_LIMIT) {
    721                 return;
    722             }
    723         }
    724 
    725         mMessagePermissionChoice = permissionChoice;
    726 
    727         SharedPreferences.Editor editor =
    728             mContext.getSharedPreferences(MESSAGE_PREFS_NAME, Context.MODE_PRIVATE).edit();
    729         if (permissionChoice == ACCESS_UNKNOWN) {
    730             editor.remove(mDevice.getAddress());
    731         } else {
    732             editor.putInt(mDevice.getAddress(), permissionChoice);
    733         }
    734         editor.commit();
    735     }
    736 
    737     private void fetchMessagePermissionChoice() {
    738         SharedPreferences preference = mContext.getSharedPreferences(MESSAGE_PREFS_NAME,
    739                                                                      Context.MODE_PRIVATE);
    740         mMessagePermissionChoice = preference.getInt(mDevice.getAddress(),
    741                                                        ACCESS_UNKNOWN);
    742     }
    743 
    744     private void fetchMessageRejectTimes() {
    745         SharedPreferences preference = mContext.getSharedPreferences(MESSAGE_REJECT_TIMES,
    746                                                                      Context.MODE_PRIVATE);
    747         mMessageRejectedTimes = preference.getInt(mDevice.getAddress(), 0);
    748     }
    749 
    750     private void saveMessageRejectTimes() {
    751         SharedPreferences.Editor editor =
    752             mContext.getSharedPreferences(MESSAGE_REJECT_TIMES, Context.MODE_PRIVATE).edit();
    753         if (mMessageRejectedTimes == 0) {
    754             editor.remove(mDevice.getAddress());
    755         } else {
    756             editor.putInt(mDevice.getAddress(), mMessageRejectedTimes);
    757         }
    758         editor.commit();
    759     }
    760 
    761 }
    762