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.text.DateFormat;
     39 import java.util.ArrayList;
     40 import java.util.Date;
     41 import java.util.Iterator;
     42 import java.util.LinkedList;
     43 import java.util.List;
     44 import java.util.Set;
     45 
     46 /**
     47  * CachedBluetoothDevice represents a remote Bluetooth device. It contains
     48  * attributes of the device (such as the address, name, RSSI, etc.) and
     49  * functionality that can be performed on the device (connect, pair, disconnect,
     50  * etc.).
     51  */
     52 public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> {
     53     private static final String TAG = "CachedBluetoothDevice";
     54     private static final boolean D = LocalBluetoothManager.D;
     55     private static final boolean V = LocalBluetoothManager.V;
     56     private static final boolean DEBUG = false;
     57 
     58     private static final int CONTEXT_ITEM_CONNECT = Menu.FIRST + 1;
     59     private static final int CONTEXT_ITEM_DISCONNECT = Menu.FIRST + 2;
     60     private static final int CONTEXT_ITEM_UNPAIR = Menu.FIRST + 3;
     61     private static final int CONTEXT_ITEM_CONNECT_ADVANCED = Menu.FIRST + 4;
     62 
     63     private final BluetoothDevice mDevice;
     64     private String mName;
     65     private short mRssi;
     66     private BluetoothClass mBtClass;
     67 
     68     private List<Profile> mProfiles = new ArrayList<Profile>();
     69 
     70     private boolean mVisible;
     71 
     72     private final LocalBluetoothManager mLocalManager;
     73 
     74     private List<Callback> mCallbacks = new ArrayList<Callback>();
     75 
     76     /**
     77      * When we connect to multiple profiles, we only want to display a single
     78      * error even if they all fail. This tracks that state.
     79      */
     80     private boolean mIsConnectingErrorPossible;
     81 
     82     /**
     83      * Last time a bt profile auto-connect was attempted.
     84      * If an ACTION_UUID intent comes in within
     85      * MAX_UUID_DELAY_FOR_AUTO_CONNECT milliseconds, we will try auto-connect
     86      * again with the new UUIDs
     87      */
     88     private long mConnectAttempted;
     89 
     90     // See mConnectAttempted
     91     private static final long MAX_UUID_DELAY_FOR_AUTO_CONNECT = 5000;
     92 
     93     // Max time to hold the work queue if we don't get or missed a response
     94     // from the bt framework.
     95     private static final long MAX_WAIT_TIME_FOR_FRAMEWORK = 25 * 1000;
     96 
     97     private enum BluetoothCommand {
     98         CONNECT, DISCONNECT, REMOVE_BOND,
     99     }
    100 
    101     static class BluetoothJob {
    102         final BluetoothCommand command; // CONNECT, DISCONNECT
    103         final CachedBluetoothDevice cachedDevice;
    104         final Profile profile; // HEADSET, A2DP, etc
    105         // 0 means this command was not been sent to the bt framework.
    106         long timeSent;
    107 
    108         public BluetoothJob(BluetoothCommand command,
    109                 CachedBluetoothDevice cachedDevice, Profile profile) {
    110             this.command = command;
    111             this.cachedDevice = cachedDevice;
    112             this.profile = profile;
    113             this.timeSent = 0;
    114         }
    115 
    116         @Override
    117         public String toString() {
    118             StringBuilder sb = new StringBuilder();
    119             sb.append(command.name());
    120             sb.append(" Address:").append(cachedDevice.mDevice);
    121             if (profile != null) {
    122                 sb.append(" Profile:").append(profile.name());
    123             }
    124             sb.append(" TimeSent:");
    125             if (timeSent == 0) {
    126                 sb.append("not yet");
    127             } else {
    128                 sb.append(DateFormat.getTimeInstance().format(new Date(timeSent)));
    129             }
    130             return sb.toString();
    131         }
    132     }
    133 
    134     /**
    135      * We want to serialize connect and disconnect calls. http://b/170538
    136      * This are some headsets that may have L2CAP resource limitation. We want
    137      * to limit the bt bandwidth usage.
    138      *
    139      * A queue to keep track of asynchronous calls to the bt framework.  The
    140      * first item, if exist, should be in progress i.e. went to the bt framework
    141      * already, waiting for a notification to come back. The second item and
    142      * beyond have not been sent to the bt framework yet.
    143      */
    144     private static LinkedList<BluetoothJob> workQueue = new LinkedList<BluetoothJob>();
    145 
    146     private void queueCommand(BluetoothJob job) {
    147         synchronized (workQueue) {
    148             if (D) {
    149                 Log.d(TAG, workQueue.toString());
    150             }
    151             boolean processNow = pruneQueue(job);
    152 
    153             // Add job to queue
    154             if (D) {
    155                 Log.d(TAG, "Adding: " + job.toString());
    156             }
    157             workQueue.add(job);
    158 
    159             // if there's nothing pending from before, send the command to bt
    160             // framework immediately.
    161             if (workQueue.size() == 1 || processNow) {
    162                 // If the failed to process, just drop it from the queue.
    163                 // There will be no callback to remove this from the queue.
    164                 processCommands();
    165             }
    166         }
    167     }
    168 
    169     private boolean pruneQueue(BluetoothJob job) {
    170         boolean removedStaleItems = false;
    171         long now = System.currentTimeMillis();
    172         Iterator<BluetoothJob> it = workQueue.iterator();
    173         while (it.hasNext()) {
    174             BluetoothJob existingJob = it.next();
    175 
    176             // Remove any pending CONNECTS when we receive a DISCONNECT
    177             if (job != null && job.command == BluetoothCommand.DISCONNECT) {
    178                 if (existingJob.timeSent == 0
    179                         && existingJob.command == BluetoothCommand.CONNECT
    180                         && existingJob.cachedDevice.mDevice.equals(job.cachedDevice.mDevice)
    181                         && existingJob.profile == job.profile) {
    182                     if (D) {
    183                         Log.d(TAG, "Removed because of a pending disconnect. " + existingJob);
    184                     }
    185                     it.remove();
    186                     continue;
    187                 }
    188             }
    189 
    190             // Defensive Code: Remove any job that older than a preset time.
    191             // We never got a call back. It is better to have overlapping
    192             // calls than to get stuck.
    193             if (existingJob.timeSent != 0
    194                     && (now - existingJob.timeSent) >= MAX_WAIT_TIME_FOR_FRAMEWORK) {
    195                 Log.w(TAG, "Timeout. Removing Job:" + existingJob.toString());
    196                 it.remove();
    197                 removedStaleItems = true;
    198                 continue;
    199             }
    200         }
    201         return removedStaleItems;
    202     }
    203 
    204     private boolean processCommand(BluetoothJob job) {
    205         boolean successful = false;
    206         if (job.timeSent == 0) {
    207             job.timeSent = System.currentTimeMillis();
    208             switch (job.command) {
    209             case CONNECT:
    210                 successful = connectInt(job.cachedDevice, job.profile);
    211                 break;
    212             case DISCONNECT:
    213                 successful = disconnectInt(job.cachedDevice, job.profile);
    214                 break;
    215             case REMOVE_BOND:
    216                 BluetoothDevice dev = job.cachedDevice.getDevice();
    217                 if (dev != null) {
    218                     successful = dev.removeBond();
    219                 }
    220                 break;
    221             }
    222 
    223             if (successful) {
    224                 if (D) {
    225                     Log.d(TAG, "Command sent successfully:" + job.toString());
    226                 }
    227             } else if (V) {
    228                 Log.v(TAG, "Framework rejected command immediately:" + job.toString());
    229             }
    230         } else if (D) {
    231             Log.d(TAG, "Job already has a sent time. Skip. " + job.toString());
    232         }
    233 
    234         return successful;
    235     }
    236 
    237     public void onProfileStateChanged(Profile profile, int newProfileState) {
    238         synchronized (workQueue) {
    239             if (D) {
    240                 Log.d(TAG, "onProfileStateChanged:" + workQueue.toString());
    241             }
    242 
    243             int newState = LocalBluetoothProfileManager.getProfileManager(mLocalManager,
    244                     profile).convertState(newProfileState);
    245 
    246             if (newState == SettingsBtStatus.CONNECTION_STATUS_CONNECTED) {
    247                 if (!mProfiles.contains(profile)) {
    248                     mProfiles.add(profile);
    249                 }
    250             }
    251 
    252             /* Ignore the transient states e.g. connecting, disconnecting */
    253             if (newState == SettingsBtStatus.CONNECTION_STATUS_CONNECTED ||
    254                     newState == SettingsBtStatus.CONNECTION_STATUS_DISCONNECTED) {
    255                 BluetoothJob job = workQueue.peek();
    256                 if (job == null) {
    257                     return;
    258                 } else if (!job.cachedDevice.mDevice.equals(mDevice)) {
    259                     // This can happen in 2 cases: 1) BT device initiated pairing and
    260                     // 2) disconnects of one headset that's triggered by connects of
    261                     // another.
    262                     if (D) {
    263                         Log.d(TAG, "mDevice:" + mDevice + " != head:" + job.toString());
    264                     }
    265 
    266                     // Check to see if we need to remove the stale items from the queue
    267                     if (!pruneQueue(null)) {
    268                         // nothing in the queue was modify. Just ignore the notification and return.
    269                         return;
    270                     }
    271                 } else {
    272                     // Remove the first item and process the next one
    273                     workQueue.poll();
    274                 }
    275 
    276                 processCommands();
    277             }
    278         }
    279     }
    280 
    281     /*
    282      * This method is called in 2 places:
    283      * 1) queryCommand() - when someone or something want to connect or
    284      *    disconnect
    285      * 2) onProfileStateChanged() - when the framework sends an intent
    286      *    notification when it finishes processing a command
    287      */
    288     private void processCommands() {
    289         if (D) {
    290             Log.d(TAG, "processCommands:" + workQueue.toString());
    291         }
    292         Iterator<BluetoothJob> it = workQueue.iterator();
    293         while (it.hasNext()) {
    294             BluetoothJob job = it.next();
    295             if (processCommand(job)) {
    296                 // Sent to bt framework. Done for now. Will remove this job
    297                 // from queue when we get an event
    298                 return;
    299             } else {
    300                 /*
    301                  * If the command failed immediately, there will be no event
    302                  * callbacks. So delete the job immediately and move on to the
    303                  * next one
    304                  */
    305                 it.remove();
    306             }
    307         }
    308     }
    309 
    310     CachedBluetoothDevice(Context context, BluetoothDevice device) {
    311         mLocalManager = LocalBluetoothManager.getInstance(context);
    312         if (mLocalManager == null) {
    313             throw new IllegalStateException(
    314                     "Cannot use CachedBluetoothDevice without Bluetooth hardware");
    315         }
    316 
    317         mDevice = device;
    318 
    319         fillData();
    320     }
    321 
    322     public void onClicked() {
    323         int bondState = getBondState();
    324 
    325         if (isConnected()) {
    326             askDisconnect();
    327         } else if (bondState == BluetoothDevice.BOND_BONDED) {
    328             connect();
    329         } else if (bondState == BluetoothDevice.BOND_NONE) {
    330             pair();
    331         }
    332     }
    333 
    334     public void disconnect() {
    335         for (Profile profile : mProfiles) {
    336             disconnect(profile);
    337         }
    338     }
    339 
    340     public void disconnect(Profile profile) {
    341         queueCommand(new BluetoothJob(BluetoothCommand.DISCONNECT, this, profile));
    342     }
    343 
    344     private boolean disconnectInt(CachedBluetoothDevice cachedDevice, Profile profile) {
    345         LocalBluetoothProfileManager profileManager =
    346                 LocalBluetoothProfileManager.getProfileManager(mLocalManager, profile);
    347         int status = profileManager.getConnectionStatus(cachedDevice.mDevice);
    348         if (SettingsBtStatus.isConnectionStatusConnected(status)) {
    349             if (profileManager.disconnect(cachedDevice.mDevice)) {
    350                 return true;
    351             }
    352         }
    353         return false;
    354     }
    355 
    356     public void askDisconnect() {
    357         Context context = mLocalManager.getForegroundActivity();
    358         if (context == null) {
    359             // Cannot ask, since we need an activity context
    360             disconnect();
    361             return;
    362         }
    363 
    364         Resources res = context.getResources();
    365 
    366         String name = getName();
    367         if (TextUtils.isEmpty(name)) {
    368             name = res.getString(R.string.bluetooth_device);
    369         }
    370         String message = res.getString(R.string.bluetooth_disconnect_blank, name);
    371 
    372         DialogInterface.OnClickListener disconnectListener = new DialogInterface.OnClickListener() {
    373             public void onClick(DialogInterface dialog, int which) {
    374                 disconnect();
    375             }
    376         };
    377 
    378         new AlertDialog.Builder(context)
    379                 .setTitle(getName())
    380                 .setMessage(message)
    381                 .setPositiveButton(android.R.string.ok, disconnectListener)
    382                 .setNegativeButton(android.R.string.cancel, null)
    383                 .show();
    384     }
    385 
    386     public void connect() {
    387         if (!ensurePaired()) return;
    388 
    389         mConnectAttempted = SystemClock.elapsedRealtime();
    390 
    391         connectWithoutResettingTimer();
    392     }
    393 
    394     /*package*/ void onBondingDockConnect() {
    395         // Don't connect just set the timer.
    396         // TODO(): Fix the actual problem
    397         mConnectAttempted = SystemClock.elapsedRealtime();
    398     }
    399 
    400     private void connectWithoutResettingTimer() {
    401         // Try to initialize the profiles if there were not.
    402         if (mProfiles.size() == 0) {
    403             if (!updateProfiles()) {
    404                 // If UUIDs are not available yet, connect will be happen
    405                 // upon arrival of the ACTION_UUID intent.
    406                 if (DEBUG) Log.d(TAG, "No profiles. Maybe we will connect later");
    407                 return;
    408             }
    409         }
    410 
    411         // Reset the only-show-one-error-dialog tracking variable
    412         mIsConnectingErrorPossible = true;
    413 
    414         int preferredProfiles = 0;
    415         for (Profile profile : mProfiles) {
    416             if (isConnectableProfile(profile)) {
    417                 LocalBluetoothProfileManager profileManager = LocalBluetoothProfileManager
    418                         .getProfileManager(mLocalManager, profile);
    419                 if (profileManager.isPreferred(mDevice)) {
    420                     ++preferredProfiles;
    421                     disconnectConnected(profile);
    422                     queueCommand(new BluetoothJob(BluetoothCommand.CONNECT, this, profile));
    423                 }
    424             }
    425         }
    426         if (DEBUG) Log.d(TAG, "Preferred profiles = " + preferredProfiles);
    427 
    428         if (preferredProfiles == 0) {
    429             connectAllProfiles();
    430         }
    431     }
    432 
    433     private void connectAllProfiles() {
    434         if (!ensurePaired()) return;
    435 
    436         // Reset the only-show-one-error-dialog tracking variable
    437         mIsConnectingErrorPossible = true;
    438 
    439         for (Profile profile : mProfiles) {
    440             if (isConnectableProfile(profile)) {
    441                 LocalBluetoothProfileManager profileManager = LocalBluetoothProfileManager
    442                         .getProfileManager(mLocalManager, profile);
    443                 profileManager.setPreferred(mDevice, false);
    444                 disconnectConnected(profile);
    445                 queueCommand(new BluetoothJob(BluetoothCommand.CONNECT, this, profile));
    446             }
    447         }
    448     }
    449 
    450     public void connect(Profile profile) {
    451         mConnectAttempted = SystemClock.elapsedRealtime();
    452         // Reset the only-show-one-error-dialog tracking variable
    453         mIsConnectingErrorPossible = true;
    454         disconnectConnected(profile);
    455         queueCommand(new BluetoothJob(BluetoothCommand.CONNECT, this, profile));
    456     }
    457 
    458     private void disconnectConnected(Profile profile) {
    459         LocalBluetoothProfileManager profileManager =
    460             LocalBluetoothProfileManager.getProfileManager(mLocalManager, profile);
    461         CachedBluetoothDeviceManager cachedDeviceManager = mLocalManager.getCachedDeviceManager();
    462         Set<BluetoothDevice> devices = profileManager.getConnectedDevices();
    463         if (devices == null) return;
    464         for (BluetoothDevice device : devices) {
    465             CachedBluetoothDevice cachedDevice = cachedDeviceManager.findDevice(device);
    466             if (cachedDevice != null) {
    467                 queueCommand(new BluetoothJob(BluetoothCommand.DISCONNECT, cachedDevice, profile));
    468             }
    469         }
    470     }
    471 
    472     private boolean connectInt(CachedBluetoothDevice cachedDevice, Profile profile) {
    473         if (!cachedDevice.ensurePaired()) return false;
    474 
    475         LocalBluetoothProfileManager profileManager =
    476                 LocalBluetoothProfileManager.getProfileManager(mLocalManager, profile);
    477         int status = profileManager.getConnectionStatus(cachedDevice.mDevice);
    478         if (!SettingsBtStatus.isConnectionStatusConnected(status)) {
    479             if (profileManager.connect(cachedDevice.mDevice)) {
    480                 return true;
    481             }
    482             Log.i(TAG, "Failed to connect " + profile.toString() + " to " + cachedDevice.mName);
    483         } else {
    484             Log.i(TAG, "Already connected");
    485         }
    486         return false;
    487     }
    488 
    489     public void showConnectingError() {
    490         if (!mIsConnectingErrorPossible) return;
    491         mIsConnectingErrorPossible = false;
    492 
    493         mLocalManager.showError(mDevice, R.string.bluetooth_error_title,
    494                 R.string.bluetooth_connecting_error_message);
    495     }
    496 
    497     private boolean ensurePaired() {
    498         if (getBondState() == BluetoothDevice.BOND_NONE) {
    499             pair();
    500             return false;
    501         } else {
    502             return true;
    503         }
    504     }
    505 
    506     public void pair() {
    507         BluetoothAdapter adapter = mLocalManager.getBluetoothAdapter();
    508 
    509         // Pairing is unreliable while scanning, so cancel discovery
    510         if (adapter.isDiscovering()) {
    511             adapter.cancelDiscovery();
    512         }
    513 
    514         if (!mDevice.createBond()) {
    515             mLocalManager.showError(mDevice, R.string.bluetooth_error_title,
    516                     R.string.bluetooth_pairing_error_message);
    517         }
    518     }
    519 
    520     public void unpair() {
    521         disconnect();
    522 
    523         int state = getBondState();
    524 
    525         if (state == BluetoothDevice.BOND_BONDING) {
    526             mDevice.cancelBondProcess();
    527         }
    528 
    529         if (state != BluetoothDevice.BOND_NONE) {
    530             queueCommand(new BluetoothJob(BluetoothCommand.REMOVE_BOND, this, null));
    531         }
    532     }
    533 
    534     private void fillData() {
    535         fetchName();
    536         fetchBtClass();
    537         updateProfiles();
    538 
    539         mVisible = false;
    540 
    541         dispatchAttributesChanged();
    542     }
    543 
    544     public BluetoothDevice getDevice() {
    545         return mDevice;
    546     }
    547 
    548     public String getName() {
    549         return mName;
    550     }
    551 
    552     public void setName(String name) {
    553         if (!mName.equals(name)) {
    554             if (TextUtils.isEmpty(name)) {
    555                 mName = mDevice.getAddress();
    556             } else {
    557                 mName = name;
    558             }
    559             dispatchAttributesChanged();
    560         }
    561     }
    562 
    563     public void refreshName() {
    564         fetchName();
    565         dispatchAttributesChanged();
    566     }
    567 
    568     private void fetchName() {
    569         mName = mDevice.getName();
    570 
    571         if (TextUtils.isEmpty(mName)) {
    572             mName = mDevice.getAddress();
    573             if (DEBUG) Log.d(TAG, "Default to address. Device has no name (yet) " + mName);
    574         }
    575     }
    576 
    577     public void refresh() {
    578         dispatchAttributesChanged();
    579     }
    580 
    581     public boolean isVisible() {
    582         return mVisible;
    583     }
    584 
    585     void setVisible(boolean visible) {
    586         if (mVisible != visible) {
    587             mVisible = visible;
    588             dispatchAttributesChanged();
    589         }
    590     }
    591 
    592     public int getBondState() {
    593         return mDevice.getBondState();
    594     }
    595 
    596     void setRssi(short rssi) {
    597         if (mRssi != rssi) {
    598             mRssi = rssi;
    599             dispatchAttributesChanged();
    600         }
    601     }
    602 
    603     /**
    604      * Checks whether we are connected to this device (any profile counts).
    605      *
    606      * @return Whether it is connected.
    607      */
    608     public boolean isConnected() {
    609         for (Profile profile : mProfiles) {
    610             int status = LocalBluetoothProfileManager.getProfileManager(mLocalManager, profile)
    611                     .getConnectionStatus(mDevice);
    612             if (SettingsBtStatus.isConnectionStatusConnected(status)) {
    613                 return true;
    614             }
    615         }
    616 
    617         return false;
    618     }
    619 
    620     public boolean isBusy() {
    621         for (Profile profile : mProfiles) {
    622             int status = LocalBluetoothProfileManager.getProfileManager(mLocalManager, profile)
    623                     .getConnectionStatus(mDevice);
    624             if (SettingsBtStatus.isConnectionStatusBusy(status)) {
    625                 return true;
    626             }
    627         }
    628 
    629         if (getBondState() == BluetoothDevice.BOND_BONDING) {
    630             return true;
    631         }
    632 
    633         return false;
    634     }
    635 
    636     public int getBtClassDrawable() {
    637         if (mBtClass != null) {
    638             switch (mBtClass.getMajorDeviceClass()) {
    639             case BluetoothClass.Device.Major.COMPUTER:
    640                 return R.drawable.ic_bt_laptop;
    641 
    642             case BluetoothClass.Device.Major.PHONE:
    643                 return R.drawable.ic_bt_cellphone;
    644             }
    645         } else {
    646             Log.w(TAG, "mBtClass is null");
    647         }
    648 
    649         if (mProfiles.size() > 0) {
    650             if (mProfiles.contains(Profile.A2DP)) {
    651                 return R.drawable.ic_bt_headphones_a2dp;
    652             } else if (mProfiles.contains(Profile.HEADSET)) {
    653                 return R.drawable.ic_bt_headset_hfp;
    654             }
    655         } else if (mBtClass != null) {
    656             if (mBtClass.doesClassMatch(BluetoothClass.PROFILE_A2DP)) {
    657                 return R.drawable.ic_bt_headphones_a2dp;
    658 
    659             }
    660             if (mBtClass.doesClassMatch(BluetoothClass.PROFILE_HEADSET)) {
    661                 return R.drawable.ic_bt_headset_hfp;
    662             }
    663         }
    664         return 0;
    665     }
    666 
    667     /**
    668      * Fetches a new value for the cached BT class.
    669      */
    670     private void fetchBtClass() {
    671         mBtClass = mDevice.getBluetoothClass();
    672     }
    673 
    674     private boolean updateProfiles() {
    675         ParcelUuid[] uuids = mDevice.getUuids();
    676         if (uuids == null) return false;
    677 
    678         LocalBluetoothProfileManager.updateProfiles(uuids, mProfiles);
    679 
    680         if (DEBUG) {
    681             Log.e(TAG, "updating profiles for " + mDevice.getName());
    682 
    683             boolean printUuids = true;
    684             BluetoothClass bluetoothClass = mDevice.getBluetoothClass();
    685 
    686             if (bluetoothClass != null) {
    687                 if (bluetoothClass.doesClassMatch(BluetoothClass.PROFILE_HEADSET) !=
    688                     mProfiles.contains(Profile.HEADSET)) {
    689                     Log.v(TAG, "headset classbits != uuid");
    690                     printUuids = true;
    691                 }
    692 
    693                 if (bluetoothClass.doesClassMatch(BluetoothClass.PROFILE_A2DP) !=
    694                     mProfiles.contains(Profile.A2DP)) {
    695                     Log.v(TAG, "a2dp classbits != uuid");
    696                     printUuids = true;
    697                 }
    698 
    699                 if (bluetoothClass.doesClassMatch(BluetoothClass.PROFILE_OPP) !=
    700                     mProfiles.contains(Profile.OPP)) {
    701                     Log.v(TAG, "opp classbits != uuid");
    702                     printUuids = true;
    703                 }
    704             }
    705 
    706             if (printUuids) {
    707                 if (bluetoothClass != null) Log.v(TAG, "Class: " + bluetoothClass.toString());
    708                 Log.v(TAG, "UUID:");
    709                 for (int i = 0; i < uuids.length; i++) {
    710                     Log.v(TAG, "  " + uuids[i]);
    711                 }
    712             }
    713         }
    714         return true;
    715     }
    716 
    717     /**
    718      * Refreshes the UI for the BT class, including fetching the latest value
    719      * for the class.
    720      */
    721     public void refreshBtClass() {
    722         fetchBtClass();
    723         dispatchAttributesChanged();
    724     }
    725 
    726     /**
    727      * Refreshes the UI when framework alerts us of a UUID change.
    728      */
    729     public void onUuidChanged() {
    730         updateProfiles();
    731 
    732         if (DEBUG) {
    733             Log.e(TAG, "onUuidChanged: Time since last connect"
    734                     + (SystemClock.elapsedRealtime() - mConnectAttempted));
    735         }
    736 
    737         /*
    738          * If a connect was attempted earlier without any UUID, we will do the
    739          * connect now.
    740          */
    741         if (mProfiles.size() > 0
    742                 && (mConnectAttempted + MAX_UUID_DELAY_FOR_AUTO_CONNECT) > SystemClock
    743                         .elapsedRealtime()) {
    744             connectWithoutResettingTimer();
    745         }
    746         dispatchAttributesChanged();
    747     }
    748 
    749     public void onBondingStateChanged(int bondState) {
    750         if (bondState == BluetoothDevice.BOND_NONE) {
    751             mProfiles.clear();
    752 
    753             BluetoothJob job = workQueue.peek();
    754             if (job != null) {
    755                 // Remove the first item and process the next one
    756                 if (job.command == BluetoothCommand.REMOVE_BOND
    757                         && job.cachedDevice.mDevice.equals(mDevice)) {
    758                     workQueue.poll(); // dequeue
    759                 } else {
    760                     // Unexpected job
    761                     if (D) {
    762                         Log.d(TAG, "job.command = " + job.command);
    763                         Log.d(TAG, "mDevice:" + mDevice + " != head:" + job.toString());
    764                     }
    765 
    766                     // Check to see if we need to remove the stale items from the queue
    767                     if (!pruneQueue(null)) {
    768                         // nothing in the queue was modify. Just ignore the notification and return.
    769                         refresh();
    770                         return;
    771                     }
    772                 }
    773 
    774                 processCommands();
    775             }
    776         }
    777 
    778         refresh();
    779     }
    780 
    781     public void setBtClass(BluetoothClass btClass) {
    782         if (btClass != null && mBtClass != btClass) {
    783             mBtClass = btClass;
    784             dispatchAttributesChanged();
    785         }
    786     }
    787 
    788     public int getSummary() {
    789         // TODO: clean up
    790         int oneOffSummary = getOneOffSummary();
    791         if (oneOffSummary != 0) {
    792             return oneOffSummary;
    793         }
    794 
    795         for (Profile profile : mProfiles) {
    796             LocalBluetoothProfileManager profileManager = LocalBluetoothProfileManager
    797                     .getProfileManager(mLocalManager, profile);
    798             int connectionStatus = profileManager.getConnectionStatus(mDevice);
    799 
    800             if (SettingsBtStatus.isConnectionStatusConnected(connectionStatus) ||
    801                     connectionStatus == SettingsBtStatus.CONNECTION_STATUS_CONNECTING ||
    802                     connectionStatus == SettingsBtStatus.CONNECTION_STATUS_DISCONNECTING) {
    803                 return SettingsBtStatus.getConnectionStatusSummary(connectionStatus);
    804             }
    805         }
    806 
    807         return SettingsBtStatus.getPairingStatusSummary(getBondState());
    808     }
    809 
    810     /**
    811      * We have special summaries when particular profiles are connected. This
    812      * checks for those states and returns an applicable summary.
    813      *
    814      * @return A one-off summary that is applicable for the current state, or 0.
    815      */
    816     private int getOneOffSummary() {
    817         boolean isA2dpConnected = false, isHeadsetConnected = false, isConnecting = false;
    818 
    819         if (mProfiles.contains(Profile.A2DP)) {
    820             LocalBluetoothProfileManager profileManager = LocalBluetoothProfileManager
    821                     .getProfileManager(mLocalManager, Profile.A2DP);
    822             isConnecting = profileManager.getConnectionStatus(mDevice) ==
    823                     SettingsBtStatus.CONNECTION_STATUS_CONNECTING;
    824             isA2dpConnected = profileManager.isConnected(mDevice);
    825         }
    826 
    827         if (mProfiles.contains(Profile.HEADSET)) {
    828             LocalBluetoothProfileManager profileManager = LocalBluetoothProfileManager
    829                     .getProfileManager(mLocalManager, Profile.HEADSET);
    830             isConnecting |= profileManager.getConnectionStatus(mDevice) ==
    831                     SettingsBtStatus.CONNECTION_STATUS_CONNECTING;
    832             isHeadsetConnected = profileManager.isConnected(mDevice);
    833         }
    834 
    835         if (isConnecting) {
    836             // If any of these important profiles is connecting, prefer that
    837             return SettingsBtStatus.getConnectionStatusSummary(
    838                     SettingsBtStatus.CONNECTION_STATUS_CONNECTING);
    839         } else if (isA2dpConnected && isHeadsetConnected) {
    840             return R.string.bluetooth_summary_connected_to_a2dp_headset;
    841         } else if (isA2dpConnected) {
    842             return R.string.bluetooth_summary_connected_to_a2dp;
    843         } else if (isHeadsetConnected) {
    844             return R.string.bluetooth_summary_connected_to_headset;
    845         } else {
    846             return 0;
    847         }
    848     }
    849 
    850     public List<Profile> getConnectableProfiles() {
    851         ArrayList<Profile> connectableProfiles = new ArrayList<Profile>();
    852         for (Profile profile : mProfiles) {
    853             if (isConnectableProfile(profile)) {
    854                 connectableProfiles.add(profile);
    855             }
    856         }
    857         return connectableProfiles;
    858     }
    859 
    860     private boolean isConnectableProfile(Profile profile) {
    861         return profile.equals(Profile.HEADSET) || profile.equals(Profile.A2DP);
    862     }
    863 
    864     public void onCreateContextMenu(ContextMenu menu) {
    865         // No context menu if it is busy (none of these items are applicable if busy)
    866         if (mLocalManager.getBluetoothState() != BluetoothAdapter.STATE_ON || isBusy()) {
    867             return;
    868         }
    869 
    870         int bondState = getBondState();
    871         boolean isConnected = isConnected();
    872         boolean hasConnectableProfiles = false;
    873 
    874         for (Profile profile : mProfiles) {
    875             if (isConnectableProfile(profile)) {
    876                 hasConnectableProfiles = true;
    877                 break;
    878             }
    879         }
    880 
    881         menu.setHeaderTitle(getName());
    882 
    883         if (bondState == BluetoothDevice.BOND_NONE) { // Not paired and not connected
    884             menu.add(0, CONTEXT_ITEM_CONNECT, 0, R.string.bluetooth_device_context_pair_connect);
    885         } else { // Paired
    886             if (isConnected) { // Paired and connected
    887                 menu.add(0, CONTEXT_ITEM_DISCONNECT, 0,
    888                         R.string.bluetooth_device_context_disconnect);
    889                 menu.add(0, CONTEXT_ITEM_UNPAIR, 0,
    890                         R.string.bluetooth_device_context_disconnect_unpair);
    891             } else { // Paired but not connected
    892                 if (hasConnectableProfiles) {
    893                     menu.add(0, CONTEXT_ITEM_CONNECT, 0, R.string.bluetooth_device_context_connect);
    894                 }
    895                 menu.add(0, CONTEXT_ITEM_UNPAIR, 0, R.string.bluetooth_device_context_unpair);
    896             }
    897 
    898             // Show the connection options item
    899             if (hasConnectableProfiles) {
    900                 menu.add(0, CONTEXT_ITEM_CONNECT_ADVANCED, 0,
    901                         R.string.bluetooth_device_context_connect_advanced);
    902             }
    903         }
    904     }
    905 
    906     /**
    907      * Called when a context menu item is clicked.
    908      *
    909      * @param item The item that was clicked.
    910      */
    911     public void onContextItemSelected(MenuItem item) {
    912         switch (item.getItemId()) {
    913             case CONTEXT_ITEM_DISCONNECT:
    914                 disconnect();
    915                 break;
    916 
    917             case CONTEXT_ITEM_CONNECT:
    918                 connect();
    919                 break;
    920 
    921             case CONTEXT_ITEM_UNPAIR:
    922                 unpair();
    923                 break;
    924 
    925             case CONTEXT_ITEM_CONNECT_ADVANCED:
    926                 Intent intent = new Intent();
    927                 // Need an activity context to open this in our task
    928                 Context context = mLocalManager.getForegroundActivity();
    929                 if (context == null) {
    930                     // Fallback on application context, and open in a new task
    931                     context = mLocalManager.getContext();
    932                     intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    933                 }
    934                 intent.setClass(context, ConnectSpecificProfilesActivity.class);
    935                 intent.putExtra(ConnectSpecificProfilesActivity.EXTRA_DEVICE, mDevice);
    936                 context.startActivity(intent);
    937                 break;
    938         }
    939     }
    940 
    941     public void registerCallback(Callback callback) {
    942         synchronized (mCallbacks) {
    943             mCallbacks.add(callback);
    944         }
    945     }
    946 
    947     public void unregisterCallback(Callback callback) {
    948         synchronized (mCallbacks) {
    949             mCallbacks.remove(callback);
    950         }
    951     }
    952 
    953     private void dispatchAttributesChanged() {
    954         synchronized (mCallbacks) {
    955             for (Callback callback : mCallbacks) {
    956                 callback.onDeviceAttributesChanged(this);
    957             }
    958         }
    959     }
    960 
    961     @Override
    962     public String toString() {
    963         return mDevice.toString();
    964     }
    965 
    966     @Override
    967     public boolean equals(Object o) {
    968         if ((o == null) || !(o instanceof CachedBluetoothDevice)) {
    969             throw new ClassCastException();
    970         }
    971 
    972         return mDevice.equals(((CachedBluetoothDevice) o).mDevice);
    973     }
    974 
    975     @Override
    976     public int hashCode() {
    977         return mDevice.getAddress().hashCode();
    978     }
    979 
    980     public int compareTo(CachedBluetoothDevice another) {
    981         int comparison;
    982 
    983         // Connected above not connected
    984         comparison = (another.isConnected() ? 1 : 0) - (isConnected() ? 1 : 0);
    985         if (comparison != 0) return comparison;
    986 
    987         // Paired above not paired
    988         comparison = (another.getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0) -
    989             (getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0);
    990         if (comparison != 0) return comparison;
    991 
    992         // Visible above not visible
    993         comparison = (another.mVisible ? 1 : 0) - (mVisible ? 1 : 0);
    994         if (comparison != 0) return comparison;
    995 
    996         // Stronger signal above weaker signal
    997         comparison = another.mRssi - mRssi;
    998         if (comparison != 0) return comparison;
    999 
   1000         // Fallback on name
   1001         return getName().compareTo(another.getName());
   1002     }
   1003 
   1004     public interface Callback {
   1005         void onDeviceAttributesChanged(CachedBluetoothDevice cachedDevice);
   1006     }
   1007 }
   1008