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.BluetoothHearingAid;
     22 import android.bluetooth.BluetoothProfile;
     23 import android.bluetooth.BluetoothUuid;
     24 import android.content.Context;
     25 import android.content.SharedPreferences;
     26 import android.media.AudioManager;
     27 import android.os.ParcelUuid;
     28 import android.os.SystemClock;
     29 import android.text.TextUtils;
     30 import android.util.Log;
     31 import android.bluetooth.BluetoothAdapter;
     32 import android.support.annotation.VisibleForTesting;
     33 
     34 import com.android.settingslib.R;
     35 
     36 import java.util.ArrayList;
     37 import java.util.Collection;
     38 import java.util.Collections;
     39 import java.util.HashMap;
     40 import java.util.List;
     41 
     42 /**
     43  * CachedBluetoothDevice represents a remote Bluetooth device. It contains
     44  * attributes of the device (such as the address, name, RSSI, etc.) and
     45  * functionality that can be performed on the device (connect, pair, disconnect,
     46  * etc.).
     47  */
     48 public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> {
     49     private static final String TAG = "CachedBluetoothDevice";
     50     private static final boolean DEBUG = Utils.V;
     51 
     52     private final Context mContext;
     53     private final LocalBluetoothAdapter mLocalAdapter;
     54     private final LocalBluetoothProfileManager mProfileManager;
     55     private final AudioManager mAudioManager;
     56     private final BluetoothDevice mDevice;
     57     //TODO: consider remove, BluetoothDevice.getName() is already cached
     58     private String mName;
     59     private long mHiSyncId;
     60     // Need this since there is no method for getting RSSI
     61     private short mRssi;
     62     //TODO: consider remove, BluetoothDevice.getBluetoothClass() is already cached
     63     private BluetoothClass mBtClass;
     64     private HashMap<LocalBluetoothProfile, Integer> mProfileConnectionState;
     65 
     66     private final List<LocalBluetoothProfile> mProfiles =
     67             new ArrayList<LocalBluetoothProfile>();
     68 
     69     // List of profiles that were previously in mProfiles, but have been removed
     70     private final List<LocalBluetoothProfile> mRemovedProfiles =
     71             new ArrayList<LocalBluetoothProfile>();
     72 
     73     // Device supports PANU but not NAP: remove PanProfile after device disconnects from NAP
     74     private boolean mLocalNapRoleConnected;
     75 
     76     private boolean mJustDiscovered;
     77 
     78     private int mMessageRejectionCount;
     79 
     80     private final Collection<Callback> mCallbacks = new ArrayList<Callback>();
     81 
     82     // Following constants indicate the user's choices of Phone book/message access settings
     83     // User hasn't made any choice or settings app has wiped out the memory
     84     public final static int ACCESS_UNKNOWN = 0;
     85     // User has accepted the connection and let Settings app remember the decision
     86     public final static int ACCESS_ALLOWED = 1;
     87     // User has rejected the connection and let Settings app remember the decision
     88     public final static int ACCESS_REJECTED = 2;
     89 
     90     // How many times user should reject the connection to make the choice persist.
     91     private final static int MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST = 2;
     92 
     93     private final static String MESSAGE_REJECTION_COUNT_PREFS_NAME = "bluetooth_message_reject";
     94 
     95     /**
     96      * When we connect to multiple profiles, we only want to display a single
     97      * error even if they all fail. This tracks that state.
     98      */
     99     private boolean mIsConnectingErrorPossible;
    100 
    101     public long getHiSyncId() {
    102         return mHiSyncId;
    103     }
    104 
    105     public void setHiSyncId(long id) {
    106         if (Utils.D) {
    107             Log.d(TAG, "setHiSyncId: mDevice " + mDevice + ", id " + id);
    108         }
    109         mHiSyncId = id;
    110     }
    111 
    112     /**
    113      * Last time a bt profile auto-connect was attempted.
    114      * If an ACTION_UUID intent comes in within
    115      * MAX_UUID_DELAY_FOR_AUTO_CONNECT milliseconds, we will try auto-connect
    116      * again with the new UUIDs
    117      */
    118     private long mConnectAttempted;
    119 
    120     // See mConnectAttempted
    121     private static final long MAX_UUID_DELAY_FOR_AUTO_CONNECT = 5000;
    122     private static final long MAX_HOGP_DELAY_FOR_AUTO_CONNECT = 30000;
    123 
    124     // Active device state
    125     private boolean mIsActiveDeviceA2dp = false;
    126     private boolean mIsActiveDeviceHeadset = false;
    127     private boolean mIsActiveDeviceHearingAid = false;
    128     /**
    129      * Describes the current device and profile for logging.
    130      *
    131      * @param profile Profile to describe
    132      * @return Description of the device and profile
    133      */
    134     private String describe(LocalBluetoothProfile profile) {
    135         StringBuilder sb = new StringBuilder();
    136         sb.append("Address:").append(mDevice);
    137         if (profile != null) {
    138             sb.append(" Profile:").append(profile);
    139         }
    140 
    141         return sb.toString();
    142     }
    143 
    144     void onProfileStateChanged(LocalBluetoothProfile profile, int newProfileState) {
    145         if (Utils.D) {
    146             Log.d(TAG, "onProfileStateChanged: profile " + profile +
    147                     " newProfileState " + newProfileState);
    148         }
    149         if (mLocalAdapter.getBluetoothState() == BluetoothAdapter.STATE_TURNING_OFF)
    150         {
    151             if (Utils.D) Log.d(TAG, " BT Turninig Off...Profile conn state change ignored...");
    152             return;
    153         }
    154         mProfileConnectionState.put(profile, newProfileState);
    155         if (newProfileState == BluetoothProfile.STATE_CONNECTED) {
    156             if (profile instanceof MapProfile) {
    157                 profile.setPreferred(mDevice, true);
    158             }
    159             if (!mProfiles.contains(profile)) {
    160                 mRemovedProfiles.remove(profile);
    161                 mProfiles.add(profile);
    162                 if (profile instanceof PanProfile &&
    163                         ((PanProfile) profile).isLocalRoleNap(mDevice)) {
    164                     // Device doesn't support NAP, so remove PanProfile on disconnect
    165                     mLocalNapRoleConnected = true;
    166                 }
    167             }
    168         } else if (profile instanceof MapProfile &&
    169                 newProfileState == BluetoothProfile.STATE_DISCONNECTED) {
    170             profile.setPreferred(mDevice, false);
    171         } else if (mLocalNapRoleConnected && profile instanceof PanProfile &&
    172                 ((PanProfile) profile).isLocalRoleNap(mDevice) &&
    173                 newProfileState == BluetoothProfile.STATE_DISCONNECTED) {
    174             Log.d(TAG, "Removing PanProfile from device after NAP disconnect");
    175             mProfiles.remove(profile);
    176             mRemovedProfiles.add(profile);
    177             mLocalNapRoleConnected = false;
    178         }
    179         fetchActiveDevices();
    180     }
    181 
    182     CachedBluetoothDevice(Context context,
    183                           LocalBluetoothAdapter adapter,
    184                           LocalBluetoothProfileManager profileManager,
    185                           BluetoothDevice device) {
    186         mContext = context;
    187         mLocalAdapter = adapter;
    188         mProfileManager = profileManager;
    189         mAudioManager = context.getSystemService(AudioManager.class);
    190         mDevice = device;
    191         mProfileConnectionState = new HashMap<LocalBluetoothProfile, Integer>();
    192         fillData();
    193         mHiSyncId = BluetoothHearingAid.HI_SYNC_ID_INVALID;
    194     }
    195 
    196     public void disconnect() {
    197         for (LocalBluetoothProfile profile : mProfiles) {
    198             disconnect(profile);
    199         }
    200         // Disconnect  PBAP server in case its connected
    201         // This is to ensure all the profiles are disconnected as some CK/Hs do not
    202         // disconnect  PBAP connection when HF connection is brought down
    203         PbapServerProfile PbapProfile = mProfileManager.getPbapProfile();
    204         if (PbapProfile.getConnectionStatus(mDevice) == BluetoothProfile.STATE_CONNECTED)
    205         {
    206             PbapProfile.disconnect(mDevice);
    207         }
    208     }
    209 
    210     public void disconnect(LocalBluetoothProfile profile) {
    211         if (profile.disconnect(mDevice)) {
    212             if (Utils.D) {
    213                 Log.d(TAG, "Command sent successfully:DISCONNECT " + describe(profile));
    214             }
    215         }
    216     }
    217 
    218     public void connect(boolean connectAllProfiles) {
    219         if (!ensurePaired()) {
    220             return;
    221         }
    222 
    223         mConnectAttempted = SystemClock.elapsedRealtime();
    224         connectWithoutResettingTimer(connectAllProfiles);
    225     }
    226 
    227     void onBondingDockConnect() {
    228         // Attempt to connect if UUIDs are available. Otherwise,
    229         // we will connect when the ACTION_UUID intent arrives.
    230         connect(false);
    231     }
    232 
    233     private void connectWithoutResettingTimer(boolean connectAllProfiles) {
    234         // Try to initialize the profiles if they were not.
    235         if (mProfiles.isEmpty()) {
    236             // if mProfiles is empty, then do not invoke updateProfiles. This causes a race
    237             // condition with carkits during pairing, wherein RemoteDevice.UUIDs have been updated
    238             // from bluetooth stack but ACTION.uuid is not sent yet.
    239             // Eventually ACTION.uuid will be received which shall trigger the connection of the
    240             // various profiles
    241             // If UUIDs are not available yet, connect will be happen
    242             // upon arrival of the ACTION_UUID intent.
    243             Log.d(TAG, "No profiles. Maybe we will connect later");
    244             return;
    245         }
    246 
    247         // Reset the only-show-one-error-dialog tracking variable
    248         mIsConnectingErrorPossible = true;
    249 
    250         int preferredProfiles = 0;
    251         for (LocalBluetoothProfile profile : mProfiles) {
    252             if (connectAllProfiles ? profile.isConnectable() : profile.isAutoConnectable()) {
    253                 if (profile.isPreferred(mDevice)) {
    254                     ++preferredProfiles;
    255                     connectInt(profile);
    256                 }
    257             }
    258         }
    259         if (DEBUG) Log.d(TAG, "Preferred profiles = " + preferredProfiles);
    260 
    261         if (preferredProfiles == 0) {
    262             connectAutoConnectableProfiles();
    263         }
    264     }
    265 
    266     private void connectAutoConnectableProfiles() {
    267         if (!ensurePaired()) {
    268             return;
    269         }
    270         // Reset the only-show-one-error-dialog tracking variable
    271         mIsConnectingErrorPossible = true;
    272 
    273         for (LocalBluetoothProfile profile : mProfiles) {
    274             if (profile.isAutoConnectable()) {
    275                 profile.setPreferred(mDevice, true);
    276                 connectInt(profile);
    277             }
    278         }
    279     }
    280 
    281     /**
    282      * Connect this device to the specified profile.
    283      *
    284      * @param profile the profile to use with the remote device
    285      */
    286     public void connectProfile(LocalBluetoothProfile profile) {
    287         mConnectAttempted = SystemClock.elapsedRealtime();
    288         // Reset the only-show-one-error-dialog tracking variable
    289         mIsConnectingErrorPossible = true;
    290         connectInt(profile);
    291         // Refresh the UI based on profile.connect() call
    292         refresh();
    293     }
    294 
    295     synchronized void connectInt(LocalBluetoothProfile profile) {
    296         if (!ensurePaired()) {
    297             return;
    298         }
    299         if (profile.connect(mDevice)) {
    300             if (Utils.D) {
    301                 Log.d(TAG, "Command sent successfully:CONNECT " + describe(profile));
    302             }
    303             return;
    304         }
    305         Log.i(TAG, "Failed to connect " + profile.toString() + " to " + mName);
    306     }
    307 
    308     private boolean ensurePaired() {
    309         if (getBondState() == BluetoothDevice.BOND_NONE) {
    310             startPairing();
    311             return false;
    312         } else {
    313             return true;
    314         }
    315     }
    316 
    317     public boolean startPairing() {
    318         // Pairing is unreliable while scanning, so cancel discovery
    319         if (mLocalAdapter.isDiscovering()) {
    320             mLocalAdapter.cancelDiscovery();
    321         }
    322 
    323         if (!mDevice.createBond()) {
    324             return false;
    325         }
    326 
    327         return true;
    328     }
    329 
    330     /**
    331      * Return true if user initiated pairing on this device. The message text is
    332      * slightly different for local vs. remote initiated pairing dialogs.
    333      */
    334     boolean isUserInitiatedPairing() {
    335         return mDevice.isBondingInitiatedLocally();
    336     }
    337 
    338     public void unpair() {
    339         int state = getBondState();
    340 
    341         if (state == BluetoothDevice.BOND_BONDING) {
    342             mDevice.cancelBondProcess();
    343         }
    344 
    345         if (state != BluetoothDevice.BOND_NONE) {
    346             final BluetoothDevice dev = mDevice;
    347             if (dev != null) {
    348                 final boolean successful = dev.removeBond();
    349                 if (successful) {
    350                     if (Utils.D) {
    351                         Log.d(TAG, "Command sent successfully:REMOVE_BOND " + describe(null));
    352                     }
    353                 } else if (Utils.V) {
    354                     Log.v(TAG, "Framework rejected command immediately:REMOVE_BOND " +
    355                         describe(null));
    356                 }
    357             }
    358         }
    359     }
    360 
    361     public int getProfileConnectionState(LocalBluetoothProfile profile) {
    362         if (mProfileConnectionState.get(profile) == null) {
    363             // If cache is empty make the binder call to get the state
    364             int state = profile.getConnectionStatus(mDevice);
    365             mProfileConnectionState.put(profile, state);
    366         }
    367         return mProfileConnectionState.get(profile);
    368     }
    369 
    370     public void clearProfileConnectionState ()
    371     {
    372         if (Utils.D) {
    373             Log.d(TAG," Clearing all connection state for dev:" + mDevice.getName());
    374         }
    375         for (LocalBluetoothProfile profile :getProfiles()) {
    376             mProfileConnectionState.put(profile, BluetoothProfile.STATE_DISCONNECTED);
    377         }
    378     }
    379 
    380     // TODO: do any of these need to run async on a background thread?
    381     private void fillData() {
    382         fetchName();
    383         fetchBtClass();
    384         updateProfiles();
    385         fetchActiveDevices();
    386         migratePhonebookPermissionChoice();
    387         migrateMessagePermissionChoice();
    388         fetchMessageRejectionCount();
    389 
    390         dispatchAttributesChanged();
    391     }
    392 
    393     public BluetoothDevice getDevice() {
    394         return mDevice;
    395     }
    396 
    397     /**
    398      * Convenience method that can be mocked - it lets tests avoid having to call getDevice() which
    399      * causes problems in tests since BluetoothDevice is final and cannot be mocked.
    400      * @return the address of this device
    401      */
    402     public String getAddress() {
    403         return mDevice.getAddress();
    404     }
    405 
    406     public String getName() {
    407         return mName;
    408     }
    409 
    410     /**
    411      * Populate name from BluetoothDevice.ACTION_FOUND intent
    412      */
    413     void setNewName(String name) {
    414         if (mName == null) {
    415             mName = name;
    416             if (mName == null || TextUtils.isEmpty(mName)) {
    417                 mName = mDevice.getAddress();
    418             }
    419             dispatchAttributesChanged();
    420         }
    421     }
    422 
    423     /**
    424      * User changes the device name
    425      * @param name new alias name to be set, should never be null
    426      */
    427     public void setName(String name) {
    428         // Prevent mName to be set to null if setName(null) is called
    429         if (name != null && !TextUtils.equals(name, mName)) {
    430             mName = name;
    431             mDevice.setAlias(name);
    432             dispatchAttributesChanged();
    433         }
    434     }
    435 
    436     /**
    437      * Set this device as active device
    438      * @return true if at least one profile on this device is set to active, false otherwise
    439      */
    440     public boolean setActive() {
    441         boolean result = false;
    442         A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile();
    443         if (a2dpProfile != null && isConnectedProfile(a2dpProfile)) {
    444             if (a2dpProfile.setActiveDevice(getDevice())) {
    445                 Log.i(TAG, "OnPreferenceClickListener: A2DP active device=" + this);
    446                 result = true;
    447             }
    448         }
    449         HeadsetProfile headsetProfile = mProfileManager.getHeadsetProfile();
    450         if ((headsetProfile != null) && isConnectedProfile(headsetProfile)) {
    451             if (headsetProfile.setActiveDevice(getDevice())) {
    452                 Log.i(TAG, "OnPreferenceClickListener: Headset active device=" + this);
    453                 result = true;
    454             }
    455         }
    456         HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile();
    457         if ((hearingAidProfile != null) && isConnectedProfile(hearingAidProfile)) {
    458             if (hearingAidProfile.setActiveDevice(getDevice())) {
    459                 Log.i(TAG, "OnPreferenceClickListener: Hearing Aid active device=" + this);
    460                 result = true;
    461             }
    462         }
    463         return result;
    464     }
    465 
    466     void refreshName() {
    467         fetchName();
    468         dispatchAttributesChanged();
    469     }
    470 
    471     private void fetchName() {
    472         mName = mDevice.getAliasName();
    473 
    474         if (TextUtils.isEmpty(mName)) {
    475             mName = mDevice.getAddress();
    476             if (DEBUG) Log.d(TAG, "Device has no name (yet), use address: " + mName);
    477         }
    478     }
    479 
    480     /**
    481      * Checks if device has a human readable name besides MAC address
    482      * @return true if device's alias name is not null nor empty, false otherwise
    483      */
    484     public boolean hasHumanReadableName() {
    485         return !TextUtils.isEmpty(mDevice.getAliasName());
    486     }
    487 
    488     /**
    489      * Get battery level from remote device
    490      * @return battery level in percentage [0-100], or {@link BluetoothDevice#BATTERY_LEVEL_UNKNOWN}
    491      */
    492     public int getBatteryLevel() {
    493         return mDevice.getBatteryLevel();
    494     }
    495 
    496     void refresh() {
    497         dispatchAttributesChanged();
    498     }
    499 
    500     public void setJustDiscovered(boolean justDiscovered) {
    501         if (mJustDiscovered != justDiscovered) {
    502             mJustDiscovered = justDiscovered;
    503             dispatchAttributesChanged();
    504         }
    505     }
    506 
    507     public int getBondState() {
    508         return mDevice.getBondState();
    509     }
    510 
    511     /**
    512      * Update the device status as active or non-active per Bluetooth profile.
    513      *
    514      * @param isActive true if the device is active
    515      * @param bluetoothProfile the Bluetooth profile
    516      */
    517     public void onActiveDeviceChanged(boolean isActive, int bluetoothProfile) {
    518         boolean changed = false;
    519         switch (bluetoothProfile) {
    520         case BluetoothProfile.A2DP:
    521             changed = (mIsActiveDeviceA2dp != isActive);
    522             mIsActiveDeviceA2dp = isActive;
    523             break;
    524         case BluetoothProfile.HEADSET:
    525             changed = (mIsActiveDeviceHeadset != isActive);
    526             mIsActiveDeviceHeadset = isActive;
    527             break;
    528         case BluetoothProfile.HEARING_AID:
    529             changed = (mIsActiveDeviceHearingAid != isActive);
    530             mIsActiveDeviceHearingAid = isActive;
    531             break;
    532         default:
    533             Log.w(TAG, "onActiveDeviceChanged: unknown profile " + bluetoothProfile +
    534                     " isActive " + isActive);
    535             break;
    536         }
    537         if (changed) {
    538             dispatchAttributesChanged();
    539         }
    540     }
    541 
    542     /**
    543      * Update the profile audio state.
    544      */
    545     void onAudioModeChanged() {
    546         dispatchAttributesChanged();
    547     }
    548     /**
    549      * Get the device status as active or non-active per Bluetooth profile.
    550      *
    551      * @param bluetoothProfile the Bluetooth profile
    552      * @return true if the device is active
    553      */
    554     @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
    555     public boolean isActiveDevice(int bluetoothProfile) {
    556         switch (bluetoothProfile) {
    557             case BluetoothProfile.A2DP:
    558                 return mIsActiveDeviceA2dp;
    559             case BluetoothProfile.HEADSET:
    560                 return mIsActiveDeviceHeadset;
    561             case BluetoothProfile.HEARING_AID:
    562                 return mIsActiveDeviceHearingAid;
    563             default:
    564                 Log.w(TAG, "getActiveDevice: unknown profile " + bluetoothProfile);
    565                 break;
    566         }
    567         return false;
    568     }
    569 
    570     void setRssi(short rssi) {
    571         if (mRssi != rssi) {
    572             mRssi = rssi;
    573             dispatchAttributesChanged();
    574         }
    575     }
    576 
    577     /**
    578      * Checks whether we are connected to this device (any profile counts).
    579      *
    580      * @return Whether it is connected.
    581      */
    582     public boolean isConnected() {
    583         for (LocalBluetoothProfile profile : mProfiles) {
    584             int status = getProfileConnectionState(profile);
    585             if (status == BluetoothProfile.STATE_CONNECTED) {
    586                 return true;
    587             }
    588         }
    589 
    590         return false;
    591     }
    592 
    593     public boolean isConnectedProfile(LocalBluetoothProfile profile) {
    594         int status = getProfileConnectionState(profile);
    595         return status == BluetoothProfile.STATE_CONNECTED;
    596 
    597     }
    598 
    599     public boolean isBusy() {
    600         for (LocalBluetoothProfile profile : mProfiles) {
    601             int status = getProfileConnectionState(profile);
    602             if (status == BluetoothProfile.STATE_CONNECTING
    603                     || status == BluetoothProfile.STATE_DISCONNECTING) {
    604                 return true;
    605             }
    606         }
    607         return getBondState() == BluetoothDevice.BOND_BONDING;
    608     }
    609 
    610     /**
    611      * Fetches a new value for the cached BT class.
    612      */
    613     private void fetchBtClass() {
    614         mBtClass = mDevice.getBluetoothClass();
    615     }
    616 
    617     private boolean updateProfiles() {
    618         ParcelUuid[] uuids = mDevice.getUuids();
    619         if (uuids == null) return false;
    620 
    621         ParcelUuid[] localUuids = mLocalAdapter.getUuids();
    622         if (localUuids == null) return false;
    623 
    624         /*
    625          * Now we know if the device supports PBAP, update permissions...
    626          */
    627         processPhonebookAccess();
    628 
    629         mProfileManager.updateProfiles(uuids, localUuids, mProfiles, mRemovedProfiles,
    630                                        mLocalNapRoleConnected, mDevice);
    631 
    632         if (DEBUG) {
    633             Log.e(TAG, "updating profiles for " + mDevice.getAliasName());
    634             BluetoothClass bluetoothClass = mDevice.getBluetoothClass();
    635 
    636             if (bluetoothClass != null) Log.v(TAG, "Class: " + bluetoothClass.toString());
    637             Log.v(TAG, "UUID:");
    638             for (ParcelUuid uuid : uuids) {
    639                 Log.v(TAG, "  " + uuid);
    640             }
    641         }
    642         return true;
    643     }
    644 
    645     private void fetchActiveDevices() {
    646         A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile();
    647         if (a2dpProfile != null) {
    648             mIsActiveDeviceA2dp = mDevice.equals(a2dpProfile.getActiveDevice());
    649         }
    650         HeadsetProfile headsetProfile = mProfileManager.getHeadsetProfile();
    651         if (headsetProfile != null) {
    652             mIsActiveDeviceHeadset = mDevice.equals(headsetProfile.getActiveDevice());
    653         }
    654         HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile();
    655         if (hearingAidProfile != null) {
    656             mIsActiveDeviceHearingAid = hearingAidProfile.getActiveDevices().contains(mDevice);
    657         }
    658     }
    659 
    660     /**
    661      * Refreshes the UI for the BT class, including fetching the latest value
    662      * for the class.
    663      */
    664     void refreshBtClass() {
    665         fetchBtClass();
    666         dispatchAttributesChanged();
    667     }
    668 
    669     /**
    670      * Refreshes the UI when framework alerts us of a UUID change.
    671      */
    672     void onUuidChanged() {
    673         updateProfiles();
    674         ParcelUuid[] uuids = mDevice.getUuids();
    675 
    676         long timeout = MAX_UUID_DELAY_FOR_AUTO_CONNECT;
    677         if (BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.Hogp)) {
    678             timeout = MAX_HOGP_DELAY_FOR_AUTO_CONNECT;
    679         }
    680 
    681         if (DEBUG) {
    682             Log.d(TAG, "onUuidChanged: Time since last connect"
    683                     + (SystemClock.elapsedRealtime() - mConnectAttempted));
    684         }
    685 
    686         /*
    687          * If a connect was attempted earlier without any UUID, we will do the connect now.
    688          * Otherwise, allow the connect on UUID change.
    689          */
    690         if (!mProfiles.isEmpty()
    691                 && ((mConnectAttempted + timeout) > SystemClock.elapsedRealtime())) {
    692             connectWithoutResettingTimer(false);
    693         }
    694 
    695         dispatchAttributesChanged();
    696     }
    697 
    698     void onBondingStateChanged(int bondState) {
    699         if (bondState == BluetoothDevice.BOND_NONE) {
    700             mProfiles.clear();
    701             setPhonebookPermissionChoice(ACCESS_UNKNOWN);
    702             setMessagePermissionChoice(ACCESS_UNKNOWN);
    703             setSimPermissionChoice(ACCESS_UNKNOWN);
    704             mMessageRejectionCount = 0;
    705             saveMessageRejectionCount();
    706         }
    707 
    708         refresh();
    709 
    710         if (bondState == BluetoothDevice.BOND_BONDED) {
    711             if (mDevice.isBluetoothDock()) {
    712                 onBondingDockConnect();
    713             } else if (mDevice.isBondingInitiatedLocally()) {
    714                 connect(false);
    715             }
    716         }
    717     }
    718 
    719     void setBtClass(BluetoothClass btClass) {
    720         if (btClass != null && mBtClass != btClass) {
    721             mBtClass = btClass;
    722             dispatchAttributesChanged();
    723         }
    724     }
    725 
    726     public BluetoothClass getBtClass() {
    727         return mBtClass;
    728     }
    729 
    730     public List<LocalBluetoothProfile> getProfiles() {
    731         return Collections.unmodifiableList(mProfiles);
    732     }
    733 
    734     public List<LocalBluetoothProfile> getConnectableProfiles() {
    735         List<LocalBluetoothProfile> connectableProfiles =
    736                 new ArrayList<LocalBluetoothProfile>();
    737         for (LocalBluetoothProfile profile : mProfiles) {
    738             if (profile.isConnectable()) {
    739                 connectableProfiles.add(profile);
    740             }
    741         }
    742         return connectableProfiles;
    743     }
    744 
    745     public List<LocalBluetoothProfile> getRemovedProfiles() {
    746         return mRemovedProfiles;
    747     }
    748 
    749     public void registerCallback(Callback callback) {
    750         synchronized (mCallbacks) {
    751             mCallbacks.add(callback);
    752         }
    753     }
    754 
    755     public void unregisterCallback(Callback callback) {
    756         synchronized (mCallbacks) {
    757             mCallbacks.remove(callback);
    758         }
    759     }
    760 
    761     private void dispatchAttributesChanged() {
    762         synchronized (mCallbacks) {
    763             for (Callback callback : mCallbacks) {
    764                 callback.onDeviceAttributesChanged();
    765             }
    766         }
    767     }
    768 
    769     @Override
    770     public String toString() {
    771         return mDevice.toString();
    772     }
    773 
    774     @Override
    775     public boolean equals(Object o) {
    776         if ((o == null) || !(o instanceof CachedBluetoothDevice)) {
    777             return false;
    778         }
    779         return mDevice.equals(((CachedBluetoothDevice) o).mDevice);
    780     }
    781 
    782     @Override
    783     public int hashCode() {
    784         return mDevice.getAddress().hashCode();
    785     }
    786 
    787     // This comparison uses non-final fields so the sort order may change
    788     // when device attributes change (such as bonding state). Settings
    789     // will completely refresh the device list when this happens.
    790     public int compareTo(CachedBluetoothDevice another) {
    791         // Connected above not connected
    792         int comparison = (another.isConnected() ? 1 : 0) - (isConnected() ? 1 : 0);
    793         if (comparison != 0) return comparison;
    794 
    795         // Paired above not paired
    796         comparison = (another.getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0) -
    797             (getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0);
    798         if (comparison != 0) return comparison;
    799 
    800         // Just discovered above discovered in the past
    801         comparison = (another.mJustDiscovered ? 1 : 0) - (mJustDiscovered ? 1 : 0);
    802         if (comparison != 0) return comparison;
    803 
    804         // Stronger signal above weaker signal
    805         comparison = another.mRssi - mRssi;
    806         if (comparison != 0) return comparison;
    807 
    808         // Fallback on name
    809         return mName.compareTo(another.mName);
    810     }
    811 
    812     public interface Callback {
    813         void onDeviceAttributesChanged();
    814     }
    815 
    816     public int getPhonebookPermissionChoice() {
    817         int permission = mDevice.getPhonebookAccessPermission();
    818         if (permission == BluetoothDevice.ACCESS_ALLOWED) {
    819             return ACCESS_ALLOWED;
    820         } else if (permission == BluetoothDevice.ACCESS_REJECTED) {
    821             return ACCESS_REJECTED;
    822         }
    823         return ACCESS_UNKNOWN;
    824     }
    825 
    826     public void setPhonebookPermissionChoice(int permissionChoice) {
    827         int permission = BluetoothDevice.ACCESS_UNKNOWN;
    828         if (permissionChoice == ACCESS_ALLOWED) {
    829             permission = BluetoothDevice.ACCESS_ALLOWED;
    830         } else if (permissionChoice == ACCESS_REJECTED) {
    831             permission = BluetoothDevice.ACCESS_REJECTED;
    832         }
    833         mDevice.setPhonebookAccessPermission(permission);
    834     }
    835 
    836     // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth
    837     // app's shared preferences).
    838     private void migratePhonebookPermissionChoice() {
    839         SharedPreferences preferences = mContext.getSharedPreferences(
    840                 "bluetooth_phonebook_permission", Context.MODE_PRIVATE);
    841         if (!preferences.contains(mDevice.getAddress())) {
    842             return;
    843         }
    844 
    845         if (mDevice.getPhonebookAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) {
    846             int oldPermission = preferences.getInt(mDevice.getAddress(), ACCESS_UNKNOWN);
    847             if (oldPermission == ACCESS_ALLOWED) {
    848                 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
    849             } else if (oldPermission == ACCESS_REJECTED) {
    850                 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED);
    851             }
    852         }
    853 
    854         SharedPreferences.Editor editor = preferences.edit();
    855         editor.remove(mDevice.getAddress());
    856         editor.commit();
    857     }
    858 
    859     public int getMessagePermissionChoice() {
    860         int permission = mDevice.getMessageAccessPermission();
    861         if (permission == BluetoothDevice.ACCESS_ALLOWED) {
    862             return ACCESS_ALLOWED;
    863         } else if (permission == BluetoothDevice.ACCESS_REJECTED) {
    864             return ACCESS_REJECTED;
    865         }
    866         return ACCESS_UNKNOWN;
    867     }
    868 
    869     public void setMessagePermissionChoice(int permissionChoice) {
    870         int permission = BluetoothDevice.ACCESS_UNKNOWN;
    871         if (permissionChoice == ACCESS_ALLOWED) {
    872             permission = BluetoothDevice.ACCESS_ALLOWED;
    873         } else if (permissionChoice == ACCESS_REJECTED) {
    874             permission = BluetoothDevice.ACCESS_REJECTED;
    875         }
    876         mDevice.setMessageAccessPermission(permission);
    877     }
    878 
    879     public int getSimPermissionChoice() {
    880         int permission = mDevice.getSimAccessPermission();
    881         if (permission == BluetoothDevice.ACCESS_ALLOWED) {
    882             return ACCESS_ALLOWED;
    883         } else if (permission == BluetoothDevice.ACCESS_REJECTED) {
    884             return ACCESS_REJECTED;
    885         }
    886         return ACCESS_UNKNOWN;
    887     }
    888 
    889     void setSimPermissionChoice(int permissionChoice) {
    890         int permission = BluetoothDevice.ACCESS_UNKNOWN;
    891         if (permissionChoice == ACCESS_ALLOWED) {
    892             permission = BluetoothDevice.ACCESS_ALLOWED;
    893         } else if (permissionChoice == ACCESS_REJECTED) {
    894             permission = BluetoothDevice.ACCESS_REJECTED;
    895         }
    896         mDevice.setSimAccessPermission(permission);
    897     }
    898 
    899     // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth
    900     // app's shared preferences).
    901     private void migrateMessagePermissionChoice() {
    902         SharedPreferences preferences = mContext.getSharedPreferences(
    903                 "bluetooth_message_permission", Context.MODE_PRIVATE);
    904         if (!preferences.contains(mDevice.getAddress())) {
    905             return;
    906         }
    907 
    908         if (mDevice.getMessageAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) {
    909             int oldPermission = preferences.getInt(mDevice.getAddress(), ACCESS_UNKNOWN);
    910             if (oldPermission == ACCESS_ALLOWED) {
    911                 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
    912             } else if (oldPermission == ACCESS_REJECTED) {
    913                 mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_REJECTED);
    914             }
    915         }
    916 
    917         SharedPreferences.Editor editor = preferences.edit();
    918         editor.remove(mDevice.getAddress());
    919         editor.commit();
    920     }
    921 
    922     /**
    923      * @return Whether this rejection should persist.
    924      */
    925     public boolean checkAndIncreaseMessageRejectionCount() {
    926         if (mMessageRejectionCount < MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST) {
    927             mMessageRejectionCount++;
    928             saveMessageRejectionCount();
    929         }
    930         return mMessageRejectionCount >= MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST;
    931     }
    932 
    933     private void fetchMessageRejectionCount() {
    934         SharedPreferences preference = mContext.getSharedPreferences(
    935                 MESSAGE_REJECTION_COUNT_PREFS_NAME, Context.MODE_PRIVATE);
    936         mMessageRejectionCount = preference.getInt(mDevice.getAddress(), 0);
    937     }
    938 
    939     private void saveMessageRejectionCount() {
    940         SharedPreferences.Editor editor = mContext.getSharedPreferences(
    941                 MESSAGE_REJECTION_COUNT_PREFS_NAME, Context.MODE_PRIVATE).edit();
    942         if (mMessageRejectionCount == 0) {
    943             editor.remove(mDevice.getAddress());
    944         } else {
    945             editor.putInt(mDevice.getAddress(), mMessageRejectionCount);
    946         }
    947         editor.commit();
    948     }
    949 
    950     private void processPhonebookAccess() {
    951         if (mDevice.getBondState() != BluetoothDevice.BOND_BONDED) return;
    952 
    953         ParcelUuid[] uuids = mDevice.getUuids();
    954         if (BluetoothUuid.containsAnyUuid(uuids, PbapServerProfile.PBAB_CLIENT_UUIDS)) {
    955             // The pairing dialog now warns of phone-book access for paired devices.
    956             // No separate prompt is displayed after pairing.
    957             if (getPhonebookPermissionChoice() == CachedBluetoothDevice.ACCESS_UNKNOWN) {
    958                 if (mDevice.getBluetoothClass().getDeviceClass()
    959                         == BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE ||
    960                     mDevice.getBluetoothClass().getDeviceClass()
    961                         == BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET) {
    962                     setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_ALLOWED);
    963                 } else {
    964                     setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_REJECTED);
    965                 }
    966             }
    967         }
    968     }
    969 
    970     public int getMaxConnectionState() {
    971         int maxState = BluetoothProfile.STATE_DISCONNECTED;
    972         for (LocalBluetoothProfile profile : getProfiles()) {
    973             int connectionStatus = getProfileConnectionState(profile);
    974             if (connectionStatus > maxState) {
    975                 maxState = connectionStatus;
    976             }
    977         }
    978         return maxState;
    979     }
    980 
    981     /**
    982      * @return resource for string that discribes the connection state of this device.
    983      * case 1: idle or playing media, show "Active" on the only one A2DP active device.
    984      * case 2: in phone call, show "Active" on the only one HFP active device
    985      */
    986     public String getConnectionSummary() {
    987         boolean profileConnected = false;    // Updated as long as BluetoothProfile is connected
    988         boolean a2dpConnected = true;        // A2DP is connected
    989         boolean hfpConnected = true;         // HFP is connected
    990         boolean hearingAidConnected = true;  // Hearing Aid is connected
    991 
    992         for (LocalBluetoothProfile profile : getProfiles()) {
    993             int connectionStatus = getProfileConnectionState(profile);
    994 
    995             switch (connectionStatus) {
    996                 case BluetoothProfile.STATE_CONNECTING:
    997                 case BluetoothProfile.STATE_DISCONNECTING:
    998                     return mContext.getString(Utils.getConnectionStateSummary(connectionStatus));
    999 
   1000                 case BluetoothProfile.STATE_CONNECTED:
   1001                     profileConnected = true;
   1002                     break;
   1003 
   1004                 case BluetoothProfile.STATE_DISCONNECTED:
   1005                     if (profile.isProfileReady()) {
   1006                         if ((profile instanceof A2dpProfile) ||
   1007                                 (profile instanceof A2dpSinkProfile)) {
   1008                             a2dpConnected = false;
   1009                         } else if ((profile instanceof HeadsetProfile) ||
   1010                                 (profile instanceof HfpClientProfile)) {
   1011                             hfpConnected = false;
   1012                         } else if (profile instanceof HearingAidProfile) {
   1013                             hearingAidConnected = false;
   1014                         }
   1015                     }
   1016                     break;
   1017             }
   1018         }
   1019 
   1020         String batteryLevelPercentageString = null;
   1021         // Android framework should only set mBatteryLevel to valid range [0-100] or
   1022         // BluetoothDevice.BATTERY_LEVEL_UNKNOWN, any other value should be a framework bug.
   1023         // Thus assume here that if value is not BluetoothDevice.BATTERY_LEVEL_UNKNOWN, it must
   1024         // be valid
   1025         final int batteryLevel = getBatteryLevel();
   1026         if (batteryLevel != BluetoothDevice.BATTERY_LEVEL_UNKNOWN) {
   1027             // TODO: name com.android.settingslib.bluetooth.Utils something different
   1028             batteryLevelPercentageString =
   1029                     com.android.settingslib.Utils.formatPercentage(batteryLevel);
   1030         }
   1031 
   1032         int stringRes = R.string.bluetooth_pairing;
   1033         //when profile is connected, information would be available
   1034         if (profileConnected) {
   1035             if (a2dpConnected || hfpConnected || hearingAidConnected) {
   1036                 //contain battery information
   1037                 if (batteryLevelPercentageString != null) {
   1038                     //device is in phone call
   1039                     if (com.android.settingslib.Utils.isAudioModeOngoingCall(mContext)) {
   1040                         if (mIsActiveDeviceHeadset) {
   1041                             stringRes = R.string.bluetooth_active_battery_level;
   1042                         } else {
   1043                             stringRes = R.string.bluetooth_battery_level;
   1044                         }
   1045                     } else {//device is not in phone call(ex. idle or playing media)
   1046                         //need to check if A2DP and HearingAid are exclusive
   1047                         if (mIsActiveDeviceHearingAid || mIsActiveDeviceA2dp) {
   1048                             stringRes = R.string.bluetooth_active_battery_level;
   1049                         } else {
   1050                             stringRes = R.string.bluetooth_battery_level;
   1051                         }
   1052                     }
   1053                 } else {
   1054                     //no battery information
   1055                     if (com.android.settingslib.Utils.isAudioModeOngoingCall(mContext)) {
   1056                         if (mIsActiveDeviceHeadset) {
   1057                             stringRes = R.string.bluetooth_active_no_battery_level;
   1058                         }
   1059                     } else {
   1060                         if (mIsActiveDeviceHearingAid || mIsActiveDeviceA2dp) {
   1061                             stringRes = R.string.bluetooth_active_no_battery_level;
   1062                         }
   1063                     }
   1064                 }
   1065             } else {//unknown profile with battery information
   1066                 if (batteryLevelPercentageString != null) {
   1067                     stringRes = R.string.bluetooth_battery_level;
   1068                 }
   1069             }
   1070         }
   1071 
   1072         return (stringRes != R.string.bluetooth_pairing
   1073                 || getBondState() == BluetoothDevice.BOND_BONDING)
   1074                 ? mContext.getString(stringRes, batteryLevelPercentageString)
   1075                 : null;
   1076     }
   1077 
   1078     /**
   1079      * @return resource for android auto string that describes the connection state of this device.
   1080      */
   1081     public String getCarConnectionSummary() {
   1082         boolean profileConnected = false;       // at least one profile is connected
   1083         boolean a2dpNotConnected = false;       // A2DP is preferred but not connected
   1084         boolean hfpNotConnected = false;        // HFP is preferred but not connected
   1085         boolean hearingAidNotConnected = false; // Hearing Aid is preferred but not connected
   1086 
   1087         for (LocalBluetoothProfile profile : getProfiles()) {
   1088             int connectionStatus = getProfileConnectionState(profile);
   1089 
   1090             switch (connectionStatus) {
   1091                 case BluetoothProfile.STATE_CONNECTING:
   1092                 case BluetoothProfile.STATE_DISCONNECTING:
   1093                     return mContext.getString(Utils.getConnectionStateSummary(connectionStatus));
   1094 
   1095                 case BluetoothProfile.STATE_CONNECTED:
   1096                     profileConnected = true;
   1097                     break;
   1098 
   1099                 case BluetoothProfile.STATE_DISCONNECTED:
   1100                     if (profile.isProfileReady()) {
   1101                         if ((profile instanceof A2dpProfile) ||
   1102                                 (profile instanceof A2dpSinkProfile)){
   1103                             a2dpNotConnected = true;
   1104                         } else if ((profile instanceof HeadsetProfile) ||
   1105                                 (profile instanceof HfpClientProfile)) {
   1106                             hfpNotConnected = true;
   1107                         } else if (profile instanceof  HearingAidProfile) {
   1108                             hearingAidNotConnected = true;
   1109                         }
   1110                     }
   1111                     break;
   1112             }
   1113         }
   1114 
   1115         String batteryLevelPercentageString = null;
   1116         // Android framework should only set mBatteryLevel to valid range [0-100] or
   1117         // BluetoothDevice.BATTERY_LEVEL_UNKNOWN, any other value should be a framework bug.
   1118         // Thus assume here that if value is not BluetoothDevice.BATTERY_LEVEL_UNKNOWN, it must
   1119         // be valid
   1120         final int batteryLevel = getBatteryLevel();
   1121         if (batteryLevel != BluetoothDevice.BATTERY_LEVEL_UNKNOWN) {
   1122             // TODO: name com.android.settingslib.bluetooth.Utils something different
   1123             batteryLevelPercentageString =
   1124                     com.android.settingslib.Utils.formatPercentage(batteryLevel);
   1125         }
   1126 
   1127         // Prepare the string for the Active Device summary
   1128         String[] activeDeviceStringsArray = mContext.getResources().getStringArray(
   1129                 R.array.bluetooth_audio_active_device_summaries);
   1130         String activeDeviceString = activeDeviceStringsArray[0];  // Default value: not active
   1131         if (mIsActiveDeviceA2dp && mIsActiveDeviceHeadset) {
   1132             activeDeviceString = activeDeviceStringsArray[1];     // Active for Media and Phone
   1133         } else {
   1134             if (mIsActiveDeviceA2dp) {
   1135                 activeDeviceString = activeDeviceStringsArray[2]; // Active for Media only
   1136             }
   1137             if (mIsActiveDeviceHeadset) {
   1138                 activeDeviceString = activeDeviceStringsArray[3]; // Active for Phone only
   1139             }
   1140         }
   1141         if (!hearingAidNotConnected && mIsActiveDeviceHearingAid) {
   1142             activeDeviceString = activeDeviceStringsArray[1];
   1143             return mContext.getString(R.string.bluetooth_connected, activeDeviceString);
   1144         }
   1145 
   1146         if (profileConnected) {
   1147             if (a2dpNotConnected && hfpNotConnected) {
   1148                 if (batteryLevelPercentageString != null) {
   1149                     return mContext.getString(
   1150                             R.string.bluetooth_connected_no_headset_no_a2dp_battery_level,
   1151                             batteryLevelPercentageString, activeDeviceString);
   1152                 } else {
   1153                     return mContext.getString(R.string.bluetooth_connected_no_headset_no_a2dp,
   1154                             activeDeviceString);
   1155                 }
   1156 
   1157             } else if (a2dpNotConnected) {
   1158                 if (batteryLevelPercentageString != null) {
   1159                     return mContext.getString(R.string.bluetooth_connected_no_a2dp_battery_level,
   1160                             batteryLevelPercentageString, activeDeviceString);
   1161                 } else {
   1162                     return mContext.getString(R.string.bluetooth_connected_no_a2dp,
   1163                             activeDeviceString);
   1164                 }
   1165 
   1166             } else if (hfpNotConnected) {
   1167                 if (batteryLevelPercentageString != null) {
   1168                     return mContext.getString(R.string.bluetooth_connected_no_headset_battery_level,
   1169                             batteryLevelPercentageString, activeDeviceString);
   1170                 } else {
   1171                     return mContext.getString(R.string.bluetooth_connected_no_headset,
   1172                             activeDeviceString);
   1173                 }
   1174             } else {
   1175                 if (batteryLevelPercentageString != null) {
   1176                     return mContext.getString(R.string.bluetooth_connected_battery_level,
   1177                             batteryLevelPercentageString, activeDeviceString);
   1178                 } else {
   1179                     return mContext.getString(R.string.bluetooth_connected, activeDeviceString);
   1180                 }
   1181             }
   1182         }
   1183 
   1184         return getBondState() == BluetoothDevice.BOND_BONDING ?
   1185                 mContext.getString(R.string.bluetooth_pairing) : null;
   1186     }
   1187 
   1188     /**
   1189      * @return {@code true} if {@code cachedBluetoothDevice} is a2dp device
   1190      */
   1191     public boolean isA2dpDevice() {
   1192         return mProfileManager.getA2dpProfile().getConnectionStatus(mDevice) ==
   1193                 BluetoothProfile.STATE_CONNECTED;
   1194     }
   1195 
   1196     /**
   1197      * @return {@code true} if {@code cachedBluetoothDevice} is HFP device
   1198      */
   1199     public boolean isHfpDevice() {
   1200         return mProfileManager.getHeadsetProfile().getConnectionStatus(mDevice) ==
   1201                 BluetoothProfile.STATE_CONNECTED;
   1202     }
   1203 }
   1204