BluetoothDeviceProfileState.java revision d50ac2966edd9c58d8026a624fd795bf54311158
1/* 2 * Copyright (C) 2010 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 17package android.bluetooth; 18 19import android.content.BroadcastReceiver; 20import android.content.Context; 21import android.content.Intent; 22import android.content.IntentFilter; 23import android.os.Message; 24import android.server.BluetoothA2dpService; 25import android.server.BluetoothService; 26import android.util.Log; 27 28import com.android.internal.util.HierarchicalState; 29import com.android.internal.util.HierarchicalStateMachine; 30 31/** 32 * This class is the Profile connection state machine associated with a remote 33 * device. When the device bonds an instance of this class is created. 34 * This tracks incoming and outgoing connections of all the profiles. Incoming 35 * connections are preferred over outgoing connections and HFP preferred over 36 * A2DP. When the device is unbonded, the instance is removed. 37 * 38 * States: 39 * {@link BondedDevice}: This state represents a bonded device. When in this 40 * state none of the profiles are in transition states. 41 * 42 * {@link OutgoingHandsfree}: Handsfree profile connection is in a transition 43 * state because of a outgoing Connect or Disconnect. 44 * 45 * {@link IncomingHandsfree}: Handsfree profile connection is in a transition 46 * state because of a incoming Connect or Disconnect. 47 * 48 * {@link IncomingA2dp}: A2dp profile connection is in a transition 49 * state because of a incoming Connect or Disconnect. 50 * 51 * {@link OutgoingA2dp}: A2dp profile connection is in a transition 52 * state because of a outgoing Connect or Disconnect. 53 * 54 * Todo(): Write tests for this class, when the Android Mock support is completed. 55 * @hide 56 */ 57public final class BluetoothDeviceProfileState extends HierarchicalStateMachine { 58 private static final String TAG = "BluetoothDeviceProfileState"; 59 private static final boolean DBG = false; 60 61 public static final int CONNECT_HFP_OUTGOING = 1; 62 public static final int CONNECT_HFP_INCOMING = 2; 63 public static final int CONNECT_A2DP_OUTGOING = 3; 64 public static final int CONNECT_A2DP_INCOMING = 4; 65 66 public static final int DISCONNECT_HFP_OUTGOING = 5; 67 private static final int DISCONNECT_HFP_INCOMING = 6; 68 public static final int DISCONNECT_A2DP_OUTGOING = 7; 69 public static final int DISCONNECT_A2DP_INCOMING = 8; 70 public static final int DISCONNECT_PBAP_OUTGOING = 9; 71 72 public static final int UNPAIR = 100; 73 public static final int AUTO_CONNECT_PROFILES = 101; 74 public static final int TRANSITION_TO_STABLE = 102; 75 public static final int CONNECT_OTHER_PROFILES = 103; 76 77 private static final int AUTO_CONNECT_DELAY = 6000; // 6 secs 78 79 private BondedDevice mBondedDevice = new BondedDevice(); 80 private OutgoingHandsfree mOutgoingHandsfree = new OutgoingHandsfree(); 81 private IncomingHandsfree mIncomingHandsfree = new IncomingHandsfree(); 82 private IncomingA2dp mIncomingA2dp = new IncomingA2dp(); 83 private OutgoingA2dp mOutgoingA2dp = new OutgoingA2dp(); 84 85 private Context mContext; 86 private BluetoothService mService; 87 private BluetoothA2dpService mA2dpService; 88 private BluetoothHeadset mHeadsetService; 89 private BluetoothPbap mPbapService; 90 private boolean mHeadsetServiceConnected; 91 private boolean mPbapServiceConnected; 92 93 private BluetoothDevice mDevice; 94 private int mHeadsetState; 95 private int mA2dpState; 96 97 private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { 98 @Override 99 public void onReceive(Context context, Intent intent) { 100 String action = intent.getAction(); 101 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 102 if (!device.equals(mDevice)) return; 103 104 if (action.equals(BluetoothHeadset.ACTION_STATE_CHANGED)) { 105 int newState = intent.getIntExtra(BluetoothHeadset.EXTRA_STATE, 0); 106 int oldState = intent.getIntExtra(BluetoothHeadset.EXTRA_PREVIOUS_STATE, 0); 107 int initiator = intent.getIntExtra( 108 BluetoothHeadset.EXTRA_DISCONNECT_INITIATOR, 109 BluetoothHeadset.LOCAL_DISCONNECT); 110 mHeadsetState = newState; 111 if (newState == BluetoothHeadset.STATE_DISCONNECTED && 112 initiator == BluetoothHeadset.REMOTE_DISCONNECT) { 113 sendMessage(DISCONNECT_HFP_INCOMING); 114 } 115 if (newState == BluetoothHeadset.STATE_CONNECTED || 116 newState == BluetoothHeadset.STATE_DISCONNECTED) { 117 sendMessage(TRANSITION_TO_STABLE); 118 } 119 } else if (action.equals(BluetoothA2dp.ACTION_SINK_STATE_CHANGED)) { 120 int newState = intent.getIntExtra(BluetoothA2dp.EXTRA_SINK_STATE, 0); 121 int oldState = intent.getIntExtra(BluetoothA2dp.EXTRA_PREVIOUS_SINK_STATE, 0); 122 mA2dpState = newState; 123 if ((oldState == BluetoothA2dp.STATE_CONNECTED || 124 oldState == BluetoothA2dp.STATE_PLAYING) && 125 newState == BluetoothA2dp.STATE_DISCONNECTED) { 126 sendMessage(DISCONNECT_A2DP_INCOMING); 127 } 128 if (newState == BluetoothA2dp.STATE_CONNECTED || 129 newState == BluetoothA2dp.STATE_DISCONNECTED) { 130 sendMessage(TRANSITION_TO_STABLE); 131 } 132 } else if (action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED)) { 133 // This is technically not needed, but we can get stuck sometimes. 134 // For example, if incoming A2DP fails, we are not informed by Bluez 135 sendMessage(TRANSITION_TO_STABLE); 136 } 137 } 138 }; 139 140 private boolean isPhoneDocked(BluetoothDevice autoConnectDevice) { 141 // This works only because these broadcast intents are "sticky" 142 Intent i = mContext.registerReceiver(null, new IntentFilter(Intent.ACTION_DOCK_EVENT)); 143 if (i != null) { 144 int state = i.getIntExtra(Intent.EXTRA_DOCK_STATE, Intent.EXTRA_DOCK_STATE_UNDOCKED); 145 if (state != Intent.EXTRA_DOCK_STATE_UNDOCKED) { 146 BluetoothDevice device = i.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 147 if (device != null && autoConnectDevice.equals(device)) { 148 return true; 149 } 150 } 151 } 152 return false; 153 } 154 155 public BluetoothDeviceProfileState(Context context, String address, 156 BluetoothService service, BluetoothA2dpService a2dpService) { 157 super(address); 158 mContext = context; 159 mDevice = new BluetoothDevice(address); 160 mService = service; 161 mA2dpService = a2dpService; 162 163 addState(mBondedDevice); 164 addState(mOutgoingHandsfree); 165 addState(mIncomingHandsfree); 166 addState(mIncomingA2dp); 167 addState(mOutgoingA2dp); 168 setInitialState(mBondedDevice); 169 170 IntentFilter filter = new IntentFilter(); 171 // Fine-grained state broadcasts 172 filter.addAction(BluetoothA2dp.ACTION_SINK_STATE_CHANGED); 173 filter.addAction(BluetoothHeadset.ACTION_STATE_CHANGED); 174 filter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED); 175 filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED); 176 177 mContext.registerReceiver(mBroadcastReceiver, filter); 178 179 HeadsetServiceListener l = new HeadsetServiceListener(); 180 PbapServiceListener p = new PbapServiceListener(); 181 } 182 183 private class HeadsetServiceListener implements BluetoothHeadset.ServiceListener { 184 public HeadsetServiceListener() { 185 mHeadsetService = new BluetoothHeadset(mContext, this); 186 } 187 public void onServiceConnected() { 188 synchronized(BluetoothDeviceProfileState.this) { 189 mHeadsetServiceConnected = true; 190 } 191 } 192 public void onServiceDisconnected() { 193 synchronized(BluetoothDeviceProfileState.this) { 194 mHeadsetServiceConnected = false; 195 } 196 } 197 } 198 199 private class PbapServiceListener implements BluetoothPbap.ServiceListener { 200 public PbapServiceListener() { 201 mPbapService = new BluetoothPbap(mContext, this); 202 } 203 public void onServiceConnected() { 204 synchronized(BluetoothDeviceProfileState.this) { 205 mPbapServiceConnected = true; 206 } 207 } 208 public void onServiceDisconnected() { 209 synchronized(BluetoothDeviceProfileState.this) { 210 mPbapServiceConnected = false; 211 } 212 } 213 } 214 215 private class BondedDevice extends HierarchicalState { 216 @Override 217 protected void enter() { 218 Log.i(TAG, "Entering ACL Connected state with: " + getCurrentMessage().what); 219 Message m = new Message(); 220 m.copyFrom(getCurrentMessage()); 221 sendMessageAtFrontOfQueue(m); 222 } 223 @Override 224 protected boolean processMessage(Message message) { 225 log("ACL Connected State -> Processing Message: " + message.what); 226 switch(message.what) { 227 case CONNECT_HFP_OUTGOING: 228 case DISCONNECT_HFP_OUTGOING: 229 transitionTo(mOutgoingHandsfree); 230 break; 231 case CONNECT_HFP_INCOMING: 232 transitionTo(mIncomingHandsfree); 233 break; 234 case DISCONNECT_HFP_INCOMING: 235 transitionTo(mIncomingHandsfree); 236 break; 237 case CONNECT_A2DP_OUTGOING: 238 case DISCONNECT_A2DP_OUTGOING: 239 transitionTo(mOutgoingA2dp); 240 break; 241 case CONNECT_A2DP_INCOMING: 242 case DISCONNECT_A2DP_INCOMING: 243 transitionTo(mIncomingA2dp); 244 break; 245 case DISCONNECT_PBAP_OUTGOING: 246 processCommand(DISCONNECT_PBAP_OUTGOING); 247 break; 248 case UNPAIR: 249 if (mHeadsetState != BluetoothHeadset.STATE_DISCONNECTED) { 250 sendMessage(DISCONNECT_HFP_OUTGOING); 251 deferMessage(message); 252 break; 253 } else if (mA2dpState != BluetoothA2dp.STATE_DISCONNECTED) { 254 sendMessage(DISCONNECT_A2DP_OUTGOING); 255 deferMessage(message); 256 break; 257 } 258 processCommand(UNPAIR); 259 break; 260 case AUTO_CONNECT_PROFILES: 261 if (isPhoneDocked(mDevice)) { 262 // Don't auto connect to docks. 263 break; 264 } else if (!mHeadsetServiceConnected) { 265 deferMessage(message); 266 } else { 267 if (mHeadsetService.getPriority(mDevice) == 268 BluetoothHeadset.PRIORITY_AUTO_CONNECT && 269 !mHeadsetService.isConnected(mDevice)) { 270 Log.i(TAG, "Headset:Auto Connect Profiles"); 271 mHeadsetService.connectHeadset(mDevice); 272 } 273 if (mA2dpService != null && 274 mA2dpService.getSinkPriority(mDevice) == 275 BluetoothA2dp.PRIORITY_AUTO_CONNECT && 276 mA2dpService.getConnectedSinks().length == 0) { 277 Log.i(TAG, "A2dp:Auto Connect Profiles"); 278 mA2dpService.connectSink(mDevice); 279 } 280 } 281 break; 282 case CONNECT_OTHER_PROFILES: 283 if (isPhoneDocked(mDevice)) { 284 break; 285 } 286 if (message.arg1 == CONNECT_A2DP_OUTGOING) { 287 if (mA2dpService != null && 288 mA2dpService.getConnectedSinks().length == 0) { 289 Log.i(TAG, "A2dp:Connect Other Profiles"); 290 mA2dpService.connectSink(mDevice); 291 } 292 } else if (message.arg1 == CONNECT_HFP_OUTGOING) { 293 if (!mHeadsetServiceConnected) { 294 deferMessage(message); 295 } else { 296 if (!mHeadsetService.isConnected(mDevice)) { 297 Log.i(TAG, "Headset:Connect Other Profiles"); 298 mHeadsetService.connectHeadset(mDevice); 299 } 300 } 301 } 302 break; 303 case TRANSITION_TO_STABLE: 304 // ignore. 305 break; 306 default: 307 return NOT_HANDLED; 308 } 309 return HANDLED; 310 } 311 } 312 313 private class OutgoingHandsfree extends HierarchicalState { 314 private boolean mStatus = false; 315 private int mCommand; 316 317 @Override 318 protected void enter() { 319 Log.i(TAG, "Entering OutgoingHandsfree state with: " + getCurrentMessage().what); 320 mCommand = getCurrentMessage().what; 321 if (mCommand != CONNECT_HFP_OUTGOING && 322 mCommand != DISCONNECT_HFP_OUTGOING) { 323 Log.e(TAG, "Error: OutgoingHandsfree state with command:" + mCommand); 324 } 325 mStatus = processCommand(mCommand); 326 if (!mStatus) { 327 sendMessage(TRANSITION_TO_STABLE); 328 mService.sendProfileStateMessage(BluetoothProfileState.HFP, 329 BluetoothProfileState.TRANSITION_TO_STABLE); 330 } 331 } 332 333 @Override 334 protected boolean processMessage(Message message) { 335 log("OutgoingHandsfree State -> Processing Message: " + message.what); 336 Message deferMsg = new Message(); 337 int command = message.what; 338 switch(command) { 339 case CONNECT_HFP_OUTGOING: 340 if (command != mCommand) { 341 // Disconnect followed by a connect - defer 342 deferMessage(message); 343 } 344 break; 345 case CONNECT_HFP_INCOMING: 346 if (mCommand == CONNECT_HFP_OUTGOING) { 347 // Cancel outgoing connect, accept incoming 348 cancelCommand(CONNECT_HFP_OUTGOING); 349 transitionTo(mIncomingHandsfree); 350 } else { 351 // We have done the disconnect but we are not 352 // sure which state we are in at this point. 353 deferMessage(message); 354 } 355 break; 356 case CONNECT_A2DP_INCOMING: 357 // accept incoming A2DP, retry HFP_OUTGOING 358 transitionTo(mIncomingA2dp); 359 360 if (mStatus) { 361 deferMsg.what = mCommand; 362 deferMessage(deferMsg); 363 } 364 break; 365 case CONNECT_A2DP_OUTGOING: 366 deferMessage(message); 367 break; 368 case DISCONNECT_HFP_OUTGOING: 369 if (mCommand == CONNECT_HFP_OUTGOING) { 370 // Cancel outgoing connect 371 cancelCommand(CONNECT_HFP_OUTGOING); 372 processCommand(DISCONNECT_HFP_OUTGOING); 373 } 374 // else ignore 375 break; 376 case DISCONNECT_HFP_INCOMING: 377 // When this happens the socket would be closed and the headset 378 // state moved to DISCONNECTED, cancel the outgoing thread. 379 // if it still is in CONNECTING state 380 cancelCommand(CONNECT_HFP_OUTGOING); 381 break; 382 case DISCONNECT_A2DP_OUTGOING: 383 deferMessage(message); 384 break; 385 case DISCONNECT_A2DP_INCOMING: 386 // Bluez will handle the disconnect. If because of this the outgoing 387 // handsfree connection has failed, then retry. 388 if (mStatus) { 389 deferMsg.what = mCommand; 390 deferMessage(deferMsg); 391 } 392 break; 393 case DISCONNECT_PBAP_OUTGOING: 394 case UNPAIR: 395 case AUTO_CONNECT_PROFILES: 396 case CONNECT_OTHER_PROFILES: 397 deferMessage(message); 398 break; 399 case TRANSITION_TO_STABLE: 400 transitionTo(mBondedDevice); 401 break; 402 default: 403 return NOT_HANDLED; 404 } 405 return HANDLED; 406 } 407 } 408 409 private class IncomingHandsfree extends HierarchicalState { 410 private boolean mStatus = false; 411 private int mCommand; 412 413 @Override 414 protected void enter() { 415 Log.i(TAG, "Entering IncomingHandsfree state with: " + getCurrentMessage().what); 416 mCommand = getCurrentMessage().what; 417 if (mCommand != CONNECT_HFP_INCOMING && 418 mCommand != DISCONNECT_HFP_INCOMING) { 419 Log.e(TAG, "Error: IncomingHandsfree state with command:" + mCommand); 420 } 421 mStatus = processCommand(mCommand); 422 if (!mStatus) { 423 sendMessage(TRANSITION_TO_STABLE); 424 mService.sendProfileStateMessage(BluetoothProfileState.HFP, 425 BluetoothProfileState.TRANSITION_TO_STABLE); 426 } 427 } 428 429 @Override 430 protected boolean processMessage(Message message) { 431 log("IncomingHandsfree State -> Processing Message: " + message.what); 432 switch(message.what) { 433 case CONNECT_HFP_OUTGOING: 434 deferMessage(message); 435 break; 436 case CONNECT_HFP_INCOMING: 437 // Ignore 438 Log.e(TAG, "Error: Incoming connection with a pending incoming connection"); 439 break; 440 case CONNECT_A2DP_INCOMING: 441 // Serialize the commands. 442 deferMessage(message); 443 break; 444 case CONNECT_A2DP_OUTGOING: 445 deferMessage(message); 446 break; 447 case DISCONNECT_HFP_OUTGOING: 448 // We don't know at what state we are in the incoming HFP connection state. 449 // We can be changing from DISCONNECTED to CONNECTING, or 450 // from CONNECTING to CONNECTED, so serializing this command is 451 // the safest option. 452 deferMessage(message); 453 break; 454 case DISCONNECT_HFP_INCOMING: 455 // Nothing to do here, we will already be DISCONNECTED 456 // by this point. 457 break; 458 case DISCONNECT_A2DP_OUTGOING: 459 deferMessage(message); 460 break; 461 case DISCONNECT_A2DP_INCOMING: 462 // Bluez handles incoming A2DP disconnect. 463 // If this causes incoming HFP to fail, it is more of a headset problem 464 // since both connections are incoming ones. 465 break; 466 case DISCONNECT_PBAP_OUTGOING: 467 case UNPAIR: 468 case AUTO_CONNECT_PROFILES: 469 case CONNECT_OTHER_PROFILES: 470 deferMessage(message); 471 break; 472 case TRANSITION_TO_STABLE: 473 transitionTo(mBondedDevice); 474 break; 475 default: 476 return NOT_HANDLED; 477 } 478 return HANDLED; 479 } 480 } 481 482 private class OutgoingA2dp extends HierarchicalState { 483 private boolean mStatus = false; 484 private int mCommand; 485 486 @Override 487 protected void enter() { 488 Log.i(TAG, "Entering OutgoingA2dp state with: " + getCurrentMessage().what); 489 mCommand = getCurrentMessage().what; 490 if (mCommand != CONNECT_A2DP_OUTGOING && 491 mCommand != DISCONNECT_A2DP_OUTGOING) { 492 Log.e(TAG, "Error: OutgoingA2DP state with command:" + mCommand); 493 } 494 mStatus = processCommand(mCommand); 495 if (!mStatus) { 496 sendMessage(TRANSITION_TO_STABLE); 497 mService.sendProfileStateMessage(BluetoothProfileState.A2DP, 498 BluetoothProfileState.TRANSITION_TO_STABLE); 499 } 500 } 501 502 @Override 503 protected boolean processMessage(Message message) { 504 log("OutgoingA2dp State->Processing Message: " + message.what); 505 Message deferMsg = new Message(); 506 switch(message.what) { 507 case CONNECT_HFP_OUTGOING: 508 processCommand(CONNECT_HFP_OUTGOING); 509 510 // Don't cancel A2DP outgoing as there is no guarantee it 511 // will get canceled. 512 // It might already be connected but we might not have got the 513 // A2DP_SINK_STATE_CHANGE. Hence, no point disconnecting here. 514 // The worst case, the connection will fail, retry. 515 // The same applies to Disconnecting an A2DP connection. 516 if (mStatus) { 517 deferMsg.what = mCommand; 518 deferMessage(deferMsg); 519 } 520 break; 521 case CONNECT_HFP_INCOMING: 522 processCommand(CONNECT_HFP_INCOMING); 523 524 // Don't cancel A2DP outgoing as there is no guarantee 525 // it will get canceled. 526 // The worst case, the connection will fail, retry. 527 if (mStatus) { 528 deferMsg.what = mCommand; 529 deferMessage(deferMsg); 530 } 531 break; 532 case CONNECT_A2DP_INCOMING: 533 // Bluez will take care of conflicts between incoming and outgoing 534 // connections. 535 transitionTo(mIncomingA2dp); 536 break; 537 case CONNECT_A2DP_OUTGOING: 538 // Ignore 539 break; 540 case DISCONNECT_HFP_OUTGOING: 541 deferMessage(message); 542 break; 543 case DISCONNECT_HFP_INCOMING: 544 // At this point, we are already disconnected 545 // with HFP. Sometimes A2DP connection can 546 // fail due to the disconnection of HFP. So add a retry 547 // for the A2DP. 548 if (mStatus) { 549 deferMsg.what = mCommand; 550 deferMessage(deferMsg); 551 } 552 break; 553 case DISCONNECT_A2DP_OUTGOING: 554 deferMessage(message); 555 break; 556 case DISCONNECT_A2DP_INCOMING: 557 // Ignore, will be handled by Bluez 558 break; 559 case DISCONNECT_PBAP_OUTGOING: 560 case UNPAIR: 561 case AUTO_CONNECT_PROFILES: 562 case CONNECT_OTHER_PROFILES: 563 deferMessage(message); 564 break; 565 case TRANSITION_TO_STABLE: 566 transitionTo(mBondedDevice); 567 break; 568 default: 569 return NOT_HANDLED; 570 } 571 return HANDLED; 572 } 573 } 574 575 private class IncomingA2dp extends HierarchicalState { 576 private boolean mStatus = false; 577 private int mCommand; 578 579 @Override 580 protected void enter() { 581 Log.i(TAG, "Entering IncomingA2dp state with: " + getCurrentMessage().what); 582 mCommand = getCurrentMessage().what; 583 if (mCommand != CONNECT_A2DP_INCOMING && 584 mCommand != DISCONNECT_A2DP_INCOMING) { 585 Log.e(TAG, "Error: IncomingA2DP state with command:" + mCommand); 586 } 587 mStatus = processCommand(mCommand); 588 if (!mStatus) { 589 sendMessage(TRANSITION_TO_STABLE); 590 mService.sendProfileStateMessage(BluetoothProfileState.A2DP, 591 BluetoothProfileState.TRANSITION_TO_STABLE); 592 } 593 } 594 595 @Override 596 protected boolean processMessage(Message message) { 597 log("IncomingA2dp State->Processing Message: " + message.what); 598 Message deferMsg = new Message(); 599 switch(message.what) { 600 case CONNECT_HFP_OUTGOING: 601 deferMessage(message); 602 break; 603 case CONNECT_HFP_INCOMING: 604 // Shouldn't happen, but serialize the commands. 605 deferMessage(message); 606 break; 607 case CONNECT_A2DP_INCOMING: 608 // ignore 609 break; 610 case CONNECT_A2DP_OUTGOING: 611 // Defer message and retry 612 deferMessage(message); 613 break; 614 case DISCONNECT_HFP_OUTGOING: 615 deferMessage(message); 616 break; 617 case DISCONNECT_HFP_INCOMING: 618 // Shouldn't happen but if does, we can handle it. 619 // Depends if the headset can handle it. 620 // Incoming A2DP will be handled by Bluez, Disconnect HFP 621 // the socket would have already been closed. 622 // ignore 623 break; 624 case DISCONNECT_A2DP_OUTGOING: 625 deferMessage(message); 626 break; 627 case DISCONNECT_A2DP_INCOMING: 628 // Ignore, will be handled by Bluez 629 break; 630 case DISCONNECT_PBAP_OUTGOING: 631 case UNPAIR: 632 case AUTO_CONNECT_PROFILES: 633 case CONNECT_OTHER_PROFILES: 634 deferMessage(message); 635 break; 636 case TRANSITION_TO_STABLE: 637 transitionTo(mBondedDevice); 638 break; 639 default: 640 return NOT_HANDLED; 641 } 642 return HANDLED; 643 } 644 } 645 646 647 648 synchronized void cancelCommand(int command) { 649 if (command == CONNECT_HFP_OUTGOING ) { 650 // Cancel the outgoing thread. 651 if (mHeadsetServiceConnected) { 652 mHeadsetService.cancelConnectThread(); 653 } 654 // HeadsetService is down. Phone process most likely crashed. 655 // The thread would have got killed. 656 } 657 } 658 659 synchronized void deferProfileServiceMessage(int command) { 660 Message msg = new Message(); 661 msg.what = command; 662 deferMessage(msg); 663 } 664 665 synchronized boolean processCommand(int command) { 666 Log.i(TAG, "Processing command:" + command); 667 switch(command) { 668 case CONNECT_HFP_OUTGOING: 669 if (mHeadsetService != null) { 670 return mHeadsetService.connectHeadsetInternal(mDevice); 671 } 672 break; 673 case CONNECT_HFP_INCOMING: 674 if (!mHeadsetServiceConnected) { 675 deferProfileServiceMessage(command); 676 } else if (mHeadsetState == BluetoothHeadset.STATE_CONNECTING) { 677 return mHeadsetService.acceptIncomingConnect(mDevice); 678 } else if (mHeadsetState == BluetoothHeadset.STATE_DISCONNECTED) { 679 handleConnectionOfOtherProfiles(command); 680 return mHeadsetService.createIncomingConnect(mDevice); 681 } 682 break; 683 case CONNECT_A2DP_OUTGOING: 684 if (mA2dpService != null) { 685 return mA2dpService.connectSinkInternal(mDevice); 686 } 687 break; 688 case CONNECT_A2DP_INCOMING: 689 handleConnectionOfOtherProfiles(command); 690 // ignore, Bluez takes care 691 return true; 692 case DISCONNECT_HFP_OUTGOING: 693 if (!mHeadsetServiceConnected) { 694 deferProfileServiceMessage(command); 695 } else { 696 // Disconnect PBAP 697 // TODO(): Add PBAP to the state machine. 698 Message m = new Message(); 699 m.what = DISCONNECT_PBAP_OUTGOING; 700 deferMessage(m); 701 if (mHeadsetService.getPriority(mDevice) == 702 BluetoothHeadset.PRIORITY_AUTO_CONNECT) { 703 mHeadsetService.setPriority(mDevice, BluetoothHeadset.PRIORITY_ON); 704 } 705 return mHeadsetService.disconnectHeadsetInternal(mDevice); 706 } 707 break; 708 case DISCONNECT_HFP_INCOMING: 709 // ignore 710 return true; 711 case DISCONNECT_A2DP_INCOMING: 712 // ignore 713 return true; 714 case DISCONNECT_A2DP_OUTGOING: 715 if (mA2dpService != null) { 716 if (mA2dpService.getSinkPriority(mDevice) == 717 BluetoothA2dp.PRIORITY_AUTO_CONNECT) { 718 mA2dpService.setSinkPriority(mDevice, BluetoothHeadset.PRIORITY_ON); 719 } 720 return mA2dpService.disconnectSinkInternal(mDevice); 721 } 722 break; 723 case DISCONNECT_PBAP_OUTGOING: 724 if (!mPbapServiceConnected) { 725 deferProfileServiceMessage(command); 726 } else { 727 return mPbapService.disconnect(); 728 } 729 break; 730 case UNPAIR: 731 return mService.removeBondInternal(mDevice.getAddress()); 732 default: 733 Log.e(TAG, "Error: Unknown Command"); 734 } 735 return false; 736 } 737 738 private void handleConnectionOfOtherProfiles(int command) { 739 // The white paper recommendations mentions that when there is a 740 // link loss, it is the responsibility of the remote device to connect. 741 // Many connect only 1 profile - and they connect the second profile on 742 // some user action (like play being pressed) and so we need this code. 743 // Auto Connect code only connects to the last connected device - which 744 // is useful in cases like when the phone reboots. But consider the 745 // following case: 746 // User is connected to the car's phone and A2DP profile. 747 // User comes to the desk and places the phone in the dock 748 // (or any speaker or music system or even another headset) and thus 749 // gets connected to the A2DP profile. User goes back to the car. 750 // Ideally the car's system is supposed to send incoming connections 751 // from both Handsfree and A2DP profile. But they don't. The Auto 752 // connect code, will not work here because we only auto connect to the 753 // last connected device for that profile which in this case is the dock. 754 // Now suppose a user is using 2 headsets simultaneously, one for the 755 // phone profile one for the A2DP profile. If this is the use case, we 756 // expect the user to use the preference to turn off the A2DP profile in 757 // the Settings screen for the first headset. Else, after link loss, 758 // there can be an incoming connection from the first headset which 759 // might result in the connection of the A2DP profile (if the second 760 // headset is slower) and thus the A2DP profile on the second headset 761 // will never get connected. 762 // 763 // TODO(): Handle other profiles here. 764 switch (command) { 765 case CONNECT_HFP_INCOMING: 766 // Connect A2DP if there is no incoming connection 767 // If the priority is OFF - don't auto connect. 768 // If the priority is AUTO_CONNECT, auto connect code takes care. 769 if (mA2dpService.getSinkPriority(mDevice) == BluetoothA2dp.PRIORITY_ON) { 770 Message msg = new Message(); 771 msg.what = CONNECT_OTHER_PROFILES; 772 msg.arg1 = CONNECT_A2DP_OUTGOING; 773 sendMessageDelayed(msg, AUTO_CONNECT_DELAY); 774 } 775 break; 776 case CONNECT_A2DP_INCOMING: 777 // This is again against spec. HFP incoming connections should be made 778 // before A2DP, so we should not hit this case. But many devices 779 // don't follow this. 780 if (mHeadsetService.getPriority(mDevice) == BluetoothHeadset.PRIORITY_ON) { 781 Message msg = new Message(); 782 msg.what = CONNECT_OTHER_PROFILES; 783 msg.arg1 = CONNECT_HFP_OUTGOING; 784 sendMessageDelayed(msg, AUTO_CONNECT_DELAY); 785 } 786 break; 787 default: 788 break; 789 } 790 791 } 792 793 /*package*/ BluetoothDevice getDevice() { 794 return mDevice; 795 } 796 797 private void log(String message) { 798 if (DBG) { 799 Log.i(TAG, "Device:" + mDevice + " Message:" + message); 800 } 801 } 802} 803