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