• 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(mContext.getString(R.string.disconnecting_peripheral) + " " +
226                                     mName + "...");
227                             break;
228                         } else {
229                             mHidResult = RESULT_DISCONNECTED;
230                         }
231                     } else {
232                         if (mHeadset.getConnectionState(mDevice)
233                                 != BluetoothProfile.STATE_DISCONNECTED) {
234                             mHfpResult = RESULT_PENDING;
235                             mHeadset.disconnect(mDevice);
236                         } else {
237                             mHfpResult = RESULT_DISCONNECTED;
238                         }
239                         if (mA2dp.getConnectionState(mDevice)
240                                 != BluetoothProfile.STATE_DISCONNECTED) {
241                             mA2dpResult = RESULT_PENDING;
242                             mA2dp.disconnect(mDevice);
243                         } else {
244                             mA2dpResult = RESULT_DISCONNECTED;
245                         }
246                         if (mA2dpResult == RESULT_PENDING || mHfpResult == RESULT_PENDING) {
247                             toast(mContext.getString(R.string.disconnecting_peripheral) + " " +
248                                     mName + "...");
249                             break;
250                         }
251                     }
252                 }
253                 // fall-through
254             case STATE_DISCONNECTING:
255                 if (mTransport == BluetoothDevice.TRANSPORT_LE) {
256                     if (mHidResult == RESULT_DISCONNECTED) {
257                         toast(mContext.getString(R.string.disconnected_peripheral) + " " + mName);
258                         complete(false);
259                     }
260 
261                     break;
262                 } else {
263                     if (mA2dpResult == RESULT_PENDING || mHfpResult == RESULT_PENDING) {
264                         // still disconnecting
265                         break;
266                     }
267                     if (mA2dpResult == RESULT_DISCONNECTED && mHfpResult == RESULT_DISCONNECTED) {
268                         toast(mContext.getString(R.string.disconnected_peripheral) + " " + mName);
269                     }
270                     complete(false);
271                     break;
272                 }
273 
274         }
275 
276     }
277 
getProfileProxys()278     boolean getProfileProxys() {
279 
280         if (mTransport == BluetoothDevice.TRANSPORT_LE) {
281             if (!mBluetoothAdapter.getProfileProxy(mContext, this, BluetoothProfile.INPUT_DEVICE))
282                 return false;
283         } else {
284             if(!mBluetoothAdapter.getProfileProxy(mContext, this, BluetoothProfile.HEADSET))
285                 return false;
286 
287             if(!mBluetoothAdapter.getProfileProxy(mContext, this, BluetoothProfile.A2DP))
288                 return false;
289         }
290 
291         return true;
292     }
293 
nextStepConnect()294     void nextStepConnect() {
295         switch (mState) {
296             case STATE_INIT_COMPLETE:
297                 if (mDevice.getBondState() != BluetoothDevice.BOND_BONDED) {
298                     requestPairConfirmation();
299                     mState = STATE_WAITING_FOR_BOND_CONFIRMATION;
300                     break;
301                 }
302 
303                 if (mTransport == BluetoothDevice.TRANSPORT_LE) {
304                     if (mDevice.getBondState() != BluetoothDevice.BOND_NONE) {
305                         mDevice.removeBond();
306                         requestPairConfirmation();
307                         mState = STATE_WAITING_FOR_BOND_CONFIRMATION;
308                         break;
309                     }
310                 }
311                 // fall-through
312             case STATE_WAITING_FOR_BOND_CONFIRMATION:
313                 if (mDevice.getBondState() != BluetoothDevice.BOND_BONDED) {
314                     startBonding();
315                     break;
316                 }
317                 // fall-through
318             case STATE_BONDING:
319                 // Bluetooth Profile service will correctly serialize
320                 // HFP then A2DP connect
321                 mState = STATE_CONNECTING;
322                 synchronized (mLock) {
323                     if (mTransport == BluetoothDevice.TRANSPORT_LE) {
324                         if (mInput.getConnectionState(mDevice)
325                                 != BluetoothProfile.STATE_CONNECTED) {
326                             mHidResult = RESULT_PENDING;
327                             mInput.connect(mDevice);
328                             toast(mContext.getString(R.string.connecting_peripheral) + " "
329                                     + mName + "...");
330                             break;
331                         } else {
332                             mHidResult = RESULT_CONNECTED;
333                         }
334                     } else {
335                         if (mHeadset.getConnectionState(mDevice) !=
336                                 BluetoothProfile.STATE_CONNECTED) {
337                             mHfpResult = RESULT_PENDING;
338                             mHeadset.connect(mDevice);
339                         } else {
340                             mHfpResult = RESULT_CONNECTED;
341                         }
342                         if (mA2dp.getConnectionState(mDevice) != BluetoothProfile.STATE_CONNECTED) {
343                             mA2dpResult = RESULT_PENDING;
344                             mA2dp.connect(mDevice);
345                         } else {
346                             mA2dpResult = RESULT_CONNECTED;
347                         }
348                         if (mA2dpResult == RESULT_PENDING || mHfpResult == RESULT_PENDING) {
349                             toast(mContext.getString(R.string.connecting_peripheral) + " "
350                                     + mName + "...");
351                             break;
352                         }
353                     }
354                 }
355                 // fall-through
356             case STATE_CONNECTING:
357                 if (mTransport == BluetoothDevice.TRANSPORT_LE) {
358                     if (mHidResult == RESULT_PENDING) {
359                         break;
360                     } else if (mHidResult == RESULT_CONNECTED) {
361                         toast(mContext.getString(R.string.connected_peripheral) + " " + mName);
362                         complete(true);
363                     } else {
364                         toast (mContext.getString(R.string.connect_peripheral_failed) + " "
365                                 + mName);
366                         complete(false);
367                     }
368                 } else {
369                     if (mA2dpResult == RESULT_PENDING || mHfpResult == RESULT_PENDING) {
370                         // another connection type still pending
371                         break;
372                     }
373                     if (mA2dpResult == RESULT_CONNECTED || mHfpResult == RESULT_CONNECTED) {
374                         // we'll take either as success
375                         toast(mContext.getString(R.string.connected_peripheral) + " " + mName);
376                         if (mA2dpResult == RESULT_CONNECTED) startTheMusic();
377                         complete(true);
378                     } else {
379                         toast (mContext.getString(R.string.connect_peripheral_failed) + " " + mName);
380                         complete(false);
381                     }
382                 }
383                 break;
384         }
385     }
386 
startBonding()387     void startBonding() {
388         mState = STATE_BONDING;
389         toast(mContext.getString(R.string.pairing_peripheral) + " " + mName + "...");
390         if (!mDevice.createBond(mTransport)) {
391             toast(mContext.getString(R.string.pairing_peripheral_failed) + " " + mName);
392             complete(false);
393         }
394     }
395 
handleIntent(Intent intent)396     void handleIntent(Intent intent) {
397         String action = intent.getAction();
398         // Everything requires the device to match...
399         BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
400         if (!mDevice.equals(device)) return;
401 
402         if (ACTION_ALLOW_CONNECT.equals(action)) {
403             nextStepConnect();
404         } else if (ACTION_DENY_CONNECT.equals(action)) {
405             complete(false);
406         } else if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(action)
407                 && mState == STATE_BONDING) {
408             int bond = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE,
409                     BluetoothAdapter.ERROR);
410             if (bond == BluetoothDevice.BOND_BONDED) {
411                 nextStepConnect();
412             } else if (bond == BluetoothDevice.BOND_NONE) {
413                 toast(mContext.getString(R.string.pairing_peripheral_failed) + " " + mName);
414                 complete(false);
415             }
416         } else if (BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED.equals(action) &&
417                 (mState == STATE_CONNECTING || mState == STATE_DISCONNECTING)) {
418             int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothAdapter.ERROR);
419             if (state == BluetoothProfile.STATE_CONNECTED) {
420                 mHfpResult = RESULT_CONNECTED;
421                 nextStep();
422             } else if (state == BluetoothProfile.STATE_DISCONNECTED) {
423                 mHfpResult = RESULT_DISCONNECTED;
424                 nextStep();
425             }
426         } else if (BluetoothA2dp.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                 mA2dpResult = RESULT_CONNECTED;
431                 nextStep();
432             } else if (state == BluetoothProfile.STATE_DISCONNECTED) {
433                 mA2dpResult = RESULT_DISCONNECTED;
434                 nextStep();
435             }
436         } else if (BluetoothInputDevice.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                 mHidResult = RESULT_CONNECTED;
441                 nextStep();
442             } else if (state == BluetoothProfile.STATE_DISCONNECTED) {
443                 mHidResult = RESULT_DISCONNECTED;
444                 nextStep();
445             }
446         }
447     }
448 
complete(boolean connected)449     void complete(boolean connected) {
450         if (DBG) Log.d(TAG, "complete()");
451         mState = STATE_COMPLETE;
452         mContext.unregisterReceiver(mReceiver);
453         mHandler.removeMessages(MSG_TIMEOUT);
454         synchronized (mLock) {
455             if (mA2dp != null) {
456                 mBluetoothAdapter.closeProfileProxy(BluetoothProfile.A2DP, mA2dp);
457             }
458             if (mHeadset != null) {
459                 mBluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, mHeadset);
460             }
461 
462             if (mInput != null) {
463                 mBluetoothAdapter.closeProfileProxy(BluetoothProfile.INPUT_DEVICE, mInput);
464             }
465 
466             mA2dp = null;
467             mHeadset = null;
468             mInput = null;
469         }
470         mCallback.onBluetoothPeripheralHandoverComplete(connected);
471     }
472 
toast(CharSequence text)473     void toast(CharSequence text) {
474         Toast.makeText(mContext,  text, Toast.LENGTH_SHORT).show();
475     }
476 
startTheMusic()477     void startTheMusic() {
478         MediaSessionLegacyHelper helper = MediaSessionLegacyHelper.getHelper(mContext);
479         if (helper != null) {
480             KeyEvent keyEvent = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY);
481             helper.sendMediaButtonEvent(keyEvent, false);
482             keyEvent = new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PLAY);
483             helper.sendMediaButtonEvent(keyEvent, false);
484         } else {
485             Log.w(TAG, "Unable to send media key event");
486         }
487     }
488 
requestPairConfirmation()489     void requestPairConfirmation() {
490         Intent dialogIntent = new Intent(mContext, ConfirmConnectActivity.class);
491         dialogIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
492         dialogIntent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice);
493 
494         mContext.startActivity(dialogIntent);
495     }
496 
497     final Handler mHandler = new Handler() {
498         @Override
499         public void handleMessage(Message msg) {
500             switch (msg.what) {
501                 case MSG_TIMEOUT:
502                     if (mState == STATE_COMPLETE) return;
503                     Log.i(TAG, "Timeout completing BT handover");
504                     complete(false);
505                     break;
506                 case MSG_NEXT_STEP:
507                     nextStep();
508                     break;
509             }
510         }
511     };
512 
513     final BroadcastReceiver mReceiver = new BroadcastReceiver() {
514         @Override
515         public void onReceive(Context context, Intent intent) {
516             handleIntent(intent);
517         }
518     };
519 
checkMainThread()520     static void checkMainThread() {
521         if (Looper.myLooper() != Looper.getMainLooper()) {
522             throw new IllegalThreadStateException("must be called on main thread");
523         }
524     }
525 
526     @Override
onServiceConnected(int profile, BluetoothProfile proxy)527     public void onServiceConnected(int profile, BluetoothProfile proxy) {
528         synchronized (mLock) {
529             switch (profile) {
530                 case BluetoothProfile.HEADSET:
531                     mHeadset = (BluetoothHeadset) proxy;
532                     if (mA2dp != null) {
533                         mHandler.sendEmptyMessage(MSG_NEXT_STEP);
534                     }
535                     break;
536                 case BluetoothProfile.A2DP:
537                     mA2dp = (BluetoothA2dp) proxy;
538                     if (mHeadset != null) {
539                         mHandler.sendEmptyMessage(MSG_NEXT_STEP);
540                     }
541                     break;
542                 case BluetoothProfile.INPUT_DEVICE:
543                     mInput = (BluetoothInputDevice) proxy;
544                     if (mInput != null) {
545                         mHandler.sendEmptyMessage(MSG_NEXT_STEP);
546                     }
547                     break;
548             }
549         }
550     }
551 
552     @Override
onServiceDisconnected(int profile)553     public void onServiceDisconnected(int profile) {
554         // We can ignore these
555     }
556 }
557