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