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