• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.BluetoothInputDevice;
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     BluetoothInputDevice mInput;
115 
116     public interface Callback {
onBluetoothPeripheralHandoverComplete(boolean connected)117         public void onBluetoothPeripheralHandoverComplete(boolean connected);
118     }
119 
BluetoothPeripheralHandover(Context context, BluetoothDevice device, String name, int transport, OobData oobData, ParcelUuid[] uuids, BluetoothClass btClass, Callback callback)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 
hasStarted()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      */
start()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(BluetoothInputDevice.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      */
nextStep()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      */
nextStepInit()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 
nextStepDisconnect()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 
getToastString(int resid)314     private String getToastString(int resid) {
315         return mContext.getString(resid, mName != null ? mName : R.string.device);
316     }
317 
getProfileProxys()318     boolean getProfileProxys() {
319 
320         if (mTransport == BluetoothDevice.TRANSPORT_LE) {
321             if (!mBluetoothAdapter.getProfileProxy(mContext, this, BluetoothProfile.INPUT_DEVICE))
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 
nextStepConnect()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 (mHeadset.getConnectionState(mDevice) !=
366                                 BluetoothProfile.STATE_CONNECTED) {
367                             if (mIsHeadsetAvailable) {
368                                 mHfpResult = RESULT_PENDING;
369                                 mHeadset.connect(mDevice);
370                             } else {
371                                 mHfpResult = RESULT_DISCONNECTED;
372                             }
373                         } else {
374                             mHfpResult = RESULT_CONNECTED;
375                         }
376                         if (mA2dp.getConnectionState(mDevice) != BluetoothProfile.STATE_CONNECTED) {
377                             if (mIsA2dpAvailable) {
378                                 mA2dpResult = RESULT_PENDING;
379                                 mA2dp.connect(mDevice);
380                             } else {
381                                 mA2dpResult = RESULT_DISCONNECTED;
382                             }
383                         } else {
384                             mA2dpResult = RESULT_CONNECTED;
385                         }
386                         if (mA2dpResult == RESULT_PENDING || mHfpResult == RESULT_PENDING) {
387                             if (mRetryCount == 0) {
388                                 toast(getToastString(R.string.connecting_peripheral));
389                             }
390                             if (mRetryCount < MAX_RETRY_COUNT) {
391                                 sendRetryMessage(RETRY_CONNECT_WAIT_TIME_MS);
392                                 break;
393                             }
394                         }
395                     }
396                 }
397                 // fall-through
398             case STATE_CONNECTING:
399                 if (mTransport != BluetoothDevice.TRANSPORT_LE) {
400                     if (mA2dpResult == RESULT_PENDING || mHfpResult == RESULT_PENDING) {
401                         // another connection type still pending
402                         break;
403                     }
404                     if (mA2dpResult == RESULT_CONNECTED || mHfpResult == RESULT_CONNECTED) {
405                         // we'll take either as success
406                         toast(getToastString(R.string.connected_peripheral));
407                         if (mA2dpResult == RESULT_CONNECTED) startTheMusic();
408                         mDevice.setAlias(mName);
409                         complete(true);
410                     } else {
411                         toast (getToastString(R.string.connect_peripheral_failed));
412                         complete(false);
413                     }
414                 }
415                 break;
416         }
417     }
418 
startBonding()419     void startBonding() {
420         mState = STATE_BONDING;
421         if (mRetryCount == 0) {
422             toast(getToastString(R.string.pairing_peripheral));
423         }
424         if (mOobData != null) {
425             if (!mDevice.createBondOutOfBand(mTransport, mOobData)) {
426                 toast(getToastString(R.string.pairing_peripheral_failed));
427                 complete(false);
428             }
429         } else if (!mDevice.createBond(mTransport)) {
430                 toast(getToastString(R.string.pairing_peripheral_failed));
431                 complete(false);
432         }
433     }
434 
handleIntent(Intent intent)435     void handleIntent(Intent intent) {
436         String action = intent.getAction();
437         // Everything requires the device to match...
438         BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
439         if (!mDevice.equals(device)) return;
440 
441         if (ACTION_ALLOW_CONNECT.equals(action)) {
442             mHandler.removeMessages(MSG_TIMEOUT);
443             mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_TIMEOUT), TIMEOUT_MS);
444             nextStepConnect();
445         } else if (ACTION_DENY_CONNECT.equals(action)) {
446             complete(false);
447         } else if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(action)
448                 && mState == STATE_BONDING) {
449             int bond = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE,
450                     BluetoothAdapter.ERROR);
451             if (bond == BluetoothDevice.BOND_BONDED) {
452                 mRetryCount = 0;
453                 nextStepConnect();
454             } else if (bond == BluetoothDevice.BOND_NONE) {
455                 if (mRetryCount < MAX_RETRY_COUNT) {
456                     sendRetryMessage(RETRY_PAIRING_WAIT_TIME_MS);
457                 } else {
458                     toast(getToastString(R.string.pairing_peripheral_failed));
459                     complete(false);
460                 }
461             }
462         } else if (BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED.equals(action) &&
463                 (mState == STATE_CONNECTING || mState == STATE_DISCONNECTING)) {
464             int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothAdapter.ERROR);
465             if (state == BluetoothProfile.STATE_CONNECTED) {
466                 mHfpResult = RESULT_CONNECTED;
467                 nextStep();
468             } else if (state == BluetoothProfile.STATE_DISCONNECTED) {
469                 if (mAction == ACTION_CONNECT && mRetryCount < MAX_RETRY_COUNT) {
470                     sendRetryMessage(RETRY_CONNECT_WAIT_TIME_MS);
471                 } else {
472                     mHfpResult = RESULT_DISCONNECTED;
473                     nextStep();
474                 }
475             }
476         } else if (BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED.equals(action) &&
477                 (mState == STATE_CONNECTING || mState == STATE_DISCONNECTING)) {
478             int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothAdapter.ERROR);
479             if (state == BluetoothProfile.STATE_CONNECTED) {
480                 mA2dpResult = RESULT_CONNECTED;
481                 nextStep();
482             } else if (state == BluetoothProfile.STATE_DISCONNECTED) {
483                 if (mAction == ACTION_CONNECT && mRetryCount < MAX_RETRY_COUNT) {
484                     sendRetryMessage(RETRY_CONNECT_WAIT_TIME_MS);
485                 } else {
486                     mA2dpResult = RESULT_DISCONNECTED;
487                     nextStep();
488                 }
489             }
490         } else if (BluetoothInputDevice.ACTION_CONNECTION_STATE_CHANGED.equals(action) &&
491                 (mState == STATE_CONNECTING || mState == STATE_DISCONNECTING)) {
492             int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothAdapter.ERROR);
493             if (state == BluetoothProfile.STATE_CONNECTED) {
494                 mHidResult = RESULT_CONNECTED;
495                 nextStep();
496             } else if (state == BluetoothProfile.STATE_DISCONNECTED) {
497                 mHidResult = RESULT_DISCONNECTED;
498                 nextStep();
499             }
500         }
501     }
502 
complete(boolean connected)503     void complete(boolean connected) {
504         if (DBG) Log.d(TAG, "complete()");
505         mState = STATE_COMPLETE;
506         mContext.unregisterReceiver(mReceiver);
507         mHandler.removeMessages(MSG_TIMEOUT);
508         mHandler.removeMessages(MSG_RETRY);
509         synchronized (mLock) {
510             if (mA2dp != null) {
511                 mBluetoothAdapter.closeProfileProxy(BluetoothProfile.A2DP, mA2dp);
512             }
513             if (mHeadset != null) {
514                 mBluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, mHeadset);
515             }
516 
517             if (mInput != null) {
518                 mBluetoothAdapter.closeProfileProxy(BluetoothProfile.INPUT_DEVICE, mInput);
519             }
520 
521             mA2dp = null;
522             mHeadset = null;
523             mInput = null;
524         }
525         mCallback.onBluetoothPeripheralHandoverComplete(connected);
526     }
527 
toast(CharSequence text)528     void toast(CharSequence text) {
529         Toast.makeText(mContext,  text, Toast.LENGTH_SHORT).show();
530     }
531 
startTheMusic()532     void startTheMusic() {
533         MediaSessionLegacyHelper helper = MediaSessionLegacyHelper.getHelper(mContext);
534         if (helper != null) {
535             KeyEvent keyEvent = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY);
536             helper.sendMediaButtonEvent(keyEvent, false);
537             keyEvent = new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PLAY);
538             helper.sendMediaButtonEvent(keyEvent, false);
539         } else {
540             Log.w(TAG, "Unable to send media key event");
541         }
542     }
543 
requestPairConfirmation()544     void requestPairConfirmation() {
545         Intent dialogIntent = new Intent(mContext, ConfirmConnectActivity.class);
546         dialogIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
547         dialogIntent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice);
548         dialogIntent.putExtra(BluetoothDevice.EXTRA_NAME, mName);
549 
550         mContext.startActivity(dialogIntent);
551     }
552 
hasA2dpCapability(ParcelUuid[] uuids, BluetoothClass btClass)553     boolean hasA2dpCapability(ParcelUuid[] uuids, BluetoothClass btClass) {
554         if (uuids != null) {
555             for (ParcelUuid uuid : uuids) {
556                 if (BluetoothUuid.isAudioSink(uuid) || BluetoothUuid.isAdvAudioDist(uuid)) {
557                     return true;
558                 }
559             }
560         }
561         if (btClass != null && btClass.doesClassMatch(BluetoothClass.PROFILE_A2DP)) {
562             return true;
563         }
564         return false;
565     }
566 
hasHeadsetCapability(ParcelUuid[] uuids, BluetoothClass btClass)567     boolean hasHeadsetCapability(ParcelUuid[] uuids, BluetoothClass btClass) {
568         if (uuids != null) {
569             for (ParcelUuid uuid : uuids) {
570                 if (BluetoothUuid.isHandsfree(uuid) || BluetoothUuid.isHeadset(uuid)) {
571                     return true;
572                 }
573             }
574         }
575         if (btClass != null && btClass.doesClassMatch(BluetoothClass.PROFILE_HEADSET)) {
576             return true;
577         }
578         return false;
579     }
580 
581     final Handler mHandler = new Handler() {
582         @Override
583         public void handleMessage(Message msg) {
584             switch (msg.what) {
585                 case MSG_TIMEOUT:
586                     if (mState == STATE_COMPLETE) return;
587                     Log.i(TAG, "Timeout completing BT handover");
588                     mContext.sendBroadcast(new Intent(ACTION_TIMEOUT_CONNECT));
589                     complete(false);
590                     break;
591                 case MSG_NEXT_STEP:
592                     nextStep();
593                     break;
594                 case MSG_RETRY:
595                     mHandler.removeMessages(MSG_RETRY);
596                     if (mState == STATE_BONDING) {
597                         mState = STATE_WAITING_FOR_BOND_CONFIRMATION;
598                     } else if (mState == STATE_CONNECTING) {
599                         mState = STATE_BONDING;
600                     }
601                     mRetryCount++;
602                     nextStepConnect();
603                     break;
604             }
605         }
606     };
607 
608     final BroadcastReceiver mReceiver = new BroadcastReceiver() {
609         @Override
610         public void onReceive(Context context, Intent intent) {
611             handleIntent(intent);
612         }
613     };
614 
checkMainThread()615     static void checkMainThread() {
616         if (Looper.myLooper() != Looper.getMainLooper()) {
617             throw new IllegalThreadStateException("must be called on main thread");
618         }
619     }
620 
621     @Override
onServiceConnected(int profile, BluetoothProfile proxy)622     public void onServiceConnected(int profile, BluetoothProfile proxy) {
623         synchronized (mLock) {
624             switch (profile) {
625                 case BluetoothProfile.HEADSET:
626                     mHeadset = (BluetoothHeadset) proxy;
627                     if (mA2dp != null) {
628                         mHandler.sendEmptyMessage(MSG_NEXT_STEP);
629                     }
630                     break;
631                 case BluetoothProfile.A2DP:
632                     mA2dp = (BluetoothA2dp) proxy;
633                     if (mHeadset != null) {
634                         mHandler.sendEmptyMessage(MSG_NEXT_STEP);
635                     }
636                     break;
637                 case BluetoothProfile.INPUT_DEVICE:
638                     mInput = (BluetoothInputDevice) proxy;
639                     if (mInput != null) {
640                         mHandler.sendEmptyMessage(MSG_NEXT_STEP);
641                     }
642                     break;
643             }
644         }
645     }
646 
647     @Override
onServiceDisconnected(int profile)648     public void onServiceDisconnected(int profile) {
649         // We can ignore these
650     }
651 
sendRetryMessage(int waitTime)652     void sendRetryMessage(int waitTime) {
653         if (!mHandler.hasMessages(MSG_RETRY)) {
654             mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_RETRY), waitTime);
655         }
656     }
657 }
658