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