• 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.BluetoothProfile;
24 import android.content.BroadcastReceiver;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.content.IntentFilter;
28 import android.os.Handler;
29 import android.os.Looper;
30 import android.os.Message;
31 import android.util.Log;
32 import android.view.KeyEvent;
33 import android.widget.Toast;
34 
35 import com.android.nfc.R;
36 
37 /**
38  * Connects / Disconnects from a Bluetooth headset (or any device that
39  * might implement BT HSP, HFP or A2DP sink) when touched with NFC.
40  *
41  * This object is created on an NFC interaction, and determines what
42  * sequence of Bluetooth actions to take, and executes them. It is not
43  * designed to be re-used after the sequence has completed or timed out.
44  * Subsequent NFC interactions should use new objects.
45  *
46  */
47 public class BluetoothHeadsetHandover implements BluetoothProfile.ServiceListener {
48     static final String TAG = HandoverManager.TAG;
49     static final boolean DBG = HandoverManager.DBG;
50 
51     static final String ACTION_ALLOW_CONNECT = "com.android.nfc.handover.action.ALLOW_CONNECT";
52     static final String ACTION_DENY_CONNECT = "com.android.nfc.handover.action.DENY_CONNECT";
53 
54     static final int TIMEOUT_MS = 20000;
55 
56     static final int STATE_INIT = 0;
57     static final int STATE_WAITING_FOR_PROXIES = 1;
58     static final int STATE_INIT_COMPLETE = 2;
59     static final int STATE_WAITING_FOR_BOND_CONFIRMATION = 3;
60     static final int STATE_BONDING = 4;
61     static final int STATE_CONNECTING = 5;
62     static final int STATE_DISCONNECTING = 6;
63     static final int STATE_COMPLETE = 7;
64 
65     static final int RESULT_PENDING = 0;
66     static final int RESULT_CONNECTED = 1;
67     static final int RESULT_DISCONNECTED = 2;
68 
69     static final int ACTION_INIT = 0;
70     static final int ACTION_DISCONNECT = 1;
71     static final int ACTION_CONNECT = 2;
72 
73     static final int MSG_TIMEOUT = 1;
74     static final int MSG_NEXT_STEP = 2;
75 
76     final Context mContext;
77     final BluetoothDevice mDevice;
78     final String mName;
79     final Callback mCallback;
80     final BluetoothAdapter mBluetoothAdapter;
81 
82     final Object mLock = new Object();
83 
84     // only used on main thread
85     int mAction;
86     int mState;
87     int mHfpResult;  // used only in STATE_CONNECTING and STATE_DISCONNETING
88     int mA2dpResult; // used only in STATE_CONNECTING and STATE_DISCONNETING
89 
90     // protected by mLock
91     BluetoothA2dp mA2dp;
92     BluetoothHeadset mHeadset;
93 
94     public interface Callback {
onBluetoothHeadsetHandoverComplete(boolean connected)95         public void onBluetoothHeadsetHandoverComplete(boolean connected);
96     }
97 
BluetoothHeadsetHandover(Context context, BluetoothDevice device, String name, Callback callback)98     public BluetoothHeadsetHandover(Context context, BluetoothDevice device, String name,
99             Callback callback) {
100         checkMainThread();  // mHandler must get get constructed on Main Thread for toasts to work
101         mContext = context;
102         mDevice = device;
103         mName = name;
104         mCallback = callback;
105         mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
106 
107         mState = STATE_INIT;
108     }
109 
hasStarted()110     public boolean hasStarted() {
111         return mState != STATE_INIT;
112     }
113 
114     /**
115      * Main entry point. This method is usually called after construction,
116      * to begin the BT sequence. Must be called on Main thread.
117      */
start()118     public void start() {
119         checkMainThread();
120         if (mState != STATE_INIT) return;
121         if (mBluetoothAdapter == null) return;
122 
123         IntentFilter filter = new IntentFilter();
124         filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
125         filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
126         filter.addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED);
127         filter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
128         filter.addAction(ACTION_ALLOW_CONNECT);
129         filter.addAction(ACTION_DENY_CONNECT);
130 
131         mContext.registerReceiver(mReceiver, filter);
132 
133         mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_TIMEOUT), TIMEOUT_MS);
134         mAction = ACTION_INIT;
135         nextStep();
136     }
137 
138     /**
139      * Called to execute next step in state machine
140      */
nextStep()141     void nextStep() {
142         if (mAction == ACTION_INIT) {
143             nextStepInit();
144         } else if (mAction == ACTION_CONNECT) {
145             nextStepConnect();
146         } else {
147             nextStepDisconnect();
148         }
149     }
150 
151     /*
152      * Enables bluetooth and gets the profile proxies
153      */
nextStepInit()154     void nextStepInit() {
155         switch (mState) {
156             case STATE_INIT:
157                 if (mA2dp == null || mHeadset == null) {
158                     mState = STATE_WAITING_FOR_PROXIES;
159                     if (!getProfileProxys()) {
160                         complete(false);
161                     }
162                     break;
163                 }
164                 // fall-through
165             case STATE_WAITING_FOR_PROXIES:
166                 mState = STATE_INIT_COMPLETE;
167                 // Check connected devices and see if we need to disconnect
168                 synchronized(mLock) {
169                     if (mA2dp.getConnectedDevices().contains(mDevice) ||
170                             mHeadset.getConnectedDevices().contains(mDevice)) {
171                         Log.i(TAG, "ACTION_DISCONNECT addr=" + mDevice + " name=" + mName);
172                         mAction = ACTION_DISCONNECT;
173                     } else {
174                         Log.i(TAG, "ACTION_CONNECT addr=" + mDevice + " name=" + mName);
175                         mAction = ACTION_CONNECT;
176                     }
177                 }
178                 nextStep();
179         }
180 
181     }
182 
nextStepDisconnect()183     void nextStepDisconnect() {
184         switch (mState) {
185             case STATE_INIT_COMPLETE:
186                 mState = STATE_DISCONNECTING;
187                 synchronized (mLock) {
188                     if (mHeadset.getConnectionState(mDevice) != BluetoothProfile.STATE_DISCONNECTED) {
189                         mHfpResult = RESULT_PENDING;
190                         mHeadset.disconnect(mDevice);
191                     } else {
192                         mHfpResult = RESULT_DISCONNECTED;
193                     }
194                     if (mA2dp.getConnectionState(mDevice) != BluetoothProfile.STATE_DISCONNECTED) {
195                         mA2dpResult = RESULT_PENDING;
196                         mA2dp.disconnect(mDevice);
197                     } else {
198                         mA2dpResult = RESULT_DISCONNECTED;
199                     }
200                     if (mA2dpResult == RESULT_PENDING || mHfpResult == RESULT_PENDING) {
201                         toast(mContext.getString(R.string.disconnecting_headset ) + " " +
202                                 mName + "...");
203                         break;
204                     }
205                 }
206                 // fall-through
207             case STATE_DISCONNECTING:
208                 if (mA2dpResult == RESULT_PENDING || mHfpResult == RESULT_PENDING) {
209                     // still disconnecting
210                     break;
211                 }
212                 if (mA2dpResult == RESULT_DISCONNECTED && mHfpResult == RESULT_DISCONNECTED) {
213                     toast(mContext.getString(R.string.disconnected_headset) + " " + mName);
214                 }
215                 complete(false);
216                 break;
217         }
218 
219     }
220 
getProfileProxys()221     boolean getProfileProxys() {
222         if(!mBluetoothAdapter.getProfileProxy(mContext, this, BluetoothProfile.HEADSET))
223             return false;
224 
225         if(!mBluetoothAdapter.getProfileProxy(mContext, this, BluetoothProfile.A2DP))
226             return false;
227 
228         return true;
229     }
230 
nextStepConnect()231     void nextStepConnect() {
232         switch (mState) {
233             case STATE_INIT_COMPLETE:
234                 if (mDevice.getBondState() != BluetoothDevice.BOND_BONDED) {
235                     requestPairConfirmation();
236                     mState = STATE_WAITING_FOR_BOND_CONFIRMATION;
237 
238                     break;
239                 }
240                 // fall-through
241             case STATE_WAITING_FOR_BOND_CONFIRMATION:
242                 if (mDevice.getBondState() != BluetoothDevice.BOND_BONDED) {
243                     startBonding();
244                     break;
245                 }
246                 // fall-through
247             case STATE_BONDING:
248                 // Bluetooth Profile service will correctly serialize
249                 // HFP then A2DP connect
250                 mState = STATE_CONNECTING;
251                 synchronized (mLock) {
252                     if (mHeadset.getConnectionState(mDevice) != BluetoothProfile.STATE_CONNECTED) {
253                         mHfpResult = RESULT_PENDING;
254                         mHeadset.connect(mDevice);
255                     } else {
256                         mHfpResult = RESULT_CONNECTED;
257                     }
258                     if (mA2dp.getConnectionState(mDevice) != BluetoothProfile.STATE_CONNECTED) {
259                         mA2dpResult = RESULT_PENDING;
260                         mA2dp.connect(mDevice);
261                     } else {
262                         mA2dpResult = RESULT_CONNECTED;
263                     }
264                     if (mA2dpResult == RESULT_PENDING || mHfpResult == RESULT_PENDING) {
265                         toast(mContext.getString(R.string.connecting_headset) + " " + mName + "...");
266                         break;
267                     }
268                 }
269                 // fall-through
270             case STATE_CONNECTING:
271                 if (mA2dpResult == RESULT_PENDING || mHfpResult == RESULT_PENDING) {
272                     // another connection type still pending
273                     break;
274                 }
275                 if (mA2dpResult == RESULT_CONNECTED || mHfpResult == RESULT_CONNECTED) {
276                     // we'll take either as success
277                     toast(mContext.getString(R.string.connected_headset) + " " + mName);
278                     if (mA2dpResult == RESULT_CONNECTED) startTheMusic();
279                     complete(true);
280                 } else {
281                     toast (mContext.getString(R.string.connect_headset_failed) + " " + mName);
282                     complete(false);
283                 }
284                 break;
285         }
286     }
287 
startBonding()288     void startBonding() {
289         mState = STATE_BONDING;
290         toast(mContext.getString(R.string.pairing_headset) + " " + mName + "...");
291         if (!mDevice.createBond()) {
292             toast(mContext.getString(R.string.pairing_headset_failed) + " " + mName);
293             complete(false);
294         }
295     }
296 
handleIntent(Intent intent)297     void handleIntent(Intent intent) {
298         String action = intent.getAction();
299         // Everything requires the device to match...
300         BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
301         if (!mDevice.equals(device)) return;
302 
303         if (ACTION_ALLOW_CONNECT.equals(action)) {
304             nextStepConnect();
305         } else if (ACTION_DENY_CONNECT.equals(action)) {
306             complete(false);
307         } else if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(action) && mState == STATE_BONDING) {
308             int bond = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE,
309                     BluetoothAdapter.ERROR);
310             if (bond == BluetoothDevice.BOND_BONDED) {
311                 nextStepConnect();
312             } else if (bond == BluetoothDevice.BOND_NONE) {
313                 toast(mContext.getString(R.string.pairing_headset_failed) + " " + mName);
314                 complete(false);
315             }
316         } else if (BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED.equals(action) &&
317                 (mState == STATE_CONNECTING || mState == STATE_DISCONNECTING)) {
318             int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothAdapter.ERROR);
319             if (state == BluetoothProfile.STATE_CONNECTED) {
320                 mHfpResult = RESULT_CONNECTED;
321                 nextStep();
322             } else if (state == BluetoothProfile.STATE_DISCONNECTED) {
323                 mHfpResult = RESULT_DISCONNECTED;
324                 nextStep();
325             }
326         } else if (BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED.equals(action) &&
327                 (mState == STATE_CONNECTING || mState == STATE_DISCONNECTING)) {
328             int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothAdapter.ERROR);
329             if (state == BluetoothProfile.STATE_CONNECTED) {
330                 mA2dpResult = RESULT_CONNECTED;
331                 nextStep();
332             } else if (state == BluetoothProfile.STATE_DISCONNECTED) {
333                 mA2dpResult = RESULT_DISCONNECTED;
334                 nextStep();
335             }
336         }
337     }
338 
complete(boolean connected)339     void complete(boolean connected) {
340         if (DBG) Log.d(TAG, "complete()");
341         mState = STATE_COMPLETE;
342         mContext.unregisterReceiver(mReceiver);
343         mHandler.removeMessages(MSG_TIMEOUT);
344         synchronized (mLock) {
345             if (mA2dp != null) {
346                 mBluetoothAdapter.closeProfileProxy(BluetoothProfile.A2DP, mA2dp);
347             }
348             if (mHeadset != null) {
349                 mBluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, mHeadset);
350             }
351             mA2dp = null;
352             mHeadset = null;
353         }
354         mCallback.onBluetoothHeadsetHandoverComplete(connected);
355     }
356 
toast(CharSequence text)357     void toast(CharSequence text) {
358         Toast.makeText(mContext,  text, Toast.LENGTH_SHORT).show();
359     }
360 
startTheMusic()361     void startTheMusic() {
362         Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON);
363         intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN,
364                 KeyEvent.KEYCODE_MEDIA_PLAY));
365         mContext.sendOrderedBroadcast(intent, null);
366         intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP,
367                 KeyEvent.KEYCODE_MEDIA_PLAY));
368         mContext.sendOrderedBroadcast(intent, null);
369     }
370 
requestPairConfirmation()371     void requestPairConfirmation() {
372         Intent dialogIntent = new Intent(mContext, ConfirmConnectActivity.class);
373         dialogIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
374         dialogIntent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice);
375 
376         mContext.startActivity(dialogIntent);
377     }
378 
379     final Handler mHandler = new Handler() {
380         @Override
381         public void handleMessage(Message msg) {
382             switch (msg.what) {
383                 case MSG_TIMEOUT:
384                     if (mState == STATE_COMPLETE) return;
385                     Log.i(TAG, "Timeout completing BT handover");
386                     complete(false);
387                     break;
388                 case MSG_NEXT_STEP:
389                     nextStep();
390                     break;
391             }
392         }
393     };
394 
395     final BroadcastReceiver mReceiver = new BroadcastReceiver() {
396         @Override
397         public void onReceive(Context context, Intent intent) {
398             handleIntent(intent);
399         }
400     };
401 
checkMainThread()402     static void checkMainThread() {
403         if (Looper.myLooper() != Looper.getMainLooper()) {
404             throw new IllegalThreadStateException("must be called on main thread");
405         }
406     }
407 
408     @Override
onServiceConnected(int profile, BluetoothProfile proxy)409     public void onServiceConnected(int profile, BluetoothProfile proxy) {
410         synchronized (mLock) {
411             switch (profile) {
412                 case BluetoothProfile.HEADSET:
413                     mHeadset = (BluetoothHeadset) proxy;
414                     if (mA2dp != null) {
415                         mHandler.sendEmptyMessage(MSG_NEXT_STEP);
416                     }
417                     break;
418                 case BluetoothProfile.A2DP:
419                     mA2dp = (BluetoothA2dp) proxy;
420                     if (mHeadset != null) {
421                         mHandler.sendEmptyMessage(MSG_NEXT_STEP);
422                     }
423                     break;
424             }
425         }
426     }
427 
428     @Override
onServiceDisconnected(int profile)429     public void onServiceDisconnected(int profile) {
430         // We can ignore these
431     }
432 }
433