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