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 { 90 public void onBluetoothHeadsetHandoverComplete(boolean connected); 91 } 92 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 */ 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 */ 140 void nextStep() { 141 if (mAction == ACTION_CONNECT) { 142 nextStepConnect(); 143 } else { 144 nextStepDisconnect(); 145 } 146 } 147 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 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 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 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 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 319 void toast(CharSequence text) { 320 Toast.makeText(mContext, text, Toast.LENGTH_SHORT).show(); 321 } 322 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 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 362 static void checkMainThread() { 363 if (Looper.myLooper() != Looper.getMainLooper()) { 364 throw new IllegalThreadStateException("must be called on main thread"); 365 } 366 } 367 } 368