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