Home | History | Annotate | Download | only in handover
      1 /*
      2  * Copyright (C) 2012 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.nfc.handover;
     18 
     19 import android.bluetooth.BluetoothA2dp;
     20 import android.bluetooth.BluetoothAdapter;
     21 import android.bluetooth.BluetoothClass;
     22 import android.bluetooth.BluetoothDevice;
     23 import android.bluetooth.BluetoothHeadset;
     24 import android.bluetooth.BluetoothHidHost;
     25 import android.bluetooth.BluetoothProfile;
     26 import android.bluetooth.BluetoothUuid;
     27 import android.bluetooth.OobData;
     28 import android.content.BroadcastReceiver;
     29 import android.content.ContentResolver;
     30 import android.content.Context;
     31 import android.content.Intent;
     32 import android.content.IntentFilter;
     33 import android.media.session.MediaSessionLegacyHelper;
     34 import android.os.Handler;
     35 import android.os.Looper;
     36 import android.os.Message;
     37 import android.os.ParcelUuid;
     38 import android.provider.Settings;
     39 import android.util.Log;
     40 import android.view.KeyEvent;
     41 import android.widget.Toast;
     42 
     43 import com.android.nfc.R;
     44 
     45 /**
     46  * Connects / Disconnects from a Bluetooth headset (or any device that
     47  * might implement BT HSP, HFP, A2DP, or HOGP sink) when touched with NFC.
     48  *
     49  * This object is created on an NFC interaction, and determines what
     50  * sequence of Bluetooth actions to take, and executes them. It is not
     51  * designed to be re-used after the sequence has completed or timed out.
     52  * Subsequent NFC interactions should use new objects.
     53  *
     54  */
     55 public class BluetoothPeripheralHandover implements BluetoothProfile.ServiceListener {
     56     static final String TAG = "BluetoothPeripheralHandover";
     57     static final boolean DBG = false;
     58 
     59     static final String ACTION_ALLOW_CONNECT = "com.android.nfc.handover.action.ALLOW_CONNECT";
     60     static final String ACTION_DENY_CONNECT = "com.android.nfc.handover.action.DENY_CONNECT";
     61     static final String ACTION_TIMEOUT_CONNECT = "com.android.nfc.handover.action.TIMEOUT_CONNECT";
     62 
     63     static final int TIMEOUT_MS = 20000;
     64     static final int RETRY_PAIRING_WAIT_TIME_MS = 2000;
     65     static final int RETRY_CONNECT_WAIT_TIME_MS = 5000;
     66 
     67     static final int STATE_INIT = 0;
     68     static final int STATE_WAITING_FOR_PROXIES = 1;
     69     static final int STATE_INIT_COMPLETE = 2;
     70     static final int STATE_WAITING_FOR_BOND_CONFIRMATION = 3;
     71     static final int STATE_BONDING = 4;
     72     static final int STATE_CONNECTING = 5;
     73     static final int STATE_DISCONNECTING = 6;
     74     static final int STATE_COMPLETE = 7;
     75 
     76     static final int RESULT_PENDING = 0;
     77     static final int RESULT_CONNECTED = 1;
     78     static final int RESULT_DISCONNECTED = 2;
     79 
     80     static final int ACTION_INIT = 0;
     81     static final int ACTION_DISCONNECT = 1;
     82     static final int ACTION_CONNECT = 2;
     83 
     84     static final int MSG_TIMEOUT = 1;
     85     static final int MSG_NEXT_STEP = 2;
     86     static final int MSG_RETRY = 3;
     87 
     88     static final int MAX_RETRY_COUNT = 3;
     89 
     90     final Context mContext;
     91     final BluetoothDevice mDevice;
     92     final String mName;
     93     final Callback mCallback;
     94     final BluetoothAdapter mBluetoothAdapter;
     95     final int mTransport;
     96     final boolean mProvisioning;
     97 
     98     final Object mLock = new Object();
     99 
    100     // only used on main thread
    101     int mAction;
    102     int mState;
    103     int mHfpResult;  // used only in STATE_CONNECTING and STATE_DISCONNETING
    104     int mA2dpResult; // used only in STATE_CONNECTING and STATE_DISCONNETING
    105     int mHidResult;
    106     int mRetryCount;
    107     OobData mOobData;
    108     boolean mIsHeadsetAvailable;
    109     boolean mIsA2dpAvailable;
    110 
    111     // protected by mLock
    112     BluetoothA2dp mA2dp;
    113     BluetoothHeadset mHeadset;
    114     BluetoothHidHost mInput;
    115 
    116     public interface Callback {
    117         public void onBluetoothPeripheralHandoverComplete(boolean connected);
    118     }
    119 
    120     public BluetoothPeripheralHandover(Context context, BluetoothDevice device, String name,
    121             int transport, OobData oobData, ParcelUuid[] uuids, BluetoothClass btClass,
    122             Callback callback) {
    123         checkMainThread();  // mHandler must get get constructed on Main Thread for toasts to work
    124         mContext = context;
    125         mDevice = device;
    126         mName = name;
    127         mTransport = transport;
    128         mOobData = oobData;
    129         mCallback = callback;
    130         mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
    131 
    132         ContentResolver contentResolver = mContext.getContentResolver();
    133         mProvisioning = Settings.Secure.getInt(contentResolver,
    134                 Settings.Global.DEVICE_PROVISIONED, 0) == 0;
    135 
    136         mIsHeadsetAvailable = hasHeadsetCapability(uuids, btClass);
    137         mIsA2dpAvailable = hasA2dpCapability(uuids, btClass);
    138 
    139         // Capability information is from NDEF optional field, then it might be empty.
    140         // If all capabilities indicate false, try to connect Headset and A2dp just in case.
    141         if (!mIsHeadsetAvailable && !mIsA2dpAvailable) {
    142             mIsHeadsetAvailable = true;
    143             mIsA2dpAvailable = true;
    144         }
    145 
    146         mState = STATE_INIT;
    147     }
    148 
    149     public boolean hasStarted() {
    150         return mState != STATE_INIT;
    151     }
    152 
    153     /**
    154      * Main entry point. This method is usually called after construction,
    155      * to begin the BT sequence. Must be called on Main thread.
    156      */
    157     public boolean start() {
    158         checkMainThread();
    159         if (mState != STATE_INIT || mBluetoothAdapter == null
    160                 || (mProvisioning && mTransport != BluetoothDevice.TRANSPORT_LE)) {
    161             return false;
    162         }
    163 
    164 
    165         IntentFilter filter = new IntentFilter();
    166         filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
    167         filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
    168         filter.addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED);
    169         filter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
    170         filter.addAction(BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED);
    171         filter.addAction(ACTION_ALLOW_CONNECT);
    172         filter.addAction(ACTION_DENY_CONNECT);
    173 
    174         mContext.registerReceiver(mReceiver, filter);
    175 
    176         mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_TIMEOUT), TIMEOUT_MS);
    177 
    178         mAction = ACTION_INIT;
    179         mRetryCount = 0;
    180 
    181         nextStep();
    182 
    183         return true;
    184     }
    185 
    186     /**
    187      * Called to execute next step in state machine
    188      */
    189     void nextStep() {
    190         if (mAction == ACTION_INIT) {
    191             nextStepInit();
    192         } else if (mAction == ACTION_CONNECT) {
    193             nextStepConnect();
    194         } else {
    195             nextStepDisconnect();
    196         }
    197     }
    198 
    199     /*
    200      * Enables bluetooth and gets the profile proxies
    201      */
    202     void nextStepInit() {
    203         switch (mState) {
    204             case STATE_INIT:
    205                 if (mA2dp == null || mHeadset == null || mInput == null) {
    206                     mState = STATE_WAITING_FOR_PROXIES;
    207                     if (!getProfileProxys()) {
    208                         complete(false);
    209                     }
    210                     break;
    211                 }
    212                 // fall-through
    213             case STATE_WAITING_FOR_PROXIES:
    214                 mState = STATE_INIT_COMPLETE;
    215                 // Check connected devices and see if we need to disconnect
    216                 synchronized(mLock) {
    217                     if (mTransport == BluetoothDevice.TRANSPORT_LE) {
    218                         if (mInput.getConnectedDevices().contains(mDevice)) {
    219                             Log.i(TAG, "ACTION_DISCONNECT addr=" + mDevice + " name=" + mName);
    220                             mAction = ACTION_DISCONNECT;
    221                         } else {
    222                             Log.i(TAG, "ACTION_CONNECT addr=" + mDevice + " name=" + mName);
    223                             mAction = ACTION_CONNECT;
    224                         }
    225                     } else {
    226                         if (mA2dp.getConnectedDevices().contains(mDevice) ||
    227                                 mHeadset.getConnectedDevices().contains(mDevice)) {
    228                             Log.i(TAG, "ACTION_DISCONNECT addr=" + mDevice + " name=" + mName);
    229                             mAction = ACTION_DISCONNECT;
    230                         } else {
    231                             // Check if each profile of the device is disabled or not
    232                             if (mHeadset.getPriority(mDevice) == BluetoothProfile.PRIORITY_OFF) {
    233                                 mIsHeadsetAvailable = false;
    234                             }
    235                             if (mA2dp.getPriority(mDevice) == BluetoothProfile.PRIORITY_OFF) {
    236                                 mIsA2dpAvailable = false;
    237                             }
    238                             if (!mIsHeadsetAvailable && !mIsA2dpAvailable) {
    239                                 Log.i(TAG, "Both Headset and A2DP profiles are unavailable");
    240                                 complete(false);
    241                                 break;
    242                             }
    243                             Log.i(TAG, "ACTION_CONNECT addr=" + mDevice + " name=" + mName);
    244                             mAction = ACTION_CONNECT;
    245                         }
    246                     }
    247                 }
    248                 nextStep();
    249         }
    250 
    251     }
    252 
    253     void nextStepDisconnect() {
    254         switch (mState) {
    255             case STATE_INIT_COMPLETE:
    256                 mState = STATE_DISCONNECTING;
    257                 synchronized (mLock) {
    258                     if (mTransport == BluetoothDevice.TRANSPORT_LE) {
    259                         if (mInput.getConnectionState(mDevice)
    260                                 != BluetoothProfile.STATE_DISCONNECTED) {
    261                             mHidResult = RESULT_PENDING;
    262                             mInput.disconnect(mDevice);
    263                             toast(getToastString(R.string.disconnecting_peripheral));
    264                             break;
    265                         } else {
    266                             mHidResult = RESULT_DISCONNECTED;
    267                         }
    268                     } else {
    269                         if (mHeadset.getConnectionState(mDevice)
    270                                 != BluetoothProfile.STATE_DISCONNECTED) {
    271                             mHfpResult = RESULT_PENDING;
    272                             mHeadset.disconnect(mDevice);
    273                         } else {
    274                             mHfpResult = RESULT_DISCONNECTED;
    275                         }
    276                         if (mA2dp.getConnectionState(mDevice)
    277                                 != BluetoothProfile.STATE_DISCONNECTED) {
    278                             mA2dpResult = RESULT_PENDING;
    279                             mA2dp.disconnect(mDevice);
    280                         } else {
    281                             mA2dpResult = RESULT_DISCONNECTED;
    282                         }
    283                         if (mA2dpResult == RESULT_PENDING || mHfpResult == RESULT_PENDING) {
    284                             toast(getToastString(R.string.disconnecting_peripheral));
    285                             break;
    286                         }
    287                     }
    288                 }
    289                 // fall-through
    290             case STATE_DISCONNECTING:
    291                 if (mTransport == BluetoothDevice.TRANSPORT_LE) {
    292                     if (mHidResult == RESULT_DISCONNECTED) {
    293                         toast(getToastString(R.string.disconnected_peripheral));
    294                         complete(false);
    295                     }
    296 
    297                     break;
    298                 } else {
    299                     if (mA2dpResult == RESULT_PENDING || mHfpResult == RESULT_PENDING) {
    300                         // still disconnecting
    301                         break;
    302                     }
    303                     if (mA2dpResult == RESULT_DISCONNECTED && mHfpResult == RESULT_DISCONNECTED) {
    304                         toast(getToastString(R.string.disconnected_peripheral));
    305                     }
    306                     complete(false);
    307                     break;
    308                 }
    309 
    310         }
    311 
    312     }
    313 
    314     private String getToastString(int resid) {
    315         return mContext.getString(resid, mName != null ? mName : R.string.device);
    316     }
    317 
    318     boolean getProfileProxys() {
    319 
    320         if (mTransport == BluetoothDevice.TRANSPORT_LE) {
    321             if (!mBluetoothAdapter.getProfileProxy(mContext, this, BluetoothProfile.HID_HOST))
    322                 return false;
    323         } else {
    324             if(!mBluetoothAdapter.getProfileProxy(mContext, this, BluetoothProfile.HEADSET))
    325                 return false;
    326 
    327             if(!mBluetoothAdapter.getProfileProxy(mContext, this, BluetoothProfile.A2DP))
    328                 return false;
    329         }
    330 
    331         return true;
    332     }
    333 
    334     void nextStepConnect() {
    335         switch (mState) {
    336             case STATE_INIT_COMPLETE:
    337 
    338                 if (mDevice.getBondState() != BluetoothDevice.BOND_BONDED) {
    339                     requestPairConfirmation();
    340                     mState = STATE_WAITING_FOR_BOND_CONFIRMATION;
    341                     break;
    342                 }
    343 
    344                 if (mTransport == BluetoothDevice.TRANSPORT_LE) {
    345                     if (mDevice.getBondState() != BluetoothDevice.BOND_NONE) {
    346                         mDevice.removeBond();
    347                         requestPairConfirmation();
    348                         mState = STATE_WAITING_FOR_BOND_CONFIRMATION;
    349                         break;
    350                     }
    351                 }
    352                 // fall-through
    353             case STATE_WAITING_FOR_BOND_CONFIRMATION:
    354                 if (mDevice.getBondState() != BluetoothDevice.BOND_BONDED) {
    355                     startBonding();
    356                     break;
    357                 }
    358                 // fall-through
    359             case STATE_BONDING:
    360                 // Bluetooth Profile service will correctly serialize
    361                 // HFP then A2DP connect
    362                 mState = STATE_CONNECTING;
    363                 synchronized (mLock) {
    364                     if (mTransport == BluetoothDevice.TRANSPORT_LE) {
    365                         if (mInput.getConnectionState(mDevice)
    366                                 != BluetoothProfile.STATE_CONNECTED) {
    367                             mHidResult = RESULT_PENDING;
    368                             toast(getToastString(R.string.connecting_peripheral));
    369                             break;
    370                         } else {
    371                             mHidResult = RESULT_CONNECTED;
    372                         }
    373                     } else {
    374                         if (mHeadset.getConnectionState(mDevice) !=
    375                                 BluetoothProfile.STATE_CONNECTED) {
    376                             if (mIsHeadsetAvailable) {
    377                                 mHfpResult = RESULT_PENDING;
    378                                 mHeadset.connect(mDevice);
    379                             } else {
    380                                 mHfpResult = RESULT_DISCONNECTED;
    381                             }
    382                         } else {
    383                             mHfpResult = RESULT_CONNECTED;
    384                         }
    385                         if (mA2dp.getConnectionState(mDevice) != BluetoothProfile.STATE_CONNECTED) {
    386                             if (mIsA2dpAvailable) {
    387                                 mA2dpResult = RESULT_PENDING;
    388                                 mA2dp.connect(mDevice);
    389                             } else {
    390                                 mA2dpResult = RESULT_DISCONNECTED;
    391                             }
    392                         } else {
    393                             mA2dpResult = RESULT_CONNECTED;
    394                         }
    395                         if (mA2dpResult == RESULT_PENDING || mHfpResult == RESULT_PENDING) {
    396                             if (mRetryCount == 0) {
    397                                 toast(getToastString(R.string.connecting_peripheral));
    398                             }
    399                             if (mRetryCount < MAX_RETRY_COUNT) {
    400                                 sendRetryMessage(RETRY_CONNECT_WAIT_TIME_MS);
    401                                 break;
    402                             }
    403                         }
    404                     }
    405                 }
    406                 // fall-through
    407             case STATE_CONNECTING:
    408                 if (mTransport == BluetoothDevice.TRANSPORT_LE) {
    409                     if (mHidResult == RESULT_PENDING) {
    410                         break;
    411                     } else if (mHidResult == RESULT_CONNECTED) {
    412                         toast(getToastString(R.string.connected_peripheral));
    413                         mDevice.setAlias(mName);
    414                         complete(true);
    415                     } else {
    416                         toast (getToastString(R.string.connect_peripheral_failed));
    417                         complete(false);
    418                     }
    419                 } else {
    420                     if (mA2dpResult == RESULT_PENDING || mHfpResult == RESULT_PENDING) {
    421                         // another connection type still pending
    422                         break;
    423                     }
    424                     if (mA2dpResult == RESULT_CONNECTED || mHfpResult == RESULT_CONNECTED) {
    425                         // we'll take either as success
    426                         toast(getToastString(R.string.connected_peripheral));
    427                         if (mA2dpResult == RESULT_CONNECTED) startTheMusic();
    428                         mDevice.setAlias(mName);
    429                         complete(true);
    430                     } else {
    431                         toast (getToastString(R.string.connect_peripheral_failed));
    432                         complete(false);
    433                     }
    434                 }
    435                 break;
    436         }
    437     }
    438 
    439     void startBonding() {
    440         mState = STATE_BONDING;
    441         if (mRetryCount == 0) {
    442             toast(getToastString(R.string.pairing_peripheral));
    443         }
    444         if (mOobData != null) {
    445             if (!mDevice.createBondOutOfBand(mTransport, mOobData)) {
    446                 toast(getToastString(R.string.pairing_peripheral_failed));
    447                 complete(false);
    448             }
    449         } else if (!mDevice.createBond(mTransport)) {
    450                 toast(getToastString(R.string.pairing_peripheral_failed));
    451                 complete(false);
    452         }
    453     }
    454 
    455     void handleIntent(Intent intent) {
    456         String action = intent.getAction();
    457         // Everything requires the device to match...
    458         BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
    459         if (!mDevice.equals(device)) return;
    460 
    461         if (ACTION_ALLOW_CONNECT.equals(action)) {
    462             mHandler.removeMessages(MSG_TIMEOUT);
    463             mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_TIMEOUT), TIMEOUT_MS);
    464             nextStepConnect();
    465         } else if (ACTION_DENY_CONNECT.equals(action)) {
    466             complete(false);
    467         } else if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(action)
    468                 && mState == STATE_BONDING) {
    469             int bond = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE,
    470                     BluetoothAdapter.ERROR);
    471             if (bond == BluetoothDevice.BOND_BONDED) {
    472                 mRetryCount = 0;
    473                 nextStepConnect();
    474             } else if (bond == BluetoothDevice.BOND_NONE) {
    475                 if (mRetryCount < MAX_RETRY_COUNT) {
    476                     sendRetryMessage(RETRY_PAIRING_WAIT_TIME_MS);
    477                 } else {
    478                     toast(getToastString(R.string.pairing_peripheral_failed));
    479                     complete(false);
    480                 }
    481             }
    482         } else if (BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED.equals(action) &&
    483                 (mState == STATE_CONNECTING || mState == STATE_DISCONNECTING)) {
    484             int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothAdapter.ERROR);
    485             if (state == BluetoothProfile.STATE_CONNECTED) {
    486                 mHfpResult = RESULT_CONNECTED;
    487                 nextStep();
    488             } else if (state == BluetoothProfile.STATE_DISCONNECTED) {
    489                 if (mAction == ACTION_CONNECT && mRetryCount < MAX_RETRY_COUNT) {
    490                     sendRetryMessage(RETRY_CONNECT_WAIT_TIME_MS);
    491                 } else {
    492                     mHfpResult = RESULT_DISCONNECTED;
    493                     nextStep();
    494                 }
    495             }
    496         } else if (BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED.equals(action) &&
    497                 (mState == STATE_CONNECTING || mState == STATE_DISCONNECTING)) {
    498             int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothAdapter.ERROR);
    499             if (state == BluetoothProfile.STATE_CONNECTED) {
    500                 mA2dpResult = RESULT_CONNECTED;
    501                 nextStep();
    502             } else if (state == BluetoothProfile.STATE_DISCONNECTED) {
    503                 if (mAction == ACTION_CONNECT && mRetryCount < MAX_RETRY_COUNT) {
    504                     sendRetryMessage(RETRY_CONNECT_WAIT_TIME_MS);
    505                 } else {
    506                     mA2dpResult = RESULT_DISCONNECTED;
    507                     nextStep();
    508                 }
    509             }
    510         } else if (BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED.equals(action) &&
    511                 (mState == STATE_CONNECTING || mState == STATE_DISCONNECTING)) {
    512             int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothAdapter.ERROR);
    513             if (state == BluetoothProfile.STATE_CONNECTED) {
    514                 mHidResult = RESULT_CONNECTED;
    515                 nextStep();
    516             } else if (state == BluetoothProfile.STATE_DISCONNECTED) {
    517                 mHidResult = RESULT_DISCONNECTED;
    518                 nextStep();
    519             }
    520         }
    521     }
    522 
    523     void complete(boolean connected) {
    524         if (DBG) Log.d(TAG, "complete()");
    525         mState = STATE_COMPLETE;
    526         mContext.unregisterReceiver(mReceiver);
    527         mHandler.removeMessages(MSG_TIMEOUT);
    528         mHandler.removeMessages(MSG_RETRY);
    529         synchronized (mLock) {
    530             if (mA2dp != null) {
    531                 mBluetoothAdapter.closeProfileProxy(BluetoothProfile.A2DP, mA2dp);
    532             }
    533             if (mHeadset != null) {
    534                 mBluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, mHeadset);
    535             }
    536 
    537             if (mInput != null) {
    538                 mBluetoothAdapter.closeProfileProxy(BluetoothProfile.HID_HOST, mInput);
    539             }
    540 
    541             mA2dp = null;
    542             mHeadset = null;
    543             mInput = null;
    544         }
    545         mCallback.onBluetoothPeripheralHandoverComplete(connected);
    546     }
    547 
    548     void toast(CharSequence text) {
    549         Toast.makeText(mContext,  text, Toast.LENGTH_SHORT).show();
    550     }
    551 
    552     void startTheMusic() {
    553         MediaSessionLegacyHelper helper = MediaSessionLegacyHelper.getHelper(mContext);
    554         if (helper != null) {
    555             KeyEvent keyEvent = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY);
    556             helper.sendMediaButtonEvent(keyEvent, false);
    557             keyEvent = new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PLAY);
    558             helper.sendMediaButtonEvent(keyEvent, false);
    559         } else {
    560             Log.w(TAG, "Unable to send media key event");
    561         }
    562     }
    563 
    564     void requestPairConfirmation() {
    565         Intent dialogIntent = new Intent(mContext, ConfirmConnectActivity.class);
    566         dialogIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
    567         dialogIntent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice);
    568         dialogIntent.putExtra(BluetoothDevice.EXTRA_NAME, mName);
    569 
    570         mContext.startActivity(dialogIntent);
    571     }
    572 
    573     boolean hasA2dpCapability(ParcelUuid[] uuids, BluetoothClass btClass) {
    574         if (uuids != null) {
    575             for (ParcelUuid uuid : uuids) {
    576                 if (BluetoothUuid.isAudioSink(uuid) || BluetoothUuid.isAdvAudioDist(uuid)) {
    577                     return true;
    578                 }
    579             }
    580         }
    581         if (btClass != null && btClass.doesClassMatch(BluetoothClass.PROFILE_A2DP)) {
    582             return true;
    583         }
    584         return false;
    585     }
    586 
    587     boolean hasHeadsetCapability(ParcelUuid[] uuids, BluetoothClass btClass) {
    588         if (uuids != null) {
    589             for (ParcelUuid uuid : uuids) {
    590                 if (BluetoothUuid.isHandsfree(uuid) || BluetoothUuid.isHeadset(uuid)) {
    591                     return true;
    592                 }
    593             }
    594         }
    595         if (btClass != null && btClass.doesClassMatch(BluetoothClass.PROFILE_HEADSET)) {
    596             return true;
    597         }
    598         return false;
    599     }
    600 
    601     final Handler mHandler = new Handler() {
    602         @Override
    603         public void handleMessage(Message msg) {
    604             switch (msg.what) {
    605                 case MSG_TIMEOUT:
    606                     if (mState == STATE_COMPLETE) return;
    607                     Log.i(TAG, "Timeout completing BT handover");
    608                     if (mState == STATE_WAITING_FOR_BOND_CONFIRMATION) {
    609                         mContext.sendBroadcast(new Intent(ACTION_TIMEOUT_CONNECT));
    610                     } else if (mState == STATE_BONDING) {
    611                         toast(getToastString(R.string.pairing_peripheral_failed));
    612                     } else if (mState == STATE_CONNECTING) {
    613                         if (mHidResult == RESULT_PENDING) {
    614                             mHidResult = RESULT_DISCONNECTED;
    615                         }
    616                         if (mA2dpResult == RESULT_PENDING) {
    617                             mA2dpResult = RESULT_DISCONNECTED;
    618                         }
    619                         if (mHfpResult == RESULT_PENDING) {
    620                             mHfpResult = RESULT_DISCONNECTED;
    621                         }
    622                         // Check if any one profile is connected, then it takes as success
    623                         nextStepConnect();
    624                         break;
    625                     }
    626                     complete(false);
    627                     break;
    628                 case MSG_NEXT_STEP:
    629                     nextStep();
    630                     break;
    631                 case MSG_RETRY:
    632                     mHandler.removeMessages(MSG_RETRY);
    633                     if (mState == STATE_BONDING) {
    634                         mState = STATE_WAITING_FOR_BOND_CONFIRMATION;
    635                     } else if (mState == STATE_CONNECTING) {
    636                         mState = STATE_BONDING;
    637                     }
    638                     mRetryCount++;
    639                     nextStepConnect();
    640                     break;
    641             }
    642         }
    643     };
    644 
    645     final BroadcastReceiver mReceiver = new BroadcastReceiver() {
    646         @Override
    647         public void onReceive(Context context, Intent intent) {
    648             handleIntent(intent);
    649         }
    650     };
    651 
    652     static void checkMainThread() {
    653         if (Looper.myLooper() != Looper.getMainLooper()) {
    654             throw new IllegalThreadStateException("must be called on main thread");
    655         }
    656     }
    657 
    658     @Override
    659     public void onServiceConnected(int profile, BluetoothProfile proxy) {
    660         synchronized (mLock) {
    661             switch (profile) {
    662                 case BluetoothProfile.HEADSET:
    663                     mHeadset = (BluetoothHeadset) proxy;
    664                     if (mA2dp != null) {
    665                         mHandler.sendEmptyMessage(MSG_NEXT_STEP);
    666                     }
    667                     break;
    668                 case BluetoothProfile.A2DP:
    669                     mA2dp = (BluetoothA2dp) proxy;
    670                     if (mHeadset != null) {
    671                         mHandler.sendEmptyMessage(MSG_NEXT_STEP);
    672                     }
    673                     break;
    674                 case BluetoothProfile.HID_HOST:
    675                     mInput = (BluetoothHidHost) proxy;
    676                     if (mInput != null) {
    677                         mHandler.sendEmptyMessage(MSG_NEXT_STEP);
    678                     }
    679                     break;
    680             }
    681         }
    682     }
    683 
    684     @Override
    685     public void onServiceDisconnected(int profile) {
    686         // We can ignore these
    687     }
    688 
    689     void sendRetryMessage(int waitTime) {
    690         if (!mHandler.hasMessages(MSG_RETRY)) {
    691             mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_RETRY), waitTime);
    692         }
    693     }
    694 }
    695