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.bluetooth.BluetoothUuid;
     23 import android.content.Context;
     24 import android.content.SharedPreferences;
     25 import android.os.ParcelUuid;
     26 import android.os.SystemClock;
     27 import android.text.TextUtils;
     28 import android.util.Log;
     29 import android.bluetooth.BluetoothAdapter;
     30 
     31 import java.util.ArrayList;
     32 import java.util.Collection;
     33 import java.util.Collections;
     34 import java.util.HashMap;
     35 import java.util.List;
     36 
     37 /**
     38  * CachedBluetoothDevice represents a remote Bluetooth device. It contains
     39  * attributes of the device (such as the address, name, RSSI, etc.) and
     40  * functionality that can be performed on the device (connect, pair, disconnect,
     41  * etc.).
     42  */
     43 final class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> {
     44     private static final String TAG = "CachedBluetoothDevice";
     45     private static final boolean DEBUG = Utils.V;
     46 
     47     private final Context mContext;
     48     private final LocalBluetoothAdapter mLocalAdapter;
     49     private final LocalBluetoothProfileManager mProfileManager;
     50     private final BluetoothDevice mDevice;
     51     private String mName;
     52     private short mRssi;
     53     private BluetoothClass mBtClass;
     54     private HashMap<LocalBluetoothProfile, Integer> mProfileConnectionState;
     55 
     56     private final List<LocalBluetoothProfile> mProfiles =
     57             new ArrayList<LocalBluetoothProfile>();
     58 
     59     // List of profiles that were previously in mProfiles, but have been removed
     60     private final List<LocalBluetoothProfile> mRemovedProfiles =
     61             new ArrayList<LocalBluetoothProfile>();
     62 
     63     // Device supports PANU but not NAP: remove PanProfile after device disconnects from NAP
     64     private boolean mLocalNapRoleConnected;
     65 
     66     private boolean mVisible;
     67 
     68     private int mPhonebookPermissionChoice;
     69 
     70     private int mMessagePermissionChoice;
     71 
     72     private int mMessageRejectionCount;
     73 
     74     private final Collection<Callback> mCallbacks = new ArrayList<Callback>();
     75 
     76     // Following constants indicate the user's choices of Phone book/message access settings
     77     // User hasn't made any choice or settings app has wiped out the memory
     78     public final static int ACCESS_UNKNOWN = 0;
     79     // User has accepted the connection and let Settings app remember the decision
     80     public final static int ACCESS_ALLOWED = 1;
     81     // User has rejected the connection and let Settings app remember the decision
     82     public final static int ACCESS_REJECTED = 2;
     83 
     84     // How many times user should reject the connection to make the choice persist.
     85     private final static int MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST = 2;
     86 
     87     private final static String MESSAGE_REJECTION_COUNT_PREFS_NAME = "bluetooth_message_reject";
     88 
     89     /**
     90      * When we connect to multiple profiles, we only want to display a single
     91      * error even if they all fail. This tracks that state.
     92      */
     93     private boolean mIsConnectingErrorPossible;
     94 
     95     /**
     96      * Last time a bt profile auto-connect was attempted.
     97      * If an ACTION_UUID intent comes in within
     98      * MAX_UUID_DELAY_FOR_AUTO_CONNECT milliseconds, we will try auto-connect
     99      * again with the new UUIDs
    100      */
    101     private long mConnectAttempted;
    102 
    103     // See mConnectAttempted
    104     private static final long MAX_UUID_DELAY_FOR_AUTO_CONNECT = 5000;
    105 
    106     /** Auto-connect after pairing only if locally initiated. */
    107     private boolean mConnectAfterPairing;
    108 
    109     /**
    110      * Describes the current device and profile for logging.
    111      *
    112      * @param profile Profile to describe
    113      * @return Description of the device and profile
    114      */
    115     private String describe(LocalBluetoothProfile profile) {
    116         StringBuilder sb = new StringBuilder();
    117         sb.append("Address:").append(mDevice);
    118         if (profile != null) {
    119             sb.append(" Profile:").append(profile);
    120         }
    121 
    122         return sb.toString();
    123     }
    124 
    125     void onProfileStateChanged(LocalBluetoothProfile profile, int newProfileState) {
    126         if (Utils.D) {
    127             Log.d(TAG, "onProfileStateChanged: profile " + profile +
    128                     " newProfileState " + newProfileState);
    129         }
    130         if (mLocalAdapter.getBluetoothState() == BluetoothAdapter.STATE_TURNING_OFF)
    131         {
    132             if (Utils.D) Log.d(TAG, " BT Turninig Off...Profile conn state change ignored...");
    133             return;
    134         }
    135         mProfileConnectionState.put(profile, newProfileState);
    136         if (newProfileState == BluetoothProfile.STATE_CONNECTED) {
    137             if (profile instanceof MapProfile) {
    138                 profile.setPreferred(mDevice, true);
    139             } else if (!mProfiles.contains(profile)) {
    140                 mRemovedProfiles.remove(profile);
    141                 mProfiles.add(profile);
    142                 if (profile instanceof PanProfile &&
    143                         ((PanProfile) profile).isLocalRoleNap(mDevice)) {
    144                     // Device doesn't support NAP, so remove PanProfile on disconnect
    145                     mLocalNapRoleConnected = true;
    146                 }
    147             }
    148         } else if (profile instanceof MapProfile &&
    149                 newProfileState == BluetoothProfile.STATE_DISCONNECTED) {
    150             profile.setPreferred(mDevice, false);
    151         } else if (mLocalNapRoleConnected && profile instanceof PanProfile &&
    152                 ((PanProfile) profile).isLocalRoleNap(mDevice) &&
    153                 newProfileState == BluetoothProfile.STATE_DISCONNECTED) {
    154             Log.d(TAG, "Removing PanProfile from device after NAP disconnect");
    155             mProfiles.remove(profile);
    156             mRemovedProfiles.add(profile);
    157             mLocalNapRoleConnected = false;
    158         }
    159     }
    160 
    161     CachedBluetoothDevice(Context context,
    162                           LocalBluetoothAdapter adapter,
    163                           LocalBluetoothProfileManager profileManager,
    164                           BluetoothDevice device) {
    165         mContext = context;
    166         mLocalAdapter = adapter;
    167         mProfileManager = profileManager;
    168         mDevice = device;
    169         mProfileConnectionState = new HashMap<LocalBluetoothProfile, Integer>();
    170         fillData();
    171     }
    172 
    173     void disconnect() {
    174         for (LocalBluetoothProfile profile : mProfiles) {
    175             disconnect(profile);
    176         }
    177         // Disconnect  PBAP server in case its connected
    178         // This is to ensure all the profiles are disconnected as some CK/Hs do not
    179         // disconnect  PBAP connection when HF connection is brought down
    180         PbapServerProfile PbapProfile = mProfileManager.getPbapProfile();
    181         if (PbapProfile.getConnectionStatus(mDevice) == BluetoothProfile.STATE_CONNECTED)
    182         {
    183             PbapProfile.disconnect(mDevice);
    184         }
    185     }
    186 
    187     void disconnect(LocalBluetoothProfile profile) {
    188         if (profile.disconnect(mDevice)) {
    189             if (Utils.D) {
    190                 Log.d(TAG, "Command sent successfully:DISCONNECT " + describe(profile));
    191             }
    192         }
    193     }
    194 
    195     void connect(boolean connectAllProfiles) {
    196         if (!ensurePaired()) {
    197             return;
    198         }
    199 
    200         mConnectAttempted = SystemClock.elapsedRealtime();
    201         connectWithoutResettingTimer(connectAllProfiles);
    202     }
    203 
    204     void onBondingDockConnect() {
    205         // Attempt to connect if UUIDs are available. Otherwise,
    206         // we will connect when the ACTION_UUID intent arrives.
    207         connect(false);
    208     }
    209 
    210     private void connectWithoutResettingTimer(boolean connectAllProfiles) {
    211         // Try to initialize the profiles if they were not.
    212         if (mProfiles.isEmpty()) {
    213             // if mProfiles is empty, then do not invoke updateProfiles. This causes a race
    214             // condition with carkits during pairing, wherein RemoteDevice.UUIDs have been updated
    215             // from bluetooth stack but ACTION.uuid is not sent yet.
    216             // Eventually ACTION.uuid will be received which shall trigger the connection of the
    217             // various profiles
    218             // If UUIDs are not available yet, connect will be happen
    219             // upon arrival of the ACTION_UUID intent.
    220             Log.d(TAG, "No profiles. Maybe we will connect later");
    221             return;
    222         }
    223 
    224         // Reset the only-show-one-error-dialog tracking variable
    225         mIsConnectingErrorPossible = true;
    226 
    227         int preferredProfiles = 0;
    228         for (LocalBluetoothProfile profile : mProfiles) {
    229             if (connectAllProfiles ? profile.isConnectable() : profile.isAutoConnectable()) {
    230                 if (profile.isPreferred(mDevice)) {
    231                     ++preferredProfiles;
    232                     connectInt(profile);
    233                 }
    234             }
    235         }
    236         if (DEBUG) Log.d(TAG, "Preferred profiles = " + preferredProfiles);
    237 
    238         if (preferredProfiles == 0) {
    239             connectAutoConnectableProfiles();
    240         }
    241     }
    242 
    243     private void connectAutoConnectableProfiles() {
    244         if (!ensurePaired()) {
    245             return;
    246         }
    247         // Reset the only-show-one-error-dialog tracking variable
    248         mIsConnectingErrorPossible = true;
    249 
    250         for (LocalBluetoothProfile profile : mProfiles) {
    251             if (profile.isAutoConnectable()) {
    252                 profile.setPreferred(mDevice, true);
    253                 connectInt(profile);
    254             }
    255         }
    256     }
    257 
    258     /**
    259      * Connect this device to the specified profile.
    260      *
    261      * @param profile the profile to use with the remote device
    262      */
    263     void connectProfile(LocalBluetoothProfile profile) {
    264         mConnectAttempted = SystemClock.elapsedRealtime();
    265         // Reset the only-show-one-error-dialog tracking variable
    266         mIsConnectingErrorPossible = true;
    267         connectInt(profile);
    268         // Refresh the UI based on profile.connect() call
    269         refresh();
    270     }
    271 
    272     synchronized void connectInt(LocalBluetoothProfile profile) {
    273         if (!ensurePaired()) {
    274             return;
    275         }
    276         if (profile.connect(mDevice)) {
    277             if (Utils.D) {
    278                 Log.d(TAG, "Command sent successfully:CONNECT " + describe(profile));
    279             }
    280             return;
    281         }
    282         Log.i(TAG, "Failed to connect " + profile.toString() + " to " + mName);
    283     }
    284 
    285     private boolean ensurePaired() {
    286         if (getBondState() == BluetoothDevice.BOND_NONE) {
    287             startPairing();
    288             return false;
    289         } else {
    290             return true;
    291         }
    292     }
    293 
    294     boolean startPairing() {
    295         // Pairing is unreliable while scanning, so cancel discovery
    296         if (mLocalAdapter.isDiscovering()) {
    297             mLocalAdapter.cancelDiscovery();
    298         }
    299 
    300         if (!mDevice.createBond()) {
    301             return false;
    302         }
    303 
    304         mConnectAfterPairing = true;  // auto-connect after pairing
    305         return true;
    306     }
    307 
    308     /**
    309      * Return true if user initiated pairing on this device. The message text is
    310      * slightly different for local vs. remote initiated pairing dialogs.
    311      */
    312     boolean isUserInitiatedPairing() {
    313         return mConnectAfterPairing;
    314     }
    315 
    316     void unpair() {
    317         int state = getBondState();
    318 
    319         if (state == BluetoothDevice.BOND_BONDING) {
    320             mDevice.cancelBondProcess();
    321         }
    322 
    323         if (state != BluetoothDevice.BOND_NONE) {
    324             final BluetoothDevice dev = mDevice;
    325             if (dev != null) {
    326                 final boolean successful = dev.removeBond();
    327                 if (successful) {
    328                     if (Utils.D) {
    329                         Log.d(TAG, "Command sent successfully:REMOVE_BOND " + describe(null));
    330                     }
    331                 } else if (Utils.V) {
    332                     Log.v(TAG, "Framework rejected command immediately:REMOVE_BOND " +
    333                             describe(null));
    334                 }
    335             }
    336         }
    337     }
    338 
    339     int getProfileConnectionState(LocalBluetoothProfile profile) {
    340         if (mProfileConnectionState == null ||
    341                 mProfileConnectionState.get(profile) == null) {
    342             // If cache is empty make the binder call to get the state
    343             int state = profile.getConnectionStatus(mDevice);
    344             mProfileConnectionState.put(profile, state);
    345         }
    346         return mProfileConnectionState.get(profile);
    347     }
    348 
    349     public void clearProfileConnectionState ()
    350     {
    351         if (Utils.D) {
    352             Log.d(TAG," Clearing all connection state for dev:" + mDevice.getName());
    353         }
    354         for (LocalBluetoothProfile profile :getProfiles()) {
    355             mProfileConnectionState.put(profile, BluetoothProfile.STATE_DISCONNECTED);
    356         }
    357     }
    358 
    359     // TODO: do any of these need to run async on a background thread?
    360     private void fillData() {
    361         fetchName();
    362         fetchBtClass();
    363         updateProfiles();
    364         migratePhonebookPermissionChoice();
    365         migrateMessagePermissionChoice();
    366         fetchMessageRejectionCount();
    367 
    368         mVisible = false;
    369         dispatchAttributesChanged();
    370     }
    371 
    372     BluetoothDevice getDevice() {
    373         return mDevice;
    374     }
    375 
    376     String getName() {
    377         return mName;
    378     }
    379 
    380     /**
    381      * Populate name from BluetoothDevice.ACTION_FOUND intent
    382      */
    383     void setNewName(String name) {
    384         if (mName == null) {
    385             mName = name;
    386             if (mName == null || TextUtils.isEmpty(mName)) {
    387                 mName = mDevice.getAddress();
    388             }
    389             dispatchAttributesChanged();
    390         }
    391     }
    392 
    393     /**
    394      * user changes the device name
    395      */
    396     void setName(String name) {
    397         if (!mName.equals(name)) {
    398             mName = name;
    399             mDevice.setAlias(name);
    400             dispatchAttributesChanged();
    401         }
    402     }
    403 
    404     void refreshName() {
    405         fetchName();
    406         dispatchAttributesChanged();
    407     }
    408 
    409     private void fetchName() {
    410         mName = mDevice.getAliasName();
    411 
    412         if (TextUtils.isEmpty(mName)) {
    413             mName = mDevice.getAddress();
    414             if (DEBUG) Log.d(TAG, "Device has no name (yet), use address: " + mName);
    415         }
    416     }
    417 
    418     void refresh() {
    419         dispatchAttributesChanged();
    420     }
    421 
    422     boolean isVisible() {
    423         return mVisible;
    424     }
    425 
    426     void setVisible(boolean visible) {
    427         if (mVisible != visible) {
    428             mVisible = visible;
    429             dispatchAttributesChanged();
    430         }
    431     }
    432 
    433     int getBondState() {
    434         return mDevice.getBondState();
    435     }
    436 
    437     void setRssi(short rssi) {
    438         if (mRssi != rssi) {
    439             mRssi = rssi;
    440             dispatchAttributesChanged();
    441         }
    442     }
    443 
    444     /**
    445      * Checks whether we are connected to this device (any profile counts).
    446      *
    447      * @return Whether it is connected.
    448      */
    449     boolean isConnected() {
    450         for (LocalBluetoothProfile profile : mProfiles) {
    451             int status = getProfileConnectionState(profile);
    452             if (status == BluetoothProfile.STATE_CONNECTED) {
    453                 return true;
    454             }
    455         }
    456 
    457         return false;
    458     }
    459 
    460     boolean isConnectedProfile(LocalBluetoothProfile profile) {
    461         int status = getProfileConnectionState(profile);
    462         return status == BluetoothProfile.STATE_CONNECTED;
    463 
    464     }
    465 
    466     boolean isBusy() {
    467         for (LocalBluetoothProfile profile : mProfiles) {
    468             int status = getProfileConnectionState(profile);
    469             if (status == BluetoothProfile.STATE_CONNECTING
    470                     || status == BluetoothProfile.STATE_DISCONNECTING) {
    471                 return true;
    472             }
    473         }
    474         return getBondState() == BluetoothDevice.BOND_BONDING;
    475     }
    476 
    477     /**
    478      * Fetches a new value for the cached BT class.
    479      */
    480     private void fetchBtClass() {
    481         mBtClass = mDevice.getBluetoothClass();
    482     }
    483 
    484     private boolean updateProfiles() {
    485         ParcelUuid[] uuids = mDevice.getUuids();
    486         if (uuids == null) return false;
    487 
    488         ParcelUuid[] localUuids = mLocalAdapter.getUuids();
    489         if (localUuids == null) return false;
    490 
    491         /**
    492          * Now we know if the device supports PBAP, update permissions...
    493          */
    494         processPhonebookAccess();
    495 
    496         mProfileManager.updateProfiles(uuids, localUuids, mProfiles, mRemovedProfiles,
    497                                        mLocalNapRoleConnected, mDevice);
    498 
    499         if (DEBUG) {
    500             Log.e(TAG, "updating profiles for " + mDevice.getAliasName());
    501             BluetoothClass bluetoothClass = mDevice.getBluetoothClass();
    502 
    503             if (bluetoothClass != null) Log.v(TAG, "Class: " + bluetoothClass.toString());
    504             Log.v(TAG, "UUID:");
    505             for (ParcelUuid uuid : uuids) {
    506                 Log.v(TAG, "  " + uuid);
    507             }
    508         }
    509         return true;
    510     }
    511 
    512     /**
    513      * Refreshes the UI for the BT class, including fetching the latest value
    514      * for the class.
    515      */
    516     void refreshBtClass() {
    517         fetchBtClass();
    518         dispatchAttributesChanged();
    519     }
    520 
    521     /**
    522      * Refreshes the UI when framework alerts us of a UUID change.
    523      */
    524     void onUuidChanged() {
    525         updateProfiles();
    526 
    527         if (DEBUG) {
    528             Log.e(TAG, "onUuidChanged: Time since last connect"
    529                     + (SystemClock.elapsedRealtime() - mConnectAttempted));
    530         }
    531 
    532         /*
    533          * If a connect was attempted earlier without any UUID, we will do the
    534          * connect now.
    535          */
    536         if (!mProfiles.isEmpty()
    537                 && (mConnectAttempted + MAX_UUID_DELAY_FOR_AUTO_CONNECT) > SystemClock
    538                         .elapsedRealtime()) {
    539             connectWithoutResettingTimer(false);
    540         }
    541         dispatchAttributesChanged();
    542     }
    543 
    544     void onBondingStateChanged(int bondState) {
    545         if (bondState == BluetoothDevice.BOND_NONE) {
    546             mProfiles.clear();
    547             mConnectAfterPairing = false;  // cancel auto-connect
    548             setPhonebookPermissionChoice(ACCESS_UNKNOWN);
    549             setMessagePermissionChoice(ACCESS_UNKNOWN);
    550             mMessageRejectionCount = 0;
    551             saveMessageRejectionCount();
    552         }
    553 
    554         refresh();
    555 
    556         if (bondState == BluetoothDevice.BOND_BONDED) {
    557             if (mDevice.isBluetoothDock()) {
    558                 onBondingDockConnect();
    559             } else if (mConnectAfterPairing) {
    560                 connect(false);
    561             }
    562             mConnectAfterPairing = false;
    563         }
    564     }
    565 
    566     void setBtClass(BluetoothClass btClass) {
    567         if (btClass != null && mBtClass != btClass) {
    568             mBtClass = btClass;
    569             dispatchAttributesChanged();
    570         }
    571     }
    572 
    573     BluetoothClass getBtClass() {
    574         return mBtClass;
    575     }
    576 
    577     List<LocalBluetoothProfile> getProfiles() {
    578         return Collections.unmodifiableList(mProfiles);
    579     }
    580 
    581     List<LocalBluetoothProfile> getConnectableProfiles() {
    582         List<LocalBluetoothProfile> connectableProfiles =
    583                 new ArrayList<LocalBluetoothProfile>();
    584         for (LocalBluetoothProfile profile : mProfiles) {
    585             if (profile.isConnectable()) {
    586                 connectableProfiles.add(profile);
    587             }
    588         }
    589         return connectableProfiles;
    590     }
    591 
    592     List<LocalBluetoothProfile> getRemovedProfiles() {
    593         return mRemovedProfiles;
    594     }
    595 
    596     void registerCallback(Callback callback) {
    597         synchronized (mCallbacks) {
    598             mCallbacks.add(callback);
    599         }
    600     }
    601 
    602     void unregisterCallback(Callback callback) {
    603         synchronized (mCallbacks) {
    604             mCallbacks.remove(callback);
    605         }
    606     }
    607 
    608     private void dispatchAttributesChanged() {
    609         synchronized (mCallbacks) {
    610             for (Callback callback : mCallbacks) {
    611                 callback.onDeviceAttributesChanged();
    612             }
    613         }
    614     }
    615 
    616     @Override
    617     public String toString() {
    618         return mDevice.toString();
    619     }
    620 
    621     @Override
    622     public boolean equals(Object o) {
    623         if ((o == null) || !(o instanceof CachedBluetoothDevice)) {
    624             return false;
    625         }
    626         return mDevice.equals(((CachedBluetoothDevice) o).mDevice);
    627     }
    628 
    629     @Override
    630     public int hashCode() {
    631         return mDevice.getAddress().hashCode();
    632     }
    633 
    634     // This comparison uses non-final fields so the sort order may change
    635     // when device attributes change (such as bonding state). Settings
    636     // will completely refresh the device list when this happens.
    637     public int compareTo(CachedBluetoothDevice another) {
    638         // Connected above not connected
    639         int comparison = (another.isConnected() ? 1 : 0) - (isConnected() ? 1 : 0);
    640         if (comparison != 0) return comparison;
    641 
    642         // Paired above not paired
    643         comparison = (another.getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0) -
    644             (getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0);
    645         if (comparison != 0) return comparison;
    646 
    647         // Visible above not visible
    648         comparison = (another.mVisible ? 1 : 0) - (mVisible ? 1 : 0);
    649         if (comparison != 0) return comparison;
    650 
    651         // Stronger signal above weaker signal
    652         comparison = another.mRssi - mRssi;
    653         if (comparison != 0) return comparison;
    654 
    655         // Fallback on name
    656         return mName.compareTo(another.mName);
    657     }
    658 
    659     public interface Callback {
    660         void onDeviceAttributesChanged();
    661     }
    662 
    663     int getPhonebookPermissionChoice() {
    664         int permission = mDevice.getPhonebookAccessPermission();
    665         if (permission == BluetoothDevice.ACCESS_ALLOWED) {
    666             return ACCESS_ALLOWED;
    667         } else if (permission == BluetoothDevice.ACCESS_REJECTED) {
    668             return ACCESS_REJECTED;
    669         }
    670         return ACCESS_UNKNOWN;
    671     }
    672 
    673     void setPhonebookPermissionChoice(int permissionChoice) {
    674         int permission = BluetoothDevice.ACCESS_UNKNOWN;
    675         if (permissionChoice == ACCESS_ALLOWED) {
    676             permission = BluetoothDevice.ACCESS_ALLOWED;
    677         } else if (permissionChoice == ACCESS_REJECTED) {
    678             permission = BluetoothDevice.ACCESS_REJECTED;
    679         }
    680         mDevice.setPhonebookAccessPermission(permission);
    681     }
    682 
    683     // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth
    684     // app's shared preferences).
    685     private void migratePhonebookPermissionChoice() {
    686         SharedPreferences preferences = mContext.getSharedPreferences(
    687                 "bluetooth_phonebook_permission", Context.MODE_PRIVATE);
    688         if (!preferences.contains(mDevice.getAddress())) {
    689             return;
    690         }
    691 
    692         if (mDevice.getPhonebookAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) {
    693             int oldPermission = preferences.getInt(mDevice.getAddress(), ACCESS_UNKNOWN);
    694             if (oldPermission == ACCESS_ALLOWED) {
    695                 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
    696             } else if (oldPermission == ACCESS_REJECTED) {
    697                 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED);
    698             }
    699         }
    700 
    701         SharedPreferences.Editor editor = preferences.edit();
    702         editor.remove(mDevice.getAddress());
    703         editor.commit();
    704     }
    705 
    706     int getMessagePermissionChoice() {
    707         int permission = mDevice.getMessageAccessPermission();
    708         if (permission == BluetoothDevice.ACCESS_ALLOWED) {
    709             return ACCESS_ALLOWED;
    710         } else if (permission == BluetoothDevice.ACCESS_REJECTED) {
    711             return ACCESS_REJECTED;
    712         }
    713         return ACCESS_UNKNOWN;
    714     }
    715 
    716     void setMessagePermissionChoice(int permissionChoice) {
    717         int permission = BluetoothDevice.ACCESS_UNKNOWN;
    718         if (permissionChoice == ACCESS_ALLOWED) {
    719             permission = BluetoothDevice.ACCESS_ALLOWED;
    720         } else if (permissionChoice == ACCESS_REJECTED) {
    721             permission = BluetoothDevice.ACCESS_REJECTED;
    722         }
    723         mDevice.setMessageAccessPermission(permission);
    724     }
    725 
    726     // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth
    727     // app's shared preferences).
    728     private void migrateMessagePermissionChoice() {
    729         SharedPreferences preferences = mContext.getSharedPreferences(
    730                 "bluetooth_message_permission", Context.MODE_PRIVATE);
    731         if (!preferences.contains(mDevice.getAddress())) {
    732             return;
    733         }
    734 
    735         if (mDevice.getMessageAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) {
    736             int oldPermission = preferences.getInt(mDevice.getAddress(), ACCESS_UNKNOWN);
    737             if (oldPermission == ACCESS_ALLOWED) {
    738                 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
    739             } else if (oldPermission == ACCESS_REJECTED) {
    740                 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_REJECTED);
    741             }
    742         }
    743 
    744         SharedPreferences.Editor editor = preferences.edit();
    745         editor.remove(mDevice.getAddress());
    746         editor.commit();
    747     }
    748 
    749     /**
    750      * @return Whether this rejection should persist.
    751      */
    752     boolean checkAndIncreaseMessageRejectionCount() {
    753         if (mMessageRejectionCount < MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST) {
    754             mMessageRejectionCount++;
    755             saveMessageRejectionCount();
    756         }
    757         return mMessageRejectionCount >= MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST;
    758     }
    759 
    760     private void fetchMessageRejectionCount() {
    761         SharedPreferences preference = mContext.getSharedPreferences(
    762                 MESSAGE_REJECTION_COUNT_PREFS_NAME, Context.MODE_PRIVATE);
    763         mMessageRejectionCount = preference.getInt(mDevice.getAddress(), 0);
    764     }
    765 
    766     private void saveMessageRejectionCount() {
    767         SharedPreferences.Editor editor = mContext.getSharedPreferences(
    768                 MESSAGE_REJECTION_COUNT_PREFS_NAME, Context.MODE_PRIVATE).edit();
    769         if (mMessageRejectionCount == 0) {
    770             editor.remove(mDevice.getAddress());
    771         } else {
    772             editor.putInt(mDevice.getAddress(), mMessageRejectionCount);
    773         }
    774         editor.commit();
    775     }
    776 
    777     private void processPhonebookAccess() {
    778         if (mDevice.getBondState() != BluetoothDevice.BOND_BONDED) return;
    779 
    780         ParcelUuid[] uuids = mDevice.getUuids();
    781         if (BluetoothUuid.containsAnyUuid(uuids, PbapServerProfile.PBAB_CLIENT_UUIDS)) {
    782             // The pairing dialog now warns of phone-book access for paired devices.
    783             // No separate prompt is displayed after pairing.
    784             setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_ALLOWED);
    785         }
    786     }
    787 }
    788