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