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.app.AlertDialog;
     20 import android.bluetooth.BluetoothAdapter;
     21 import android.bluetooth.BluetoothClass;
     22 import android.bluetooth.BluetoothDevice;
     23 import android.content.Context;
     24 import android.content.DialogInterface;
     25 import android.content.Intent;
     26 import android.content.res.Resources;
     27 import android.os.ParcelUuid;
     28 import android.os.SystemClock;
     29 import android.text.TextUtils;
     30 import android.util.Log;
     31 import android.view.ContextMenu;
     32 import android.view.Menu;
     33 import android.view.MenuItem;
     34 
     35 import com.android.settings.R;
     36 import com.android.settings.bluetooth.LocalBluetoothProfileManager.Profile;
     37 
     38 import java.util.ArrayList;
     39 import java.util.List;
     40 import java.util.Set;
     41 
     42 /**
     43  * CachedBluetoothDevice represents a remote Bluetooth device. It contains
     44  * attributes of the device (such as the address, name, RSSI, etc.) and
     45  * functionality that can be performed on the device (connect, pair, disconnect,
     46  * etc.).
     47  */
     48 public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> {
     49     private static final String TAG = "CachedBluetoothDevice";
     50     private static final boolean D = LocalBluetoothManager.D;
     51     private static final boolean V = LocalBluetoothManager.V;
     52     private static final boolean DEBUG = false;
     53 
     54     private static final int CONTEXT_ITEM_CONNECT = Menu.FIRST + 1;
     55     private static final int CONTEXT_ITEM_DISCONNECT = Menu.FIRST + 2;
     56     private static final int CONTEXT_ITEM_UNPAIR = Menu.FIRST + 3;
     57     private static final int CONTEXT_ITEM_CONNECT_ADVANCED = Menu.FIRST + 4;
     58 
     59     private final BluetoothDevice mDevice;
     60     private String mName;
     61     private short mRssi;
     62     private BluetoothClass mBtClass;
     63 
     64     private List<Profile> mProfiles = new ArrayList<Profile>();
     65 
     66     private boolean mVisible;
     67 
     68     private final LocalBluetoothManager mLocalManager;
     69 
     70     private List<Callback> mCallbacks = new ArrayList<Callback>();
     71 
     72     /**
     73      * When we connect to multiple profiles, we only want to display a single
     74      * error even if they all fail. This tracks that state.
     75      */
     76     private boolean mIsConnectingErrorPossible;
     77 
     78     /**
     79      * Last time a bt profile auto-connect was attempted.
     80      * If an ACTION_UUID intent comes in within
     81      * MAX_UUID_DELAY_FOR_AUTO_CONNECT milliseconds, we will try auto-connect
     82      * again with the new UUIDs
     83      */
     84     private long mConnectAttempted;
     85 
     86     // See mConnectAttempted
     87     private static final long MAX_UUID_DELAY_FOR_AUTO_CONNECT = 5000;
     88 
     89     /** Auto-connect after pairing only if locally initiated. */
     90     private boolean mConnectAfterPairing;
     91 
     92     /**
     93      * Describes the current device and profile for logging.
     94      *
     95      * @param profile Profile to describe
     96      * @return Description of the device and profile
     97      */
     98     private String describe(CachedBluetoothDevice cachedDevice, Profile profile) {
     99         StringBuilder sb = new StringBuilder();
    100         sb.append("Address:").append(cachedDevice.mDevice);
    101         if (profile != null) {
    102             sb.append(" Profile:").append(profile.name());
    103         }
    104 
    105         return sb.toString();
    106     }
    107 
    108     private String describe(Profile profile) {
    109         return describe(this, profile);
    110     }
    111 
    112     public void onProfileStateChanged(Profile profile, int newProfileState) {
    113         if (D) {
    114             Log.d(TAG, "onProfileStateChanged: profile " + profile.toString() +
    115                     " newProfileState " + newProfileState);
    116         }
    117 
    118         int newState = LocalBluetoothProfileManager.getProfileManager(mLocalManager,
    119                 profile).convertState(newProfileState);
    120 
    121         if (newState == SettingsBtStatus.CONNECTION_STATUS_CONNECTED) {
    122             if (!mProfiles.contains(profile)) {
    123                 mProfiles.add(profile);
    124             }
    125         }
    126     }
    127 
    128     CachedBluetoothDevice(Context context, BluetoothDevice device) {
    129         mLocalManager = LocalBluetoothManager.getInstance(context);
    130         if (mLocalManager == null) {
    131             throw new IllegalStateException(
    132                     "Cannot use CachedBluetoothDevice without Bluetooth hardware");
    133         }
    134 
    135         mDevice = device;
    136 
    137         fillData();
    138     }
    139 
    140     public void onClicked() {
    141         int bondState = getBondState();
    142 
    143         if (isConnected()) {
    144             askDisconnect();
    145         } else if (bondState == BluetoothDevice.BOND_BONDED) {
    146             connect();
    147         } else if (bondState == BluetoothDevice.BOND_NONE) {
    148             pair();
    149         }
    150     }
    151 
    152     public void disconnect() {
    153         for (Profile profile : mProfiles) {
    154             disconnect(profile);
    155         }
    156     }
    157 
    158     public void disconnect(Profile profile) {
    159         disconnectInt(this, profile);
    160     }
    161 
    162     private boolean disconnectInt(CachedBluetoothDevice cachedDevice, Profile profile) {
    163         LocalBluetoothProfileManager profileManager =
    164                 LocalBluetoothProfileManager.getProfileManager(mLocalManager, profile);
    165         int status = profileManager.getConnectionStatus(cachedDevice.mDevice);
    166         if (profileManager.disconnect(cachedDevice.mDevice)) {
    167             if (D) {
    168                 Log.d(TAG, "Command sent successfully:DISCONNECT " + describe(profile));
    169             }
    170             return true;
    171         }
    172         return false;
    173     }
    174 
    175     public void askDisconnect() {
    176         Context context = mLocalManager.getForegroundActivity();
    177         if (context == null) {
    178             // Cannot ask, since we need an activity context
    179             disconnect();
    180             return;
    181         }
    182 
    183         Resources res = context.getResources();
    184 
    185         String name = getName();
    186         if (TextUtils.isEmpty(name)) {
    187             name = res.getString(R.string.bluetooth_device);
    188         }
    189         String message = res.getString(R.string.bluetooth_disconnect_blank, name);
    190 
    191         DialogInterface.OnClickListener disconnectListener = new DialogInterface.OnClickListener() {
    192             public void onClick(DialogInterface dialog, int which) {
    193                 disconnect();
    194             }
    195         };
    196 
    197         new AlertDialog.Builder(context)
    198                 .setTitle(getName())
    199                 .setMessage(message)
    200                 .setPositiveButton(android.R.string.ok, disconnectListener)
    201                 .setNegativeButton(android.R.string.cancel, null)
    202                 .show();
    203     }
    204 
    205     public void connect() {
    206         if (!ensurePaired()) return;
    207 
    208         mConnectAttempted = SystemClock.elapsedRealtime();
    209 
    210         connectWithoutResettingTimer();
    211     }
    212 
    213     /*package*/ void onBondingDockConnect() {
    214         // Attempt to connect if UUIDs are available. Otherwise,
    215         // we will connect when the ACTION_UUID intent arrives.
    216         connect();
    217     }
    218 
    219     private void connectWithoutResettingTimer() {
    220         // Try to initialize the profiles if there were not.
    221         if (mProfiles.size() == 0) {
    222             if (!updateProfiles()) {
    223                 // If UUIDs are not available yet, connect will be happen
    224                 // upon arrival of the ACTION_UUID intent.
    225                 if (DEBUG) Log.d(TAG, "No profiles. Maybe we will connect later");
    226                 return;
    227             }
    228         }
    229 
    230         // Reset the only-show-one-error-dialog tracking variable
    231         mIsConnectingErrorPossible = true;
    232 
    233         int preferredProfiles = 0;
    234         for (Profile profile : mProfiles) {
    235             if (isConnectableProfile(profile)) {
    236                 LocalBluetoothProfileManager profileManager = LocalBluetoothProfileManager
    237                         .getProfileManager(mLocalManager, profile);
    238                 if (profileManager.isPreferred(mDevice)) {
    239                     ++preferredProfiles;
    240                     disconnectConnected(this, profile);
    241                     connectInt(this, profile);
    242                 }
    243             }
    244         }
    245         if (DEBUG) Log.d(TAG, "Preferred profiles = " + preferredProfiles);
    246 
    247         if (preferredProfiles == 0) {
    248             connectAllProfiles();
    249         }
    250     }
    251 
    252     private void connectAllProfiles() {
    253         if (!ensurePaired()) return;
    254 
    255         // Reset the only-show-one-error-dialog tracking variable
    256         mIsConnectingErrorPossible = true;
    257 
    258         for (Profile profile : mProfiles) {
    259             if (isConnectableProfile(profile)) {
    260                 LocalBluetoothProfileManager profileManager = LocalBluetoothProfileManager
    261                         .getProfileManager(mLocalManager, profile);
    262                 profileManager.setPreferred(mDevice, false);
    263                 disconnectConnected(this, profile);
    264                 connectInt(this, profile);
    265             }
    266         }
    267     }
    268 
    269     public void connect(Profile profile) {
    270         mConnectAttempted = SystemClock.elapsedRealtime();
    271         // Reset the only-show-one-error-dialog tracking variable
    272         mIsConnectingErrorPossible = true;
    273         disconnectConnected(this, profile);
    274         connectInt(this, profile);
    275     }
    276 
    277     private void disconnectConnected(CachedBluetoothDevice device, Profile profile) {
    278         LocalBluetoothProfileManager profileManager =
    279             LocalBluetoothProfileManager.getProfileManager(mLocalManager, profile);
    280         CachedBluetoothDeviceManager cachedDeviceManager = mLocalManager.getCachedDeviceManager();
    281         Set<BluetoothDevice> devices = profileManager.getConnectedDevices();
    282         if (devices == null) return;
    283         for (BluetoothDevice btDevice : devices) {
    284             CachedBluetoothDevice cachedDevice = cachedDeviceManager.findDevice(btDevice);
    285 
    286             if (cachedDevice != null && !cachedDevice.equals(device)) {
    287                 disconnectInt(cachedDevice, profile);
    288             }
    289         }
    290     }
    291 
    292     private boolean connectInt(CachedBluetoothDevice cachedDevice, Profile profile) {
    293         if (!cachedDevice.ensurePaired()) return false;
    294 
    295         LocalBluetoothProfileManager profileManager =
    296                 LocalBluetoothProfileManager.getProfileManager(mLocalManager, profile);
    297         int status = profileManager.getConnectionStatus(cachedDevice.mDevice);
    298         if (profileManager.connect(cachedDevice.mDevice)) {
    299             if (D) {
    300                 Log.d(TAG, "Command sent successfully:CONNECT " + describe(profile));
    301             }
    302             return true;
    303         }
    304         Log.i(TAG, "Failed to connect " + profile.toString() + " to " + cachedDevice.mName);
    305 
    306         return false;
    307     }
    308 
    309     public void showConnectingError() {
    310         if (!mIsConnectingErrorPossible) return;
    311         mIsConnectingErrorPossible = false;
    312 
    313         mLocalManager.showError(mDevice, R.string.bluetooth_error_title,
    314                 R.string.bluetooth_connecting_error_message);
    315     }
    316 
    317     private boolean ensurePaired() {
    318         if (getBondState() == BluetoothDevice.BOND_NONE) {
    319             pair();
    320             return false;
    321         } else {
    322             return true;
    323         }
    324     }
    325 
    326     public void pair() {
    327         BluetoothAdapter adapter = mLocalManager.getBluetoothAdapter();
    328 
    329         // Pairing is unreliable while scanning, so cancel discovery
    330         if (adapter.isDiscovering()) {
    331             adapter.cancelDiscovery();
    332         }
    333 
    334         if (!mDevice.createBond()) {
    335             mLocalManager.showError(mDevice, R.string.bluetooth_error_title,
    336                     R.string.bluetooth_pairing_error_message);
    337             return;
    338         }
    339 
    340         mConnectAfterPairing = true;  // auto-connect after pairing
    341     }
    342 
    343     public void unpair() {
    344         disconnect();
    345 
    346         int state = getBondState();
    347 
    348         if (state == BluetoothDevice.BOND_BONDING) {
    349             mDevice.cancelBondProcess();
    350         }
    351 
    352         if (state != BluetoothDevice.BOND_NONE) {
    353             final BluetoothDevice dev = getDevice();
    354             if (dev != null) {
    355                 final boolean successful = dev.removeBond();
    356                 if (successful) {
    357                     if (D) {
    358                         Log.d(TAG, "Command sent successfully:REMOVE_BOND " + describe(null));
    359                     }
    360                 } else if (V) {
    361                     Log.v(TAG, "Framework rejected command immediately:REMOVE_BOND " +
    362                             describe(null));
    363                 }
    364             }
    365         }
    366     }
    367 
    368     private void fillData() {
    369         fetchName();
    370         fetchBtClass();
    371         updateProfiles();
    372 
    373         mVisible = false;
    374 
    375         dispatchAttributesChanged();
    376     }
    377 
    378     public BluetoothDevice getDevice() {
    379         return mDevice;
    380     }
    381 
    382     public String getName() {
    383         return mName;
    384     }
    385 
    386     public void setName(String name) {
    387         if (!mName.equals(name)) {
    388             if (TextUtils.isEmpty(name)) {
    389                 mName = mDevice.getAddress();
    390             } else {
    391                 mName = name;
    392             }
    393             dispatchAttributesChanged();
    394         }
    395     }
    396 
    397     public void refreshName() {
    398         fetchName();
    399         dispatchAttributesChanged();
    400     }
    401 
    402     private void fetchName() {
    403         mName = mDevice.getName();
    404 
    405         if (TextUtils.isEmpty(mName)) {
    406             mName = mDevice.getAddress();
    407             if (DEBUG) Log.d(TAG, "Default to address. Device has no name (yet) " + mName);
    408         }
    409     }
    410 
    411     public void refresh() {
    412         dispatchAttributesChanged();
    413     }
    414 
    415     public boolean isVisible() {
    416         return mVisible;
    417     }
    418 
    419     void setVisible(boolean visible) {
    420         if (mVisible != visible) {
    421             mVisible = visible;
    422             dispatchAttributesChanged();
    423         }
    424     }
    425 
    426     public int getBondState() {
    427         return mDevice.getBondState();
    428     }
    429 
    430     void setRssi(short rssi) {
    431         if (mRssi != rssi) {
    432             mRssi = rssi;
    433             dispatchAttributesChanged();
    434         }
    435     }
    436 
    437     /**
    438      * Checks whether we are connected to this device (any profile counts).
    439      *
    440      * @return Whether it is connected.
    441      */
    442     public boolean isConnected() {
    443         for (Profile profile : mProfiles) {
    444             int status = LocalBluetoothProfileManager.getProfileManager(mLocalManager, profile)
    445                     .getConnectionStatus(mDevice);
    446             if (SettingsBtStatus.isConnectionStatusConnected(status)) {
    447                 return true;
    448             }
    449         }
    450 
    451         return false;
    452     }
    453 
    454     public boolean isBusy() {
    455         for (Profile profile : mProfiles) {
    456             int status = LocalBluetoothProfileManager.getProfileManager(mLocalManager, profile)
    457                     .getConnectionStatus(mDevice);
    458             if (SettingsBtStatus.isConnectionStatusBusy(status)) {
    459                 return true;
    460             }
    461         }
    462 
    463         if (getBondState() == BluetoothDevice.BOND_BONDING) {
    464             return true;
    465         }
    466 
    467         return false;
    468     }
    469 
    470     public int getBtClassDrawable() {
    471         if (mBtClass != null) {
    472             switch (mBtClass.getMajorDeviceClass()) {
    473             case BluetoothClass.Device.Major.COMPUTER:
    474                 return R.drawable.ic_bt_laptop;
    475 
    476             case BluetoothClass.Device.Major.PHONE:
    477                 return R.drawable.ic_bt_cellphone;
    478             }
    479         } else {
    480             Log.w(TAG, "mBtClass is null");
    481         }
    482 
    483         if (mProfiles.size() > 0) {
    484             if (mProfiles.contains(Profile.A2DP)) {
    485                 return R.drawable.ic_bt_headphones_a2dp;
    486             } else if (mProfiles.contains(Profile.HEADSET)) {
    487                 return R.drawable.ic_bt_headset_hfp;
    488             }
    489         } else if (mBtClass != null) {
    490             if (mBtClass.doesClassMatch(BluetoothClass.PROFILE_A2DP)) {
    491                 return R.drawable.ic_bt_headphones_a2dp;
    492 
    493             }
    494             if (mBtClass.doesClassMatch(BluetoothClass.PROFILE_HEADSET)) {
    495                 return R.drawable.ic_bt_headset_hfp;
    496             }
    497         }
    498         return 0;
    499     }
    500 
    501     /**
    502      * Fetches a new value for the cached BT class.
    503      */
    504     private void fetchBtClass() {
    505         mBtClass = mDevice.getBluetoothClass();
    506     }
    507 
    508     private boolean updateProfiles() {
    509         ParcelUuid[] uuids = mDevice.getUuids();
    510         if (uuids == null) return false;
    511 
    512         LocalBluetoothProfileManager.updateProfiles(uuids, mProfiles);
    513 
    514         if (DEBUG) {
    515             Log.e(TAG, "updating profiles for " + mDevice.getName());
    516 
    517             boolean printUuids = true;
    518             BluetoothClass bluetoothClass = mDevice.getBluetoothClass();
    519 
    520             if (bluetoothClass != null) {
    521                 if (bluetoothClass.doesClassMatch(BluetoothClass.PROFILE_HEADSET) !=
    522                     mProfiles.contains(Profile.HEADSET)) {
    523                     Log.v(TAG, "headset classbits != uuid");
    524                     printUuids = true;
    525                 }
    526 
    527                 if (bluetoothClass.doesClassMatch(BluetoothClass.PROFILE_A2DP) !=
    528                     mProfiles.contains(Profile.A2DP)) {
    529                     Log.v(TAG, "a2dp classbits != uuid");
    530                     printUuids = true;
    531                 }
    532 
    533                 if (bluetoothClass.doesClassMatch(BluetoothClass.PROFILE_OPP) !=
    534                     mProfiles.contains(Profile.OPP)) {
    535                     Log.v(TAG, "opp classbits != uuid");
    536                     printUuids = true;
    537                 }
    538             }
    539 
    540             if (printUuids) {
    541                 if (bluetoothClass != null) Log.v(TAG, "Class: " + bluetoothClass.toString());
    542                 Log.v(TAG, "UUID:");
    543                 for (int i = 0; i < uuids.length; i++) {
    544                     Log.v(TAG, "  " + uuids[i]);
    545                 }
    546             }
    547         }
    548         return true;
    549     }
    550 
    551     /**
    552      * Refreshes the UI for the BT class, including fetching the latest value
    553      * for the class.
    554      */
    555     public void refreshBtClass() {
    556         fetchBtClass();
    557         dispatchAttributesChanged();
    558     }
    559 
    560     /**
    561      * Refreshes the UI when framework alerts us of a UUID change.
    562      */
    563     public void onUuidChanged() {
    564         updateProfiles();
    565 
    566         if (DEBUG) {
    567             Log.e(TAG, "onUuidChanged: Time since last connect"
    568                     + (SystemClock.elapsedRealtime() - mConnectAttempted));
    569         }
    570 
    571         /*
    572          * If a connect was attempted earlier without any UUID, we will do the
    573          * connect now.
    574          */
    575         if (mProfiles.size() > 0
    576                 && (mConnectAttempted + MAX_UUID_DELAY_FOR_AUTO_CONNECT) > SystemClock
    577                         .elapsedRealtime()) {
    578             connectWithoutResettingTimer();
    579         }
    580         dispatchAttributesChanged();
    581     }
    582 
    583     public void onBondingStateChanged(int bondState) {
    584         if (bondState == BluetoothDevice.BOND_NONE) {
    585             mProfiles.clear();
    586             mConnectAfterPairing = false;  // cancel auto-connect
    587         }
    588 
    589         refresh();
    590 
    591         if (bondState == BluetoothDevice.BOND_BONDED) {
    592             if (mDevice.isBluetoothDock()) {
    593                 onBondingDockConnect();
    594             } else if (mConnectAfterPairing) {
    595                 connect();
    596             }
    597             mConnectAfterPairing = false;
    598         }
    599     }
    600 
    601     public void setBtClass(BluetoothClass btClass) {
    602         if (btClass != null && mBtClass != btClass) {
    603             mBtClass = btClass;
    604             dispatchAttributesChanged();
    605         }
    606     }
    607 
    608     public int getSummary() {
    609         // TODO: clean up
    610         int oneOffSummary = getOneOffSummary();
    611         if (oneOffSummary != 0) {
    612             return oneOffSummary;
    613         }
    614 
    615         for (Profile profile : mProfiles) {
    616             LocalBluetoothProfileManager profileManager = LocalBluetoothProfileManager
    617                     .getProfileManager(mLocalManager, profile);
    618             int connectionStatus = profileManager.getConnectionStatus(mDevice);
    619 
    620             if (SettingsBtStatus.isConnectionStatusConnected(connectionStatus) ||
    621                     connectionStatus == SettingsBtStatus.CONNECTION_STATUS_CONNECTING ||
    622                     connectionStatus == SettingsBtStatus.CONNECTION_STATUS_DISCONNECTING) {
    623                 return SettingsBtStatus.getConnectionStatusSummary(connectionStatus);
    624             }
    625         }
    626 
    627         return SettingsBtStatus.getPairingStatusSummary(getBondState());
    628     }
    629 
    630     /**
    631      * We have special summaries when particular profiles are connected. This
    632      * checks for those states and returns an applicable summary.
    633      *
    634      * @return A one-off summary that is applicable for the current state, or 0.
    635      */
    636     private int getOneOffSummary() {
    637         boolean isA2dpConnected = false, isHeadsetConnected = false, isConnecting = false;
    638 
    639         if (mProfiles.contains(Profile.A2DP)) {
    640             LocalBluetoothProfileManager profileManager = LocalBluetoothProfileManager
    641                     .getProfileManager(mLocalManager, Profile.A2DP);
    642             isConnecting = profileManager.getConnectionStatus(mDevice) ==
    643                     SettingsBtStatus.CONNECTION_STATUS_CONNECTING;
    644             isA2dpConnected = profileManager.isConnected(mDevice);
    645         }
    646 
    647         if (mProfiles.contains(Profile.HEADSET)) {
    648             LocalBluetoothProfileManager profileManager = LocalBluetoothProfileManager
    649                     .getProfileManager(mLocalManager, Profile.HEADSET);
    650             isConnecting |= profileManager.getConnectionStatus(mDevice) ==
    651                     SettingsBtStatus.CONNECTION_STATUS_CONNECTING;
    652             isHeadsetConnected = profileManager.isConnected(mDevice);
    653         }
    654 
    655         if (isConnecting) {
    656             // If any of these important profiles is connecting, prefer that
    657             return SettingsBtStatus.getConnectionStatusSummary(
    658                     SettingsBtStatus.CONNECTION_STATUS_CONNECTING);
    659         } else if (isA2dpConnected && isHeadsetConnected) {
    660             return R.string.bluetooth_summary_connected_to_a2dp_headset;
    661         } else if (isA2dpConnected) {
    662             return R.string.bluetooth_summary_connected_to_a2dp;
    663         } else if (isHeadsetConnected) {
    664             return R.string.bluetooth_summary_connected_to_headset;
    665         } else {
    666             return 0;
    667         }
    668     }
    669 
    670     public List<Profile> getConnectableProfiles() {
    671         ArrayList<Profile> connectableProfiles = new ArrayList<Profile>();
    672         for (Profile profile : mProfiles) {
    673             if (isConnectableProfile(profile)) {
    674                 connectableProfiles.add(profile);
    675             }
    676         }
    677         return connectableProfiles;
    678     }
    679 
    680     private boolean isConnectableProfile(Profile profile) {
    681         return profile.equals(Profile.HEADSET) || profile.equals(Profile.A2DP);
    682     }
    683 
    684     public void onCreateContextMenu(ContextMenu menu) {
    685         // No context menu if it is busy (none of these items are applicable if busy)
    686         if (mLocalManager.getBluetoothState() != BluetoothAdapter.STATE_ON || isBusy()) {
    687             return;
    688         }
    689 
    690         int bondState = getBondState();
    691         boolean isConnected = isConnected();
    692         boolean hasConnectableProfiles = false;
    693 
    694         for (Profile profile : mProfiles) {
    695             if (isConnectableProfile(profile)) {
    696                 hasConnectableProfiles = true;
    697                 break;
    698             }
    699         }
    700 
    701         menu.setHeaderTitle(getName());
    702 
    703         if (bondState == BluetoothDevice.BOND_NONE) { // Not paired and not connected
    704             menu.add(0, CONTEXT_ITEM_CONNECT, 0, R.string.bluetooth_device_context_pair_connect);
    705         } else { // Paired
    706             if (isConnected) { // Paired and connected
    707                 menu.add(0, CONTEXT_ITEM_DISCONNECT, 0,
    708                         R.string.bluetooth_device_context_disconnect);
    709                 menu.add(0, CONTEXT_ITEM_UNPAIR, 0,
    710                         R.string.bluetooth_device_context_disconnect_unpair);
    711             } else { // Paired but not connected
    712                 if (hasConnectableProfiles) {
    713                     menu.add(0, CONTEXT_ITEM_CONNECT, 0, R.string.bluetooth_device_context_connect);
    714                 }
    715                 menu.add(0, CONTEXT_ITEM_UNPAIR, 0, R.string.bluetooth_device_context_unpair);
    716             }
    717 
    718             // Show the connection options item
    719             if (hasConnectableProfiles) {
    720                 menu.add(0, CONTEXT_ITEM_CONNECT_ADVANCED, 0,
    721                         R.string.bluetooth_device_context_connect_advanced);
    722             }
    723         }
    724     }
    725 
    726     /**
    727      * Called when a context menu item is clicked.
    728      *
    729      * @param item The item that was clicked.
    730      */
    731     public void onContextItemSelected(MenuItem item) {
    732         switch (item.getItemId()) {
    733             case CONTEXT_ITEM_DISCONNECT:
    734                 disconnect();
    735                 break;
    736 
    737             case CONTEXT_ITEM_CONNECT:
    738                 connect();
    739                 break;
    740 
    741             case CONTEXT_ITEM_UNPAIR:
    742                 unpair();
    743                 break;
    744 
    745             case CONTEXT_ITEM_CONNECT_ADVANCED:
    746                 Intent intent = new Intent();
    747                 // Need an activity context to open this in our task
    748                 Context context = mLocalManager.getForegroundActivity();
    749                 if (context == null) {
    750                     // Fallback on application context, and open in a new task
    751                     context = mLocalManager.getContext();
    752                     intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    753                 }
    754                 intent.setClass(context, ConnectSpecificProfilesActivity.class);
    755                 intent.putExtra(ConnectSpecificProfilesActivity.EXTRA_DEVICE, mDevice);
    756                 context.startActivity(intent);
    757                 break;
    758         }
    759     }
    760 
    761     public void registerCallback(Callback callback) {
    762         synchronized (mCallbacks) {
    763             mCallbacks.add(callback);
    764         }
    765     }
    766 
    767     public void unregisterCallback(Callback callback) {
    768         synchronized (mCallbacks) {
    769             mCallbacks.remove(callback);
    770         }
    771     }
    772 
    773     private void dispatchAttributesChanged() {
    774         synchronized (mCallbacks) {
    775             for (Callback callback : mCallbacks) {
    776                 callback.onDeviceAttributesChanged(this);
    777             }
    778         }
    779     }
    780 
    781     @Override
    782     public String toString() {
    783         return mDevice.toString();
    784     }
    785 
    786     @Override
    787     public boolean equals(Object o) {
    788         if ((o == null) || !(o instanceof CachedBluetoothDevice)) {
    789             throw new ClassCastException();
    790         }
    791 
    792         return mDevice.equals(((CachedBluetoothDevice) o).mDevice);
    793     }
    794 
    795     @Override
    796     public int hashCode() {
    797         return mDevice.getAddress().hashCode();
    798     }
    799 
    800     public int compareTo(CachedBluetoothDevice another) {
    801         int comparison;
    802 
    803         // Connected above not connected
    804         comparison = (another.isConnected() ? 1 : 0) - (isConnected() ? 1 : 0);
    805         if (comparison != 0) return comparison;
    806 
    807         // Paired above not paired
    808         comparison = (another.getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0) -
    809             (getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0);
    810         if (comparison != 0) return comparison;
    811 
    812         // Visible above not visible
    813         comparison = (another.mVisible ? 1 : 0) - (mVisible ? 1 : 0);
    814         if (comparison != 0) return comparison;
    815 
    816         // Stronger signal above weaker signal
    817         comparison = another.mRssi - mRssi;
    818         if (comparison != 0) return comparison;
    819 
    820         // Fallback on name
    821         return getName().compareTo(another.getName());
    822     }
    823 
    824     public interface Callback {
    825         void onDeviceAttributesChanged(CachedBluetoothDevice cachedDevice);
    826     }
    827 }
    828