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