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.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.sysprop.NfcProperties;
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 = NfcProperties.debug_enabled().orElse(true);
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 = mContext.getSystemService(AudioManager.class);
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                             mDevice.disconnect();
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                         } else {
284                             mHfpResult = RESULT_DISCONNECTED;
285                         }
286                         if (mA2dp.getConnectionState(mDevice)
287                                 != BluetoothProfile.STATE_DISCONNECTED) {
288                             mA2dpResult = RESULT_PENDING;
289                         } else {
290                             mA2dpResult = RESULT_DISCONNECTED;
291                         }
292                         if (mA2dpResult == RESULT_PENDING || mHfpResult == RESULT_PENDING) {
293                             mDevice.disconnect();
294                             toast(getToastString(R.string.disconnecting_peripheral));
295                             break;
296                         }
297                     }
298                 }
299                 // fall-through
300             case STATE_DISCONNECTING:
301                 if (mTransport == BluetoothDevice.TRANSPORT_LE) {
302                     if (mHidResult == RESULT_DISCONNECTED) {
303                         toast(getToastString(R.string.disconnected_peripheral));
304                         complete(false);
305                     }
306 
307                     break;
308                 } else {
309                     if (mA2dpResult == RESULT_PENDING || mHfpResult == RESULT_PENDING) {
310                         // still disconnecting
311                         break;
312                     }
313                     if (mA2dpResult == RESULT_DISCONNECTED && mHfpResult == RESULT_DISCONNECTED) {
314                         toast(getToastString(R.string.disconnected_peripheral));
315                     }
316                     complete(false);
317                     break;
318                 }
319 
320         }
321 
322     }
323 
getToastString(int resid)324     private String getToastString(int resid) {
325         return mContext.getString(resid, mName != null ? mName : R.string.device);
326     }
327 
getProfileProxys()328     boolean getProfileProxys() {
329 
330         if (mTransport == BluetoothDevice.TRANSPORT_LE) {
331             if (!mBluetoothAdapter.getProfileProxy(mContext, this, BluetoothProfile.HID_HOST))
332                 return false;
333         } else {
334             if(!mBluetoothAdapter.getProfileProxy(mContext, this, BluetoothProfile.HEADSET))
335                 return false;
336 
337             if(!mBluetoothAdapter.getProfileProxy(mContext, this, BluetoothProfile.A2DP))
338                 return false;
339         }
340 
341         return true;
342     }
343 
nextStepConnect()344     void nextStepConnect() {
345         switch (mState) {
346             case STATE_INIT_COMPLETE:
347 
348                 if (mDevice.getBondState() != BluetoothDevice.BOND_BONDED) {
349                     requestPairConfirmation();
350                     mState = STATE_WAITING_FOR_BOND_CONFIRMATION;
351                     break;
352                 }
353 
354                 if (mTransport == BluetoothDevice.TRANSPORT_LE) {
355                     if (mDevice.getBondState() != BluetoothDevice.BOND_NONE) {
356                         mDevice.removeBond();
357                         requestPairConfirmation();
358                         mState = STATE_WAITING_FOR_BOND_CONFIRMATION;
359                         break;
360                     }
361                 }
362                 // fall-through
363             case STATE_WAITING_FOR_BOND_CONFIRMATION:
364                 if (mDevice.getBondState() != BluetoothDevice.BOND_BONDED) {
365                     startBonding();
366                     break;
367                 }
368                 // fall-through
369             case STATE_BONDING:
370                 // Bluetooth Profile service will correctly serialize
371                 // HFP then A2DP connect
372                 mState = STATE_CONNECTING;
373                 synchronized (mLock) {
374                     if (mTransport == BluetoothDevice.TRANSPORT_LE) {
375                         if (mInput.getConnectionState(mDevice)
376                                 != BluetoothProfile.STATE_CONNECTED) {
377                             mHidResult = RESULT_PENDING;
378                             toast(getToastString(R.string.connecting_peripheral));
379                             break;
380                         } else {
381                             mHidResult = RESULT_CONNECTED;
382                         }
383                     } else {
384                         if (mHeadset.getConnectionState(mDevice) !=
385                                 BluetoothProfile.STATE_CONNECTED) {
386                             if (mIsHeadsetAvailable) {
387                                 mHfpResult = RESULT_PENDING;
388                                 mHeadset.setConnectionPolicy(mDevice,
389                                     BluetoothProfile.CONNECTION_POLICY_ALLOWED);
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.setConnectionPolicy(mDevice,
400                                     BluetoothProfile.CONNECTION_POLICY_ALLOWED);
401                             } else {
402                                 mA2dpResult = RESULT_DISCONNECTED;
403                             }
404                         } else {
405                             mA2dpResult = RESULT_CONNECTED;
406                         }
407                         if (mA2dpResult == RESULT_PENDING || mHfpResult == RESULT_PENDING) {
408                             if (mRetryCount == 0) {
409                                 toast(getToastString(R.string.connecting_peripheral));
410                             }
411                             if (mRetryCount < MAX_RETRY_COUNT) {
412                                 sendRetryMessage(RETRY_CONNECT_WAIT_TIME_MS);
413                                 break;
414                             }
415                         }
416                     }
417                 }
418                 // fall-through
419             case STATE_CONNECTING:
420                 if (mTransport == BluetoothDevice.TRANSPORT_LE) {
421                     if (mHidResult == RESULT_PENDING) {
422                         break;
423                     } else if (mHidResult == RESULT_CONNECTED) {
424                         toast(getToastString(R.string.connected_peripheral));
425                         mDevice.setAlias(mName);
426                         complete(true);
427                     } else {
428                         toast (getToastString(R.string.connect_peripheral_failed));
429                         complete(false);
430                     }
431                 } else {
432                     if (mA2dpResult == RESULT_PENDING || mHfpResult == RESULT_PENDING) {
433                         // another connection type still pending
434                         break;
435                     }
436                     if (mA2dpResult == RESULT_CONNECTED || mHfpResult == RESULT_CONNECTED) {
437                         // we'll take either as success
438                         toast(getToastString(R.string.connected_peripheral));
439                         if (mA2dpResult == RESULT_CONNECTED) startTheMusic();
440                         mDevice.setAlias(mName);
441                         complete(true);
442                     } else {
443                         toast (getToastString(R.string.connect_peripheral_failed));
444                         complete(false);
445                     }
446                 }
447                 break;
448         }
449     }
450 
startBonding()451     void startBonding() {
452         mState = STATE_BONDING;
453         if (mRetryCount == 0) {
454             toast(getToastString(R.string.pairing_peripheral));
455         }
456         if (mOobData != null) {
457             if (!mDevice.createBondOutOfBand(mTransport, /* p192 not implemented for LE */ null,
458                 mOobData)) {
459                 toast(getToastString(R.string.pairing_peripheral_failed));
460                 complete(false);
461             }
462         } else if (!mDevice.createBond(mTransport)) {
463                 toast(getToastString(R.string.pairing_peripheral_failed));
464                 complete(false);
465         }
466     }
467 
handleIntent(Intent intent)468     void handleIntent(Intent intent) {
469         String action = intent.getAction();
470         // Everything requires the device to match...
471         BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
472         if (!mDevice.equals(device)) return;
473 
474         if (ACTION_ALLOW_CONNECT.equals(action)) {
475             mHandler.removeMessages(MSG_TIMEOUT);
476             mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_TIMEOUT), TIMEOUT_MS);
477             nextStepConnect();
478         } else if (ACTION_DENY_CONNECT.equals(action)) {
479             complete(false);
480         } else if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(action)
481                 && mState == STATE_BONDING) {
482             int bond = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE,
483                     BluetoothAdapter.ERROR);
484             if (bond == BluetoothDevice.BOND_BONDED) {
485                 mRetryCount = 0;
486                 nextStepConnect();
487             } else if (bond == BluetoothDevice.BOND_NONE) {
488                 int reason = intent.getIntExtra(BluetoothDevice.EXTRA_UNBOND_REASON,
489                         BluetoothAdapter.ERROR);
490                 if (mRetryCount < MAX_RETRY_COUNT
491                         && reason != BluetoothDevice.UNBOND_REASON_AUTH_FAILED) {
492                     sendRetryMessage(RETRY_PAIRING_WAIT_TIME_MS);
493                 } else {
494                     toast(getToastString(R.string.pairing_peripheral_failed));
495                     complete(false);
496                 }
497             }
498         } else if (BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED.equals(action) &&
499                 (mState == STATE_CONNECTING || mState == STATE_DISCONNECTING)) {
500             int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothAdapter.ERROR);
501             if (state == BluetoothProfile.STATE_CONNECTED) {
502                 mHfpResult = RESULT_CONNECTED;
503                 nextStep();
504             } else if (state == BluetoothProfile.STATE_DISCONNECTED) {
505                 if (mAction == ACTION_CONNECT && mRetryCount < MAX_RETRY_COUNT) {
506                     sendRetryMessage(RETRY_CONNECT_WAIT_TIME_MS);
507                 } else {
508                     mHfpResult = RESULT_DISCONNECTED;
509                     nextStep();
510                 }
511             }
512         } else if (BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED.equals(action) &&
513                 (mState == STATE_CONNECTING || mState == STATE_DISCONNECTING)) {
514             int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothAdapter.ERROR);
515             if (state == BluetoothProfile.STATE_CONNECTED) {
516                 mA2dpResult = RESULT_CONNECTED;
517                 nextStep();
518             } else if (state == BluetoothProfile.STATE_DISCONNECTED) {
519                 if (mAction == ACTION_CONNECT && mRetryCount < MAX_RETRY_COUNT) {
520                     sendRetryMessage(RETRY_CONNECT_WAIT_TIME_MS);
521                 } else {
522                     mA2dpResult = RESULT_DISCONNECTED;
523                     nextStep();
524                 }
525             }
526         } else if (BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED.equals(action) &&
527                 (mState == STATE_CONNECTING || mState == STATE_DISCONNECTING)) {
528             int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothAdapter.ERROR);
529             if (state == BluetoothProfile.STATE_CONNECTED) {
530                 mHidResult = RESULT_CONNECTED;
531                 nextStep();
532             } else if (state == BluetoothProfile.STATE_DISCONNECTED) {
533                 mHidResult = RESULT_DISCONNECTED;
534                 nextStep();
535             }
536         }
537     }
538 
complete(boolean connected)539     void complete(boolean connected) {
540         if (DBG) Log.d(TAG, "complete()");
541         mState = STATE_COMPLETE;
542         mContext.unregisterReceiver(mReceiver);
543         mHandler.removeMessages(MSG_TIMEOUT);
544         mHandler.removeMessages(MSG_RETRY);
545         synchronized (mLock) {
546             if (mA2dp != null) {
547                 mBluetoothAdapter.closeProfileProxy(BluetoothProfile.A2DP, mA2dp);
548             }
549             if (mHeadset != null) {
550                 mBluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, mHeadset);
551             }
552 
553             if (mInput != null) {
554                 mBluetoothAdapter.closeProfileProxy(BluetoothProfile.HID_HOST, mInput);
555             }
556 
557             mA2dp = null;
558             mHeadset = null;
559             mInput = null;
560         }
561         mCallback.onBluetoothPeripheralHandoverComplete(connected);
562     }
563 
toast(CharSequence text)564     void toast(CharSequence text) {
565         Toast.makeText(mContext,  text, Toast.LENGTH_SHORT).show();
566     }
567 
startTheMusic()568     void startTheMusic() {
569         if (!mContext.getResources().getBoolean(R.bool.enable_auto_play) && !mIsMusicActive) {
570             return;
571         }
572 
573         KeyEvent keyEvent = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY);
574         mAudioManager.dispatchMediaKeyEvent(keyEvent);
575         keyEvent = new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PLAY);
576         mAudioManager.dispatchMediaKeyEvent(keyEvent);
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