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.BluetoothClass; 22 import android.bluetooth.BluetoothDevice; 23 import android.bluetooth.BluetoothHeadset; 24 import android.bluetooth.BluetoothHidHost; 25 import android.bluetooth.BluetoothProfile; 26 import android.bluetooth.BluetoothUuid; 27 import android.bluetooth.OobData; 28 import android.content.BroadcastReceiver; 29 import android.content.ContentResolver; 30 import android.content.Context; 31 import android.content.Intent; 32 import android.content.IntentFilter; 33 import android.media.AudioManager; 34 import android.os.Handler; 35 import android.os.Looper; 36 import android.os.Message; 37 import android.os.ParcelUuid; 38 import android.provider.Settings; 39 import android.sysprop.NfcProperties; 40 import android.util.Log; 41 import android.view.KeyEvent; 42 import android.widget.Toast; 43 44 import com.android.nfc.R; 45 46 /** 47 * Connects / Disconnects from a Bluetooth headset (or any device that 48 * might implement BT HSP, HFP, A2DP, or HOGP sink) when touched with NFC. 49 * 50 * This object is created on an NFC interaction, and determines what 51 * sequence of Bluetooth actions to take, and executes them. It is not 52 * designed to be re-used after the sequence has completed or timed out. 53 * Subsequent NFC interactions should use new objects. 54 * 55 */ 56 public class BluetoothPeripheralHandover implements BluetoothProfile.ServiceListener { 57 static final String TAG = "BluetoothPeripheralHandover"; 58 static final boolean DBG = NfcProperties.debug_enabled().orElse(true); 59 60 static final String ACTION_ALLOW_CONNECT = "com.android.nfc.handover.action.ALLOW_CONNECT"; 61 static final String ACTION_DENY_CONNECT = "com.android.nfc.handover.action.DENY_CONNECT"; 62 static final String ACTION_TIMEOUT_CONNECT = "com.android.nfc.handover.action.TIMEOUT_CONNECT"; 63 64 static final int TIMEOUT_MS = 20000; 65 static final int RETRY_PAIRING_WAIT_TIME_MS = 2000; 66 static final int RETRY_CONNECT_WAIT_TIME_MS = 5000; 67 68 static final int STATE_INIT = 0; 69 static final int STATE_WAITING_FOR_PROXIES = 1; 70 static final int STATE_INIT_COMPLETE = 2; 71 static final int STATE_WAITING_FOR_BOND_CONFIRMATION = 3; 72 static final int STATE_BONDING = 4; 73 static final int STATE_CONNECTING = 5; 74 static final int STATE_DISCONNECTING = 6; 75 static final int STATE_COMPLETE = 7; 76 77 static final int RESULT_PENDING = 0; 78 static final int RESULT_CONNECTED = 1; 79 static final int RESULT_DISCONNECTED = 2; 80 81 static final int ACTION_INIT = 0; 82 static final int ACTION_DISCONNECT = 1; 83 static final int ACTION_CONNECT = 2; 84 85 static final int MSG_TIMEOUT = 1; 86 static final int MSG_NEXT_STEP = 2; 87 static final int MSG_RETRY = 3; 88 89 static final int MAX_RETRY_COUNT = 3; 90 91 final Context mContext; 92 final BluetoothDevice mDevice; 93 final String mName; 94 final Callback mCallback; 95 final BluetoothAdapter mBluetoothAdapter; 96 final int mTransport; 97 final boolean mProvisioning; 98 final AudioManager mAudioManager; 99 100 final Object mLock = new Object(); 101 102 // only used on main thread 103 int mAction; 104 int mState; 105 int mHfpResult; // used only in STATE_CONNECTING and STATE_DISCONNETING 106 int mA2dpResult; // used only in STATE_CONNECTING and STATE_DISCONNETING 107 int mHidResult; 108 int mRetryCount; 109 OobData mOobData; 110 boolean mIsHeadsetAvailable; 111 boolean mIsA2dpAvailable; 112 boolean mIsMusicActive; 113 114 // protected by mLock 115 BluetoothA2dp mA2dp; 116 BluetoothHeadset mHeadset; 117 BluetoothHidHost mInput; 118 119 public interface Callback { onBluetoothPeripheralHandoverComplete(boolean connected)120 public void onBluetoothPeripheralHandoverComplete(boolean connected); 121 } 122 BluetoothPeripheralHandover(Context context, BluetoothDevice device, String name, int transport, OobData oobData, ParcelUuid[] uuids, BluetoothClass btClass, Callback callback)123 public BluetoothPeripheralHandover(Context context, BluetoothDevice device, String name, 124 int transport, OobData oobData, ParcelUuid[] uuids, BluetoothClass btClass, 125 Callback callback) { 126 checkMainThread(); // mHandler must get get constructed on Main Thread for toasts to work 127 mContext = context; 128 mDevice = device; 129 mName = name; 130 mTransport = transport; 131 mOobData = oobData; 132 mCallback = callback; 133 mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); 134 135 ContentResolver contentResolver = mContext.getContentResolver(); 136 mProvisioning = Settings.Global.getInt(contentResolver, 137 Settings.Global.DEVICE_PROVISIONED, 0) == 0; 138 139 mIsHeadsetAvailable = hasHeadsetCapability(uuids, btClass); 140 mIsA2dpAvailable = hasA2dpCapability(uuids, btClass); 141 142 // Capability information is from NDEF optional field, then it might be empty. 143 // If all capabilities indicate false, try to connect Headset and A2dp just in case. 144 if (!mIsHeadsetAvailable && !mIsA2dpAvailable) { 145 mIsHeadsetAvailable = true; 146 mIsA2dpAvailable = true; 147 } 148 149 mAudioManager = mContext.getSystemService(AudioManager.class); 150 151 mState = STATE_INIT; 152 } 153 hasStarted()154 public boolean hasStarted() { 155 return mState != STATE_INIT; 156 } 157 158 /** 159 * Main entry point. This method is usually called after construction, 160 * to begin the BT sequence. Must be called on Main thread. 161 */ start()162 public boolean start() { 163 checkMainThread(); 164 if (mState != STATE_INIT || mBluetoothAdapter == null 165 || (mProvisioning && mTransport != BluetoothDevice.TRANSPORT_LE)) { 166 return false; 167 } 168 169 170 IntentFilter filter = new IntentFilter(); 171 filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED); 172 filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED); 173 filter.addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED); 174 filter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED); 175 filter.addAction(BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED); 176 filter.addAction(ACTION_ALLOW_CONNECT); 177 filter.addAction(ACTION_DENY_CONNECT); 178 179 mContext.registerReceiver(mReceiver, filter); 180 181 mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_TIMEOUT), TIMEOUT_MS); 182 183 mAction = ACTION_INIT; 184 mRetryCount = 0; 185 186 nextStep(); 187 188 return true; 189 } 190 191 /** 192 * Called to execute next step in state machine 193 */ nextStep()194 void nextStep() { 195 if (mAction == ACTION_INIT) { 196 nextStepInit(); 197 } else if (mAction == ACTION_CONNECT) { 198 nextStepConnect(); 199 } else { 200 nextStepDisconnect(); 201 } 202 } 203 204 /* 205 * Enables bluetooth and gets the profile proxies 206 */ nextStepInit()207 void nextStepInit() { 208 switch (mState) { 209 case STATE_INIT: 210 if (mA2dp == null || mHeadset == null || mInput == null) { 211 mState = STATE_WAITING_FOR_PROXIES; 212 if (!getProfileProxys()) { 213 complete(false); 214 } 215 break; 216 } 217 // fall-through 218 case STATE_WAITING_FOR_PROXIES: 219 mState = STATE_INIT_COMPLETE; 220 // Check connected devices and see if we need to disconnect 221 synchronized(mLock) { 222 if (mTransport == BluetoothDevice.TRANSPORT_LE) { 223 if (mInput.getConnectedDevices().contains(mDevice)) { 224 Log.i(TAG, "ACTION_DISCONNECT addr=" + mDevice + " name=" + mName); 225 mAction = ACTION_DISCONNECT; 226 } else { 227 Log.i(TAG, "ACTION_CONNECT addr=" + mDevice + " name=" + mName); 228 mAction = ACTION_CONNECT; 229 } 230 } else { 231 if (mA2dp.getConnectedDevices().contains(mDevice) || 232 mHeadset.getConnectedDevices().contains(mDevice)) { 233 Log.i(TAG, "ACTION_DISCONNECT addr=" + mDevice + " name=" + mName); 234 mAction = ACTION_DISCONNECT; 235 } else { 236 // Check if each profile of the device is disabled or not 237 if (mHeadset.getConnectionPolicy(mDevice) == 238 BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) { 239 mIsHeadsetAvailable = false; 240 } 241 if (mA2dp.getConnectionPolicy(mDevice) == 242 BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) { 243 mIsA2dpAvailable = false; 244 } 245 if (!mIsHeadsetAvailable && !mIsA2dpAvailable) { 246 Log.i(TAG, "Both Headset and A2DP profiles are unavailable"); 247 complete(false); 248 break; 249 } 250 Log.i(TAG, "ACTION_CONNECT addr=" + mDevice + " name=" + mName); 251 mAction = ACTION_CONNECT; 252 253 if (mIsA2dpAvailable) { 254 mIsMusicActive = mAudioManager.isMusicActive(); 255 } 256 } 257 } 258 } 259 nextStep(); 260 } 261 262 } 263 nextStepDisconnect()264 void nextStepDisconnect() { 265 switch (mState) { 266 case STATE_INIT_COMPLETE: 267 mState = STATE_DISCONNECTING; 268 synchronized (mLock) { 269 if (mTransport == BluetoothDevice.TRANSPORT_LE) { 270 if (mInput.getConnectionState(mDevice) 271 != BluetoothProfile.STATE_DISCONNECTED) { 272 mHidResult = RESULT_PENDING; 273 mDevice.disconnect(); 274 toast(getToastString(R.string.disconnecting_peripheral)); 275 break; 276 } else { 277 mHidResult = RESULT_DISCONNECTED; 278 } 279 } else { 280 if (mHeadset.getConnectionState(mDevice) 281 != BluetoothProfile.STATE_DISCONNECTED) { 282 mHfpResult = RESULT_PENDING; 283 } else { 284 mHfpResult = RESULT_DISCONNECTED; 285 } 286 if (mA2dp.getConnectionState(mDevice) 287 != BluetoothProfile.STATE_DISCONNECTED) { 288 mA2dpResult = RESULT_PENDING; 289 } else { 290 mA2dpResult = RESULT_DISCONNECTED; 291 } 292 if (mA2dpResult == RESULT_PENDING || mHfpResult == RESULT_PENDING) { 293 mDevice.disconnect(); 294 toast(getToastString(R.string.disconnecting_peripheral)); 295 break; 296 } 297 } 298 } 299 // fall-through 300 case STATE_DISCONNECTING: 301 if (mTransport == BluetoothDevice.TRANSPORT_LE) { 302 if (mHidResult == RESULT_DISCONNECTED) { 303 toast(getToastString(R.string.disconnected_peripheral)); 304 complete(false); 305 } 306 307 break; 308 } else { 309 if (mA2dpResult == RESULT_PENDING || mHfpResult == RESULT_PENDING) { 310 // still disconnecting 311 break; 312 } 313 if (mA2dpResult == RESULT_DISCONNECTED && mHfpResult == RESULT_DISCONNECTED) { 314 toast(getToastString(R.string.disconnected_peripheral)); 315 } 316 complete(false); 317 break; 318 } 319 320 } 321 322 } 323 getToastString(int resid)324 private String getToastString(int resid) { 325 return mContext.getString(resid, mName != null ? mName : R.string.device); 326 } 327 getProfileProxys()328 boolean getProfileProxys() { 329 330 if (mTransport == BluetoothDevice.TRANSPORT_LE) { 331 if (!mBluetoothAdapter.getProfileProxy(mContext, this, BluetoothProfile.HID_HOST)) 332 return false; 333 } else { 334 if(!mBluetoothAdapter.getProfileProxy(mContext, this, BluetoothProfile.HEADSET)) 335 return false; 336 337 if(!mBluetoothAdapter.getProfileProxy(mContext, this, BluetoothProfile.A2DP)) 338 return false; 339 } 340 341 return true; 342 } 343 nextStepConnect()344 void nextStepConnect() { 345 switch (mState) { 346 case STATE_INIT_COMPLETE: 347 348 if (mDevice.getBondState() != BluetoothDevice.BOND_BONDED) { 349 requestPairConfirmation(); 350 mState = STATE_WAITING_FOR_BOND_CONFIRMATION; 351 break; 352 } 353 354 if (mTransport == BluetoothDevice.TRANSPORT_LE) { 355 if (mDevice.getBondState() != BluetoothDevice.BOND_NONE) { 356 mDevice.removeBond(); 357 requestPairConfirmation(); 358 mState = STATE_WAITING_FOR_BOND_CONFIRMATION; 359 break; 360 } 361 } 362 // fall-through 363 case STATE_WAITING_FOR_BOND_CONFIRMATION: 364 if (mDevice.getBondState() != BluetoothDevice.BOND_BONDED) { 365 startBonding(); 366 break; 367 } 368 // fall-through 369 case STATE_BONDING: 370 // Bluetooth Profile service will correctly serialize 371 // HFP then A2DP connect 372 mState = STATE_CONNECTING; 373 synchronized (mLock) { 374 if (mTransport == BluetoothDevice.TRANSPORT_LE) { 375 if (mInput.getConnectionState(mDevice) 376 != BluetoothProfile.STATE_CONNECTED) { 377 mHidResult = RESULT_PENDING; 378 toast(getToastString(R.string.connecting_peripheral)); 379 break; 380 } else { 381 mHidResult = RESULT_CONNECTED; 382 } 383 } else { 384 if (mHeadset.getConnectionState(mDevice) != 385 BluetoothProfile.STATE_CONNECTED) { 386 if (mIsHeadsetAvailable) { 387 mHfpResult = RESULT_PENDING; 388 mHeadset.setConnectionPolicy(mDevice, 389 BluetoothProfile.CONNECTION_POLICY_ALLOWED); 390 } else { 391 mHfpResult = RESULT_DISCONNECTED; 392 } 393 } else { 394 mHfpResult = RESULT_CONNECTED; 395 } 396 if (mA2dp.getConnectionState(mDevice) != BluetoothProfile.STATE_CONNECTED) { 397 if (mIsA2dpAvailable) { 398 mA2dpResult = RESULT_PENDING; 399 mA2dp.setConnectionPolicy(mDevice, 400 BluetoothProfile.CONNECTION_POLICY_ALLOWED); 401 } else { 402 mA2dpResult = RESULT_DISCONNECTED; 403 } 404 } else { 405 mA2dpResult = RESULT_CONNECTED; 406 } 407 if (mA2dpResult == RESULT_PENDING || mHfpResult == RESULT_PENDING) { 408 if (mRetryCount == 0) { 409 toast(getToastString(R.string.connecting_peripheral)); 410 } 411 if (mRetryCount < MAX_RETRY_COUNT) { 412 sendRetryMessage(RETRY_CONNECT_WAIT_TIME_MS); 413 break; 414 } 415 } 416 } 417 } 418 // fall-through 419 case STATE_CONNECTING: 420 if (mTransport == BluetoothDevice.TRANSPORT_LE) { 421 if (mHidResult == RESULT_PENDING) { 422 break; 423 } else if (mHidResult == RESULT_CONNECTED) { 424 toast(getToastString(R.string.connected_peripheral)); 425 mDevice.setAlias(mName); 426 complete(true); 427 } else { 428 toast (getToastString(R.string.connect_peripheral_failed)); 429 complete(false); 430 } 431 } else { 432 if (mA2dpResult == RESULT_PENDING || mHfpResult == RESULT_PENDING) { 433 // another connection type still pending 434 break; 435 } 436 if (mA2dpResult == RESULT_CONNECTED || mHfpResult == RESULT_CONNECTED) { 437 // we'll take either as success 438 toast(getToastString(R.string.connected_peripheral)); 439 if (mA2dpResult == RESULT_CONNECTED) startTheMusic(); 440 mDevice.setAlias(mName); 441 complete(true); 442 } else { 443 toast (getToastString(R.string.connect_peripheral_failed)); 444 complete(false); 445 } 446 } 447 break; 448 } 449 } 450 startBonding()451 void startBonding() { 452 mState = STATE_BONDING; 453 if (mRetryCount == 0) { 454 toast(getToastString(R.string.pairing_peripheral)); 455 } 456 if (mOobData != null) { 457 if (!mDevice.createBondOutOfBand(mTransport, /* p192 not implemented for LE */ null, 458 mOobData)) { 459 toast(getToastString(R.string.pairing_peripheral_failed)); 460 complete(false); 461 } 462 } else if (!mDevice.createBond(mTransport)) { 463 toast(getToastString(R.string.pairing_peripheral_failed)); 464 complete(false); 465 } 466 } 467 handleIntent(Intent intent)468 void handleIntent(Intent intent) { 469 String action = intent.getAction(); 470 // Everything requires the device to match... 471 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 472 if (!mDevice.equals(device)) return; 473 474 if (ACTION_ALLOW_CONNECT.equals(action)) { 475 mHandler.removeMessages(MSG_TIMEOUT); 476 mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_TIMEOUT), TIMEOUT_MS); 477 nextStepConnect(); 478 } else if (ACTION_DENY_CONNECT.equals(action)) { 479 complete(false); 480 } else if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(action) 481 && mState == STATE_BONDING) { 482 int bond = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, 483 BluetoothAdapter.ERROR); 484 if (bond == BluetoothDevice.BOND_BONDED) { 485 mRetryCount = 0; 486 nextStepConnect(); 487 } else if (bond == BluetoothDevice.BOND_NONE) { 488 int reason = intent.getIntExtra(BluetoothDevice.EXTRA_UNBOND_REASON, 489 BluetoothAdapter.ERROR); 490 if (mRetryCount < MAX_RETRY_COUNT 491 && reason != BluetoothDevice.UNBOND_REASON_AUTH_FAILED) { 492 sendRetryMessage(RETRY_PAIRING_WAIT_TIME_MS); 493 } else { 494 toast(getToastString(R.string.pairing_peripheral_failed)); 495 complete(false); 496 } 497 } 498 } else if (BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED.equals(action) && 499 (mState == STATE_CONNECTING || mState == STATE_DISCONNECTING)) { 500 int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothAdapter.ERROR); 501 if (state == BluetoothProfile.STATE_CONNECTED) { 502 mHfpResult = RESULT_CONNECTED; 503 nextStep(); 504 } else if (state == BluetoothProfile.STATE_DISCONNECTED) { 505 if (mAction == ACTION_CONNECT && mRetryCount < MAX_RETRY_COUNT) { 506 sendRetryMessage(RETRY_CONNECT_WAIT_TIME_MS); 507 } else { 508 mHfpResult = RESULT_DISCONNECTED; 509 nextStep(); 510 } 511 } 512 } else if (BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED.equals(action) && 513 (mState == STATE_CONNECTING || mState == STATE_DISCONNECTING)) { 514 int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothAdapter.ERROR); 515 if (state == BluetoothProfile.STATE_CONNECTED) { 516 mA2dpResult = RESULT_CONNECTED; 517 nextStep(); 518 } else if (state == BluetoothProfile.STATE_DISCONNECTED) { 519 if (mAction == ACTION_CONNECT && mRetryCount < MAX_RETRY_COUNT) { 520 sendRetryMessage(RETRY_CONNECT_WAIT_TIME_MS); 521 } else { 522 mA2dpResult = RESULT_DISCONNECTED; 523 nextStep(); 524 } 525 } 526 } else if (BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED.equals(action) && 527 (mState == STATE_CONNECTING || mState == STATE_DISCONNECTING)) { 528 int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothAdapter.ERROR); 529 if (state == BluetoothProfile.STATE_CONNECTED) { 530 mHidResult = RESULT_CONNECTED; 531 nextStep(); 532 } else if (state == BluetoothProfile.STATE_DISCONNECTED) { 533 mHidResult = RESULT_DISCONNECTED; 534 nextStep(); 535 } 536 } 537 } 538 complete(boolean connected)539 void complete(boolean connected) { 540 if (DBG) Log.d(TAG, "complete()"); 541 mState = STATE_COMPLETE; 542 mContext.unregisterReceiver(mReceiver); 543 mHandler.removeMessages(MSG_TIMEOUT); 544 mHandler.removeMessages(MSG_RETRY); 545 synchronized (mLock) { 546 if (mA2dp != null) { 547 mBluetoothAdapter.closeProfileProxy(BluetoothProfile.A2DP, mA2dp); 548 } 549 if (mHeadset != null) { 550 mBluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, mHeadset); 551 } 552 553 if (mInput != null) { 554 mBluetoothAdapter.closeProfileProxy(BluetoothProfile.HID_HOST, mInput); 555 } 556 557 mA2dp = null; 558 mHeadset = null; 559 mInput = null; 560 } 561 mCallback.onBluetoothPeripheralHandoverComplete(connected); 562 } 563 toast(CharSequence text)564 void toast(CharSequence text) { 565 Toast.makeText(mContext, text, Toast.LENGTH_SHORT).show(); 566 } 567 startTheMusic()568 void startTheMusic() { 569 if (!mContext.getResources().getBoolean(R.bool.enable_auto_play) && !mIsMusicActive) { 570 return; 571 } 572 573 KeyEvent keyEvent = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY); 574 mAudioManager.dispatchMediaKeyEvent(keyEvent); 575 keyEvent = new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PLAY); 576 mAudioManager.dispatchMediaKeyEvent(keyEvent); 577 } 578 requestPairConfirmation()579 void requestPairConfirmation() { 580 Intent dialogIntent = new Intent(mContext, ConfirmConnectActivity.class); 581 dialogIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); 582 dialogIntent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice); 583 dialogIntent.putExtra(BluetoothDevice.EXTRA_NAME, mName); 584 585 mContext.startActivity(dialogIntent); 586 } 587 hasA2dpCapability(ParcelUuid[] uuids, BluetoothClass btClass)588 boolean hasA2dpCapability(ParcelUuid[] uuids, BluetoothClass btClass) { 589 if (uuids != null) { 590 for (ParcelUuid uuid : uuids) { 591 if (uuid.equals(BluetoothUuid.A2DP_SINK) || uuid.equals(BluetoothUuid.ADV_AUDIO_DIST)) { 592 return true; 593 } 594 } 595 } 596 if (btClass != null && btClass.doesClassMatch(BluetoothClass.PROFILE_A2DP)) { 597 return true; 598 } 599 return false; 600 } 601 hasHeadsetCapability(ParcelUuid[] uuids, BluetoothClass btClass)602 boolean hasHeadsetCapability(ParcelUuid[] uuids, BluetoothClass btClass) { 603 if (uuids != null) { 604 for (ParcelUuid uuid : uuids) { 605 if (uuid.equals(BluetoothUuid.HFP) || uuid.equals(BluetoothUuid.HSP)) { 606 return true; 607 } 608 } 609 } 610 if (btClass != null && btClass.doesClassMatch(BluetoothClass.PROFILE_HEADSET)) { 611 return true; 612 } 613 return false; 614 } 615 616 final Handler mHandler = new Handler() { 617 @Override 618 public void handleMessage(Message msg) { 619 switch (msg.what) { 620 case MSG_TIMEOUT: 621 if (mState == STATE_COMPLETE) return; 622 Log.i(TAG, "Timeout completing BT handover"); 623 if (mState == STATE_WAITING_FOR_BOND_CONFIRMATION) { 624 mContext.sendBroadcast(new Intent(ACTION_TIMEOUT_CONNECT)); 625 } else if (mState == STATE_BONDING) { 626 toast(getToastString(R.string.pairing_peripheral_failed)); 627 } else if (mState == STATE_CONNECTING) { 628 if (mHidResult == RESULT_PENDING) { 629 mHidResult = RESULT_DISCONNECTED; 630 } 631 if (mA2dpResult == RESULT_PENDING) { 632 mA2dpResult = RESULT_DISCONNECTED; 633 } 634 if (mHfpResult == RESULT_PENDING) { 635 mHfpResult = RESULT_DISCONNECTED; 636 } 637 // Check if any one profile is connected, then it takes as success 638 nextStepConnect(); 639 break; 640 } 641 complete(false); 642 break; 643 case MSG_NEXT_STEP: 644 nextStep(); 645 break; 646 case MSG_RETRY: 647 mHandler.removeMessages(MSG_RETRY); 648 if (mState == STATE_BONDING) { 649 mState = STATE_WAITING_FOR_BOND_CONFIRMATION; 650 } else if (mState == STATE_CONNECTING) { 651 mState = STATE_BONDING; 652 } 653 mRetryCount++; 654 nextStepConnect(); 655 break; 656 } 657 } 658 }; 659 660 final BroadcastReceiver mReceiver = new BroadcastReceiver() { 661 @Override 662 public void onReceive(Context context, Intent intent) { 663 handleIntent(intent); 664 } 665 }; 666 checkMainThread()667 static void checkMainThread() { 668 if (Looper.myLooper() != Looper.getMainLooper()) { 669 throw new IllegalThreadStateException("must be called on main thread"); 670 } 671 } 672 673 @Override onServiceConnected(int profile, BluetoothProfile proxy)674 public void onServiceConnected(int profile, BluetoothProfile proxy) { 675 synchronized (mLock) { 676 switch (profile) { 677 case BluetoothProfile.HEADSET: 678 mHeadset = (BluetoothHeadset) proxy; 679 if (mA2dp != null) { 680 mHandler.sendEmptyMessage(MSG_NEXT_STEP); 681 } 682 break; 683 case BluetoothProfile.A2DP: 684 mA2dp = (BluetoothA2dp) proxy; 685 if (mHeadset != null) { 686 mHandler.sendEmptyMessage(MSG_NEXT_STEP); 687 } 688 break; 689 case BluetoothProfile.HID_HOST: 690 mInput = (BluetoothHidHost) proxy; 691 if (mInput != null) { 692 mHandler.sendEmptyMessage(MSG_NEXT_STEP); 693 } 694 break; 695 } 696 } 697 } 698 699 @Override onServiceDisconnected(int profile)700 public void onServiceDisconnected(int profile) { 701 // We can ignore these 702 } 703 sendRetryMessage(int waitTime)704 void sendRetryMessage(int waitTime) { 705 if (!mHandler.hasMessages(MSG_RETRY)) { 706 mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_RETRY), waitTime); 707 } 708 } 709 } 710