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, mOobData)) {
457                 toast(getToastString(R.string.pairing_peripheral_failed));
458                 complete(false);
459             }
460         } else if (!mDevice.createBond(mTransport)) {
461                 toast(getToastString(R.string.pairing_peripheral_failed));
462                 complete(false);
463         }
464     }
465 
handleIntent(Intent intent)466     void handleIntent(Intent intent) {
467         String action = intent.getAction();
468         // Everything requires the device to match...
469         BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
470         if (!mDevice.equals(device)) return;
471 
472         if (ACTION_ALLOW_CONNECT.equals(action)) {
473             mHandler.removeMessages(MSG_TIMEOUT);
474             mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_TIMEOUT), TIMEOUT_MS);
475             nextStepConnect();
476         } else if (ACTION_DENY_CONNECT.equals(action)) {
477             complete(false);
478         } else if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(action)
479                 && mState == STATE_BONDING) {
480             int bond = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE,
481                     BluetoothAdapter.ERROR);
482             if (bond == BluetoothDevice.BOND_BONDED) {
483                 mRetryCount = 0;
484                 nextStepConnect();
485             } else if (bond == BluetoothDevice.BOND_NONE) {
486                 if (mRetryCount < MAX_RETRY_COUNT) {
487                     sendRetryMessage(RETRY_PAIRING_WAIT_TIME_MS);
488                 } else {
489                     toast(getToastString(R.string.pairing_peripheral_failed));
490                     complete(false);
491                 }
492             }
493         } else if (BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED.equals(action) &&
494                 (mState == STATE_CONNECTING || mState == STATE_DISCONNECTING)) {
495             int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothAdapter.ERROR);
496             if (state == BluetoothProfile.STATE_CONNECTED) {
497                 mHfpResult = RESULT_CONNECTED;
498                 nextStep();
499             } else if (state == BluetoothProfile.STATE_DISCONNECTED) {
500                 if (mAction == ACTION_CONNECT && mRetryCount < MAX_RETRY_COUNT) {
501                     sendRetryMessage(RETRY_CONNECT_WAIT_TIME_MS);
502                 } else {
503                     mHfpResult = RESULT_DISCONNECTED;
504                     nextStep();
505                 }
506             }
507         } else if (BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED.equals(action) &&
508                 (mState == STATE_CONNECTING || mState == STATE_DISCONNECTING)) {
509             int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothAdapter.ERROR);
510             if (state == BluetoothProfile.STATE_CONNECTED) {
511                 mA2dpResult = RESULT_CONNECTED;
512                 nextStep();
513             } else if (state == BluetoothProfile.STATE_DISCONNECTED) {
514                 if (mAction == ACTION_CONNECT && mRetryCount < MAX_RETRY_COUNT) {
515                     sendRetryMessage(RETRY_CONNECT_WAIT_TIME_MS);
516                 } else {
517                     mA2dpResult = RESULT_DISCONNECTED;
518                     nextStep();
519                 }
520             }
521         } else if (BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED.equals(action) &&
522                 (mState == STATE_CONNECTING || mState == STATE_DISCONNECTING)) {
523             int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothAdapter.ERROR);
524             if (state == BluetoothProfile.STATE_CONNECTED) {
525                 mHidResult = RESULT_CONNECTED;
526                 nextStep();
527             } else if (state == BluetoothProfile.STATE_DISCONNECTED) {
528                 mHidResult = RESULT_DISCONNECTED;
529                 nextStep();
530             }
531         }
532     }
533 
complete(boolean connected)534     void complete(boolean connected) {
535         if (DBG) Log.d(TAG, "complete()");
536         mState = STATE_COMPLETE;
537         mContext.unregisterReceiver(mReceiver);
538         mHandler.removeMessages(MSG_TIMEOUT);
539         mHandler.removeMessages(MSG_RETRY);
540         synchronized (mLock) {
541             if (mA2dp != null) {
542                 mBluetoothAdapter.closeProfileProxy(BluetoothProfile.A2DP, mA2dp);
543             }
544             if (mHeadset != null) {
545                 mBluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, mHeadset);
546             }
547 
548             if (mInput != null) {
549                 mBluetoothAdapter.closeProfileProxy(BluetoothProfile.HID_HOST, mInput);
550             }
551 
552             mA2dp = null;
553             mHeadset = null;
554             mInput = null;
555         }
556         mCallback.onBluetoothPeripheralHandoverComplete(connected);
557     }
558 
toast(CharSequence text)559     void toast(CharSequence text) {
560         Toast.makeText(mContext,  text, Toast.LENGTH_SHORT).show();
561     }
562 
startTheMusic()563     void startTheMusic() {
564         if (!mContext.getResources().getBoolean(R.bool.enable_auto_play) && !mIsMusicActive) {
565             return;
566         }
567 
568         MediaSessionLegacyHelper helper = MediaSessionLegacyHelper.getHelper(mContext);
569         if (helper != null) {
570             KeyEvent keyEvent = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY);
571             helper.sendMediaButtonEvent(keyEvent, false);
572             keyEvent = new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PLAY);
573             helper.sendMediaButtonEvent(keyEvent, false);
574         } else {
575             Log.w(TAG, "Unable to send media key event");
576         }
577     }
578 
requestPairConfirmation()579     void requestPairConfirmation() {
580         Intent dialogIntent = new Intent(mContext, ConfirmConnectActivity.class);
581         dialogIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
582         dialogIntent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice);
583         dialogIntent.putExtra(BluetoothDevice.EXTRA_NAME, mName);
584 
585         mContext.startActivity(dialogIntent);
586     }
587 
hasA2dpCapability(ParcelUuid[] uuids, BluetoothClass btClass)588     boolean hasA2dpCapability(ParcelUuid[] uuids, BluetoothClass btClass) {
589         if (uuids != null) {
590             for (ParcelUuid uuid : uuids) {
591                 if (uuid.equals(BluetoothUuid.A2DP_SINK) || uuid.equals(BluetoothUuid.ADV_AUDIO_DIST)) {
592                     return true;
593                 }
594             }
595         }
596         if (btClass != null && btClass.doesClassMatch(BluetoothClass.PROFILE_A2DP)) {
597             return true;
598         }
599         return false;
600     }
601 
hasHeadsetCapability(ParcelUuid[] uuids, BluetoothClass btClass)602     boolean hasHeadsetCapability(ParcelUuid[] uuids, BluetoothClass btClass) {
603         if (uuids != null) {
604             for (ParcelUuid uuid : uuids) {
605                 if (uuid.equals(BluetoothUuid.HFP) || uuid.equals(BluetoothUuid.HSP)) {
606                     return true;
607                 }
608             }
609         }
610         if (btClass != null && btClass.doesClassMatch(BluetoothClass.PROFILE_HEADSET)) {
611             return true;
612         }
613         return false;
614     }
615 
616     final Handler mHandler = new Handler() {
617         @Override
618         public void handleMessage(Message msg) {
619             switch (msg.what) {
620                 case MSG_TIMEOUT:
621                     if (mState == STATE_COMPLETE) return;
622                     Log.i(TAG, "Timeout completing BT handover");
623                     if (mState == STATE_WAITING_FOR_BOND_CONFIRMATION) {
624                         mContext.sendBroadcast(new Intent(ACTION_TIMEOUT_CONNECT));
625                     } else if (mState == STATE_BONDING) {
626                         toast(getToastString(R.string.pairing_peripheral_failed));
627                     } else if (mState == STATE_CONNECTING) {
628                         if (mHidResult == RESULT_PENDING) {
629                             mHidResult = RESULT_DISCONNECTED;
630                         }
631                         if (mA2dpResult == RESULT_PENDING) {
632                             mA2dpResult = RESULT_DISCONNECTED;
633                         }
634                         if (mHfpResult == RESULT_PENDING) {
635                             mHfpResult = RESULT_DISCONNECTED;
636                         }
637                         // Check if any one profile is connected, then it takes as success
638                         nextStepConnect();
639                         break;
640                     }
641                     complete(false);
642                     break;
643                 case MSG_NEXT_STEP:
644                     nextStep();
645                     break;
646                 case MSG_RETRY:
647                     mHandler.removeMessages(MSG_RETRY);
648                     if (mState == STATE_BONDING) {
649                         mState = STATE_WAITING_FOR_BOND_CONFIRMATION;
650                     } else if (mState == STATE_CONNECTING) {
651                         mState = STATE_BONDING;
652                     }
653                     mRetryCount++;
654                     nextStepConnect();
655                     break;
656             }
657         }
658     };
659 
660     final BroadcastReceiver mReceiver = new BroadcastReceiver() {
661         @Override
662         public void onReceive(Context context, Intent intent) {
663             handleIntent(intent);
664         }
665     };
666 
checkMainThread()667     static void checkMainThread() {
668         if (Looper.myLooper() != Looper.getMainLooper()) {
669             throw new IllegalThreadStateException("must be called on main thread");
670         }
671     }
672 
673     @Override
onServiceConnected(int profile, BluetoothProfile proxy)674     public void onServiceConnected(int profile, BluetoothProfile proxy) {
675         synchronized (mLock) {
676             switch (profile) {
677                 case BluetoothProfile.HEADSET:
678                     mHeadset = (BluetoothHeadset) proxy;
679                     if (mA2dp != null) {
680                         mHandler.sendEmptyMessage(MSG_NEXT_STEP);
681                     }
682                     break;
683                 case BluetoothProfile.A2DP:
684                     mA2dp = (BluetoothA2dp) proxy;
685                     if (mHeadset != null) {
686                         mHandler.sendEmptyMessage(MSG_NEXT_STEP);
687                     }
688                     break;
689                 case BluetoothProfile.HID_HOST:
690                     mInput = (BluetoothHidHost) proxy;
691                     if (mInput != null) {
692                         mHandler.sendEmptyMessage(MSG_NEXT_STEP);
693                     }
694                     break;
695             }
696         }
697     }
698 
699     @Override
onServiceDisconnected(int profile)700     public void onServiceDisconnected(int profile) {
701         // We can ignore these
702     }
703 
sendRetryMessage(int waitTime)704     void sendRetryMessage(int waitTime) {
705         if (!mHandler.hasMessages(MSG_RETRY)) {
706             mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_RETRY), waitTime);
707         }
708     }
709 }
710