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