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