Home | History | Annotate | Download | only in bluetooth
      1 /*
      2  * Copyright (C) 2008 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.settingslib.bluetooth;
     18 
     19 import android.bluetooth.BluetoothClass;
     20 import android.bluetooth.BluetoothDevice;
     21 import android.bluetooth.BluetoothProfile;
     22 import android.bluetooth.BluetoothUuid;
     23 import android.content.Context;
     24 import android.content.SharedPreferences;
     25 import android.os.ParcelUuid;
     26 import android.os.SystemClock;
     27 import android.text.TextUtils;
     28 import android.util.Log;
     29 import android.bluetooth.BluetoothAdapter;
     30 
     31 import com.android.settingslib.R;
     32 
     33 import java.util.ArrayList;
     34 import java.util.Collection;
     35 import java.util.Collections;
     36 import java.util.HashMap;
     37 import java.util.List;
     38 
     39 /**
     40  * CachedBluetoothDevice represents a remote Bluetooth device. It contains
     41  * attributes of the device (such as the address, name, RSSI, etc.) and
     42  * functionality that can be performed on the device (connect, pair, disconnect,
     43  * etc.).
     44  */
     45 public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> {
     46     private static final String TAG = "CachedBluetoothDevice";
     47     private static final boolean DEBUG = Utils.V;
     48 
     49     private final Context mContext;
     50     private final LocalBluetoothAdapter mLocalAdapter;
     51     private final LocalBluetoothProfileManager mProfileManager;
     52     private final BluetoothDevice mDevice;
     53     private String mName;
     54     private short mRssi;
     55     private BluetoothClass mBtClass;
     56     private HashMap<LocalBluetoothProfile, Integer> mProfileConnectionState;
     57 
     58     private final List<LocalBluetoothProfile> mProfiles =
     59             new ArrayList<LocalBluetoothProfile>();
     60 
     61     // List of profiles that were previously in mProfiles, but have been removed
     62     private final List<LocalBluetoothProfile> mRemovedProfiles =
     63             new ArrayList<LocalBluetoothProfile>();
     64 
     65     // Device supports PANU but not NAP: remove PanProfile after device disconnects from NAP
     66     private boolean mLocalNapRoleConnected;
     67 
     68     private boolean mVisible;
     69 
     70     private int mMessageRejectionCount;
     71 
     72     private final Collection<Callback> mCallbacks = new ArrayList<Callback>();
     73 
     74     // Following constants indicate the user's choices of Phone book/message access settings
     75     // User hasn't made any choice or settings app has wiped out the memory
     76     public final static int ACCESS_UNKNOWN = 0;
     77     // User has accepted the connection and let Settings app remember the decision
     78     public final static int ACCESS_ALLOWED = 1;
     79     // User has rejected the connection and let Settings app remember the decision
     80     public final static int ACCESS_REJECTED = 2;
     81 
     82     // How many times user should reject the connection to make the choice persist.
     83     private final static int MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST = 2;
     84 
     85     private final static String MESSAGE_REJECTION_COUNT_PREFS_NAME = "bluetooth_message_reject";
     86 
     87     /**
     88      * When we connect to multiple profiles, we only want to display a single
     89      * error even if they all fail. This tracks that state.
     90      */
     91     private boolean mIsConnectingErrorPossible;
     92 
     93     /**
     94      * Last time a bt profile auto-connect was attempted.
     95      * If an ACTION_UUID intent comes in within
     96      * MAX_UUID_DELAY_FOR_AUTO_CONNECT milliseconds, we will try auto-connect
     97      * again with the new UUIDs
     98      */
     99     private long mConnectAttempted;
    100 
    101     // See mConnectAttempted
    102     private static final long MAX_UUID_DELAY_FOR_AUTO_CONNECT = 5000;
    103     private static final long MAX_HOGP_DELAY_FOR_AUTO_CONNECT = 30000;
    104 
    105     /**
    106      * Describes the current device and profile for logging.
    107      *
    108      * @param profile Profile to describe
    109      * @return Description of the device and profile
    110      */
    111     private String describe(LocalBluetoothProfile profile) {
    112         StringBuilder sb = new StringBuilder();
    113         sb.append("Address:").append(mDevice);
    114         if (profile != null) {
    115             sb.append(" Profile:").append(profile);
    116         }
    117 
    118         return sb.toString();
    119     }
    120 
    121     void onProfileStateChanged(LocalBluetoothProfile profile, int newProfileState) {
    122         if (Utils.D) {
    123             Log.d(TAG, "onProfileStateChanged: profile " + profile +
    124                     " newProfileState " + newProfileState);
    125         }
    126         if (mLocalAdapter.getBluetoothState() == BluetoothAdapter.STATE_TURNING_OFF)
    127         {
    128             if (Utils.D) Log.d(TAG, " BT Turninig Off...Profile conn state change ignored...");
    129             return;
    130         }
    131         mProfileConnectionState.put(profile, newProfileState);
    132         if (newProfileState == BluetoothProfile.STATE_CONNECTED) {
    133             if (profile instanceof MapProfile) {
    134                 profile.setPreferred(mDevice, true);
    135             } else if (!mProfiles.contains(profile)) {
    136                 mRemovedProfiles.remove(profile);
    137                 mProfiles.add(profile);
    138                 if (profile instanceof PanProfile &&
    139                         ((PanProfile) profile).isLocalRoleNap(mDevice)) {
    140                     // Device doesn't support NAP, so remove PanProfile on disconnect
    141                     mLocalNapRoleConnected = true;
    142                 }
    143             }
    144         } else if (profile instanceof MapProfile &&
    145                 newProfileState == BluetoothProfile.STATE_DISCONNECTED) {
    146             profile.setPreferred(mDevice, false);
    147         } else if (mLocalNapRoleConnected && profile instanceof PanProfile &&
    148                 ((PanProfile) profile).isLocalRoleNap(mDevice) &&
    149                 newProfileState == BluetoothProfile.STATE_DISCONNECTED) {
    150             Log.d(TAG, "Removing PanProfile from device after NAP disconnect");
    151             mProfiles.remove(profile);
    152             mRemovedProfiles.add(profile);
    153             mLocalNapRoleConnected = false;
    154         }
    155     }
    156 
    157     CachedBluetoothDevice(Context context,
    158                           LocalBluetoothAdapter adapter,
    159                           LocalBluetoothProfileManager profileManager,
    160                           BluetoothDevice device) {
    161         mContext = context;
    162         mLocalAdapter = adapter;
    163         mProfileManager = profileManager;
    164         mDevice = device;
    165         mProfileConnectionState = new HashMap<LocalBluetoothProfile, Integer>();
    166         fillData();
    167     }
    168 
    169     public void disconnect() {
    170         for (LocalBluetoothProfile profile : mProfiles) {
    171             disconnect(profile);
    172         }
    173         // Disconnect  PBAP server in case its connected
    174         // This is to ensure all the profiles are disconnected as some CK/Hs do not
    175         // disconnect  PBAP connection when HF connection is brought down
    176         PbapServerProfile PbapProfile = mProfileManager.getPbapProfile();
    177         if (PbapProfile.getConnectionStatus(mDevice) == BluetoothProfile.STATE_CONNECTED)
    178         {
    179             PbapProfile.disconnect(mDevice);
    180         }
    181     }
    182 
    183     public void disconnect(LocalBluetoothProfile profile) {
    184         if (profile.disconnect(mDevice)) {
    185             if (Utils.D) {
    186                 Log.d(TAG, "Command sent successfully:DISCONNECT " + describe(profile));
    187             }
    188         }
    189     }
    190 
    191     public void connect(boolean connectAllProfiles) {
    192         if (!ensurePaired()) {
    193             return;
    194         }
    195 
    196         mConnectAttempted = SystemClock.elapsedRealtime();
    197         connectWithoutResettingTimer(connectAllProfiles);
    198     }
    199 
    200     void onBondingDockConnect() {
    201         // Attempt to connect if UUIDs are available. Otherwise,
    202         // we will connect when the ACTION_UUID intent arrives.
    203         connect(false);
    204     }
    205 
    206     private void connectWithoutResettingTimer(boolean connectAllProfiles) {
    207         // Try to initialize the profiles if they were not.
    208         if (mProfiles.isEmpty()) {
    209             // if mProfiles is empty, then do not invoke updateProfiles. This causes a race
    210             // condition with carkits during pairing, wherein RemoteDevice.UUIDs have been updated
    211             // from bluetooth stack but ACTION.uuid is not sent yet.
    212             // Eventually ACTION.uuid will be received which shall trigger the connection of the
    213             // various profiles
    214             // If UUIDs are not available yet, connect will be happen
    215             // upon arrival of the ACTION_UUID intent.
    216             Log.d(TAG, "No profiles. Maybe we will connect later");
    217             return;
    218         }
    219 
    220         // Reset the only-show-one-error-dialog tracking variable
    221         mIsConnectingErrorPossible = true;
    222 
    223         int preferredProfiles = 0;
    224         for (LocalBluetoothProfile profile : mProfiles) {
    225             if (connectAllProfiles ? profile.isConnectable() : profile.isAutoConnectable()) {
    226                 if (profile.isPreferred(mDevice)) {
    227                     ++preferredProfiles;
    228                     connectInt(profile);
    229                 }
    230             }
    231         }
    232         if (DEBUG) Log.d(TAG, "Preferred profiles = " + preferredProfiles);
    233 
    234         if (preferredProfiles == 0) {
    235             connectAutoConnectableProfiles();
    236         }
    237     }
    238 
    239     private void connectAutoConnectableProfiles() {
    240         if (!ensurePaired()) {
    241             return;
    242         }
    243         // Reset the only-show-one-error-dialog tracking variable
    244         mIsConnectingErrorPossible = true;
    245 
    246         for (LocalBluetoothProfile profile : mProfiles) {
    247             if (profile.isAutoConnectable()) {
    248                 profile.setPreferred(mDevice, true);
    249                 connectInt(profile);
    250             }
    251         }
    252     }
    253 
    254     /**
    255      * Connect this device to the specified profile.
    256      *
    257      * @param profile the profile to use with the remote device
    258      */
    259     public void connectProfile(LocalBluetoothProfile profile) {
    260         mConnectAttempted = SystemClock.elapsedRealtime();
    261         // Reset the only-show-one-error-dialog tracking variable
    262         mIsConnectingErrorPossible = true;
    263         connectInt(profile);
    264         // Refresh the UI based on profile.connect() call
    265         refresh();
    266     }
    267 
    268     synchronized void connectInt(LocalBluetoothProfile profile) {
    269         if (!ensurePaired()) {
    270             return;
    271         }
    272         if (profile.connect(mDevice)) {
    273             if (Utils.D) {
    274                 Log.d(TAG, "Command sent successfully:CONNECT " + describe(profile));
    275             }
    276             return;
    277         }
    278         Log.i(TAG, "Failed to connect " + profile.toString() + " to " + mName);
    279     }
    280 
    281     private boolean ensurePaired() {
    282         if (getBondState() == BluetoothDevice.BOND_NONE) {
    283             startPairing();
    284             return false;
    285         } else {
    286             return true;
    287         }
    288     }
    289 
    290     public boolean startPairing() {
    291         // Pairing is unreliable while scanning, so cancel discovery
    292         if (mLocalAdapter.isDiscovering()) {
    293             mLocalAdapter.cancelDiscovery();
    294         }
    295 
    296         if (!mDevice.createBond()) {
    297             return false;
    298         }
    299 
    300         return true;
    301     }
    302 
    303     /**
    304      * Return true if user initiated pairing on this device. The message text is
    305      * slightly different for local vs. remote initiated pairing dialogs.
    306      */
    307     boolean isUserInitiatedPairing() {
    308         return mDevice.isBondingInitiatedLocally();
    309     }
    310 
    311     public void unpair() {
    312         int state = getBondState();
    313 
    314         if (state == BluetoothDevice.BOND_BONDING) {
    315             mDevice.cancelBondProcess();
    316         }
    317 
    318         if (state != BluetoothDevice.BOND_NONE) {
    319             final BluetoothDevice dev = mDevice;
    320             if (dev != null) {
    321                 final boolean successful = dev.removeBond();
    322                 if (successful) {
    323                     if (Utils.D) {
    324                         Log.d(TAG, "Command sent successfully:REMOVE_BOND " + describe(null));
    325                     }
    326                 } else if (Utils.V) {
    327                     Log.v(TAG, "Framework rejected command immediately:REMOVE_BOND " +
    328                             describe(null));
    329                 }
    330             }
    331         }
    332     }
    333 
    334     public int getProfileConnectionState(LocalBluetoothProfile profile) {
    335         if (mProfileConnectionState == null ||
    336                 mProfileConnectionState.get(profile) == null) {
    337             // If cache is empty make the binder call to get the state
    338             int state = profile.getConnectionStatus(mDevice);
    339             mProfileConnectionState.put(profile, state);
    340         }
    341         return mProfileConnectionState.get(profile);
    342     }
    343 
    344     public void clearProfileConnectionState ()
    345     {
    346         if (Utils.D) {
    347             Log.d(TAG," Clearing all connection state for dev:" + mDevice.getName());
    348         }
    349         for (LocalBluetoothProfile profile :getProfiles()) {
    350             mProfileConnectionState.put(profile, BluetoothProfile.STATE_DISCONNECTED);
    351         }
    352     }
    353 
    354     // TODO: do any of these need to run async on a background thread?
    355     private void fillData() {
    356         fetchName();
    357         fetchBtClass();
    358         updateProfiles();
    359         migratePhonebookPermissionChoice();
    360         migrateMessagePermissionChoice();
    361         fetchMessageRejectionCount();
    362 
    363         mVisible = false;
    364         dispatchAttributesChanged();
    365     }
    366 
    367     public BluetoothDevice getDevice() {
    368         return mDevice;
    369     }
    370 
    371     public String getName() {
    372         return mName;
    373     }
    374 
    375     /**
    376      * Populate name from BluetoothDevice.ACTION_FOUND intent
    377      */
    378     void setNewName(String name) {
    379         if (mName == null) {
    380             mName = name;
    381             if (mName == null || TextUtils.isEmpty(mName)) {
    382                 mName = mDevice.getAddress();
    383             }
    384             dispatchAttributesChanged();
    385         }
    386     }
    387 
    388     /**
    389      * user changes the device name
    390      */
    391     public void setName(String name) {
    392         if (!mName.equals(name)) {
    393             mName = name;
    394             mDevice.setAlias(name);
    395             dispatchAttributesChanged();
    396         }
    397     }
    398 
    399     void refreshName() {
    400         fetchName();
    401         dispatchAttributesChanged();
    402     }
    403 
    404     private void fetchName() {
    405         mName = mDevice.getAliasName();
    406 
    407         if (TextUtils.isEmpty(mName)) {
    408             mName = mDevice.getAddress();
    409             if (DEBUG) Log.d(TAG, "Device has no name (yet), use address: " + mName);
    410         }
    411     }
    412 
    413     void refresh() {
    414         dispatchAttributesChanged();
    415     }
    416 
    417     public boolean isVisible() {
    418         return mVisible;
    419     }
    420 
    421     public void setVisible(boolean visible) {
    422         if (mVisible != visible) {
    423             mVisible = visible;
    424             dispatchAttributesChanged();
    425         }
    426     }
    427 
    428     public int getBondState() {
    429         return mDevice.getBondState();
    430     }
    431 
    432     void setRssi(short rssi) {
    433         if (mRssi != rssi) {
    434             mRssi = rssi;
    435             dispatchAttributesChanged();
    436         }
    437     }
    438 
    439     /**
    440      * Checks whether we are connected to this device (any profile counts).
    441      *
    442      * @return Whether it is connected.
    443      */
    444     public boolean isConnected() {
    445         for (LocalBluetoothProfile profile : mProfiles) {
    446             int status = getProfileConnectionState(profile);
    447             if (status == BluetoothProfile.STATE_CONNECTED) {
    448                 return true;
    449             }
    450         }
    451 
    452         return false;
    453     }
    454 
    455     public boolean isConnectedProfile(LocalBluetoothProfile profile) {
    456         int status = getProfileConnectionState(profile);
    457         return status == BluetoothProfile.STATE_CONNECTED;
    458 
    459     }
    460 
    461     public boolean isBusy() {
    462         for (LocalBluetoothProfile profile : mProfiles) {
    463             int status = getProfileConnectionState(profile);
    464             if (status == BluetoothProfile.STATE_CONNECTING
    465                     || status == BluetoothProfile.STATE_DISCONNECTING) {
    466                 return true;
    467             }
    468         }
    469         return getBondState() == BluetoothDevice.BOND_BONDING;
    470     }
    471 
    472     /**
    473      * Fetches a new value for the cached BT class.
    474      */
    475     private void fetchBtClass() {
    476         mBtClass = mDevice.getBluetoothClass();
    477     }
    478 
    479     private boolean updateProfiles() {
    480         ParcelUuid[] uuids = mDevice.getUuids();
    481         if (uuids == null) return false;
    482 
    483         ParcelUuid[] localUuids = mLocalAdapter.getUuids();
    484         if (localUuids == null) return false;
    485 
    486         /**
    487          * Now we know if the device supports PBAP, update permissions...
    488          */
    489         processPhonebookAccess();
    490 
    491         mProfileManager.updateProfiles(uuids, localUuids, mProfiles, mRemovedProfiles,
    492                                        mLocalNapRoleConnected, mDevice);
    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         ParcelUuid[] uuids = mDevice.getUuids();
    522 
    523         long timeout = MAX_UUID_DELAY_FOR_AUTO_CONNECT;
    524         if (BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.Hogp)) {
    525             timeout = MAX_HOGP_DELAY_FOR_AUTO_CONNECT;
    526         }
    527 
    528         if (DEBUG) {
    529             Log.d(TAG, "onUuidChanged: Time since last connect"
    530                     + (SystemClock.elapsedRealtime() - mConnectAttempted));
    531         }
    532 
    533         /*
    534          * If a connect was attempted earlier without any UUID, we will do the connect now.
    535          * Otherwise, allow the connect on UUID change.
    536          */
    537         if (!mProfiles.isEmpty()
    538                 && ((mConnectAttempted + timeout) > SystemClock.elapsedRealtime())) {
    539             connectWithoutResettingTimer(false);
    540         }
    541 
    542         dispatchAttributesChanged();
    543     }
    544 
    545     void onBondingStateChanged(int bondState) {
    546         if (bondState == BluetoothDevice.BOND_NONE) {
    547             mProfiles.clear();
    548             setPhonebookPermissionChoice(ACCESS_UNKNOWN);
    549             setMessagePermissionChoice(ACCESS_UNKNOWN);
    550             setSimPermissionChoice(ACCESS_UNKNOWN);
    551             mMessageRejectionCount = 0;
    552             saveMessageRejectionCount();
    553         }
    554 
    555         refresh();
    556 
    557         if (bondState == BluetoothDevice.BOND_BONDED) {
    558             if (mDevice.isBluetoothDock()) {
    559                 onBondingDockConnect();
    560             } else if (mDevice.isBondingInitiatedLocally()) {
    561                 connect(false);
    562             }
    563         }
    564     }
    565 
    566     void setBtClass(BluetoothClass btClass) {
    567         if (btClass != null && mBtClass != btClass) {
    568             mBtClass = btClass;
    569             dispatchAttributesChanged();
    570         }
    571     }
    572 
    573     public BluetoothClass getBtClass() {
    574         return mBtClass;
    575     }
    576 
    577     public List<LocalBluetoothProfile> getProfiles() {
    578         return Collections.unmodifiableList(mProfiles);
    579     }
    580 
    581     public 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     public List<LocalBluetoothProfile> getRemovedProfiles() {
    593         return mRemovedProfiles;
    594     }
    595 
    596     public void registerCallback(Callback callback) {
    597         synchronized (mCallbacks) {
    598             mCallbacks.add(callback);
    599         }
    600     }
    601 
    602     public 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     public 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     public 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     public 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     public 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     public int getSimPermissionChoice() {
    727         int permission = mDevice.getSimAccessPermission();
    728         if (permission == BluetoothDevice.ACCESS_ALLOWED) {
    729             return ACCESS_ALLOWED;
    730         } else if (permission == BluetoothDevice.ACCESS_REJECTED) {
    731             return ACCESS_REJECTED;
    732         }
    733         return ACCESS_UNKNOWN;
    734     }
    735 
    736     void setSimPermissionChoice(int permissionChoice) {
    737         int permission = BluetoothDevice.ACCESS_UNKNOWN;
    738         if (permissionChoice == ACCESS_ALLOWED) {
    739             permission = BluetoothDevice.ACCESS_ALLOWED;
    740         } else if (permissionChoice == ACCESS_REJECTED) {
    741             permission = BluetoothDevice.ACCESS_REJECTED;
    742         }
    743         mDevice.setSimAccessPermission(permission);
    744     }
    745 
    746     // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth
    747     // app's shared preferences).
    748     private void migrateMessagePermissionChoice() {
    749         SharedPreferences preferences = mContext.getSharedPreferences(
    750                 "bluetooth_message_permission", Context.MODE_PRIVATE);
    751         if (!preferences.contains(mDevice.getAddress())) {
    752             return;
    753         }
    754 
    755         if (mDevice.getMessageAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) {
    756             int oldPermission = preferences.getInt(mDevice.getAddress(), ACCESS_UNKNOWN);
    757             if (oldPermission == ACCESS_ALLOWED) {
    758                 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
    759             } else if (oldPermission == ACCESS_REJECTED) {
    760                 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_REJECTED);
    761             }
    762         }
    763 
    764         SharedPreferences.Editor editor = preferences.edit();
    765         editor.remove(mDevice.getAddress());
    766         editor.commit();
    767     }
    768 
    769     /**
    770      * @return Whether this rejection should persist.
    771      */
    772     public boolean checkAndIncreaseMessageRejectionCount() {
    773         if (mMessageRejectionCount < MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST) {
    774             mMessageRejectionCount++;
    775             saveMessageRejectionCount();
    776         }
    777         return mMessageRejectionCount >= MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST;
    778     }
    779 
    780     private void fetchMessageRejectionCount() {
    781         SharedPreferences preference = mContext.getSharedPreferences(
    782                 MESSAGE_REJECTION_COUNT_PREFS_NAME, Context.MODE_PRIVATE);
    783         mMessageRejectionCount = preference.getInt(mDevice.getAddress(), 0);
    784     }
    785 
    786     private void saveMessageRejectionCount() {
    787         SharedPreferences.Editor editor = mContext.getSharedPreferences(
    788                 MESSAGE_REJECTION_COUNT_PREFS_NAME, Context.MODE_PRIVATE).edit();
    789         if (mMessageRejectionCount == 0) {
    790             editor.remove(mDevice.getAddress());
    791         } else {
    792             editor.putInt(mDevice.getAddress(), mMessageRejectionCount);
    793         }
    794         editor.commit();
    795     }
    796 
    797     private void processPhonebookAccess() {
    798         if (mDevice.getBondState() != BluetoothDevice.BOND_BONDED) return;
    799 
    800         ParcelUuid[] uuids = mDevice.getUuids();
    801         if (BluetoothUuid.containsAnyUuid(uuids, PbapServerProfile.PBAB_CLIENT_UUIDS)) {
    802             // The pairing dialog now warns of phone-book access for paired devices.
    803             // No separate prompt is displayed after pairing.
    804             if (getPhonebookPermissionChoice() == CachedBluetoothDevice.ACCESS_UNKNOWN) {
    805                 if (mDevice.getBluetoothClass().getDeviceClass()
    806                         == BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE ||
    807                     mDevice.getBluetoothClass().getDeviceClass()
    808                         == BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET) {
    809                     setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_ALLOWED);
    810                 } else {
    811                     setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_REJECTED);
    812                 }
    813             }
    814         }
    815     }
    816 
    817     public int getMaxConnectionState() {
    818         int maxState = BluetoothProfile.STATE_DISCONNECTED;
    819         for (LocalBluetoothProfile profile : getProfiles()) {
    820             int connectionStatus = getProfileConnectionState(profile);
    821             if (connectionStatus > maxState) {
    822                 maxState = connectionStatus;
    823             }
    824         }
    825         return maxState;
    826     }
    827 
    828     /**
    829      * @return resource for string that discribes the connection state of this device.
    830      */
    831     public int getConnectionSummary() {
    832         boolean profileConnected = false;       // at least one profile is connected
    833         boolean a2dpNotConnected = false;       // A2DP is preferred but not connected
    834         boolean hfpNotConnected = false;    // HFP is preferred but not connected
    835 
    836         for (LocalBluetoothProfile profile : getProfiles()) {
    837             int connectionStatus = getProfileConnectionState(profile);
    838 
    839             switch (connectionStatus) {
    840                 case BluetoothProfile.STATE_CONNECTING:
    841                 case BluetoothProfile.STATE_DISCONNECTING:
    842                     return Utils.getConnectionStateSummary(connectionStatus);
    843 
    844                 case BluetoothProfile.STATE_CONNECTED:
    845                     profileConnected = true;
    846                     break;
    847 
    848                 case BluetoothProfile.STATE_DISCONNECTED:
    849                     if (profile.isProfileReady()) {
    850                         if ((profile instanceof A2dpProfile) ||
    851                             (profile instanceof A2dpSinkProfile)){
    852                             a2dpNotConnected = true;
    853                         } else if ((profile instanceof HeadsetProfile) ||
    854                                    (profile instanceof HfpClientProfile)) {
    855                             hfpNotConnected = true;
    856                         }
    857                     }
    858                     break;
    859             }
    860         }
    861 
    862         if (profileConnected) {
    863             if (a2dpNotConnected && hfpNotConnected) {
    864                 return R.string.bluetooth_connected_no_headset_no_a2dp;
    865             } else if (a2dpNotConnected) {
    866                 return R.string.bluetooth_connected_no_a2dp;
    867             } else if (hfpNotConnected) {
    868                 return R.string.bluetooth_connected_no_headset;
    869             } else {
    870                 return R.string.bluetooth_connected;
    871             }
    872         }
    873 
    874         return getBondState() == BluetoothDevice.BOND_BONDING ? R.string.bluetooth_pairing : 0;
    875     }
    876 }
    877