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