1 /* 2 * Copyright 2018 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 /** 18 * Bluetooth HearingAid StateMachine. There is one instance per remote device. 19 * - "Disconnected" and "Connected" are steady states. 20 * - "Connecting" and "Disconnecting" are transient states until the 21 * connection / disconnection is completed. 22 * 23 * 24 * (Disconnected) 25 * | ^ 26 * CONNECT | | DISCONNECTED 27 * V | 28 * (Connecting)<--->(Disconnecting) 29 * | ^ 30 * CONNECTED | | DISCONNECT 31 * V | 32 * (Connected) 33 * NOTES: 34 * - If state machine is in "Connecting" state and the remote device sends 35 * DISCONNECT request, the state machine transitions to "Disconnecting" state. 36 * - Similarly, if the state machine is in "Disconnecting" state and the remote device 37 * sends CONNECT request, the state machine transitions to "Connecting" state. 38 * 39 * DISCONNECT 40 * (Connecting) ---------------> (Disconnecting) 41 * <--------------- 42 * CONNECT 43 * 44 */ 45 46 package com.android.bluetooth.hearingaid; 47 48 import static android.Manifest.permission.BLUETOOTH_CONNECT; 49 50 import android.bluetooth.BluetoothDevice; 51 import android.bluetooth.BluetoothHearingAid; 52 import android.bluetooth.BluetoothProfile; 53 import android.content.Intent; 54 import android.os.Looper; 55 import android.os.Message; 56 import android.util.Log; 57 58 import com.android.bluetooth.Utils; 59 import com.android.bluetooth.btservice.ProfileService; 60 import com.android.bluetooth.statemachine.State; 61 import com.android.bluetooth.statemachine.StateMachine; 62 import com.android.internal.annotations.VisibleForTesting; 63 64 import java.io.FileDescriptor; 65 import java.io.PrintWriter; 66 import java.io.StringWriter; 67 import java.util.Scanner; 68 69 final class HearingAidStateMachine extends StateMachine { 70 private static final boolean DBG = false; 71 private static final String TAG = "HearingAidStateMachine"; 72 73 static final int CONNECT = 1; 74 static final int DISCONNECT = 2; 75 @VisibleForTesting 76 static final int STACK_EVENT = 101; 77 private static final int CONNECT_TIMEOUT = 201; 78 79 // NOTE: the value is not "final" - it is modified in the unit tests 80 @VisibleForTesting 81 static int sConnectTimeoutMs = 30000; // 30s 82 83 private Disconnected mDisconnected; 84 private Connecting mConnecting; 85 private Disconnecting mDisconnecting; 86 private Connected mConnected; 87 private int mConnectionState = BluetoothProfile.STATE_DISCONNECTED; 88 private int mLastConnectionState = -1; 89 90 private HearingAidService mService; 91 private HearingAidNativeInterface mNativeInterface; 92 93 private final BluetoothDevice mDevice; 94 HearingAidStateMachine(BluetoothDevice device, HearingAidService svc, HearingAidNativeInterface nativeInterface, Looper looper)95 HearingAidStateMachine(BluetoothDevice device, HearingAidService svc, 96 HearingAidNativeInterface nativeInterface, Looper looper) { 97 super(TAG, looper); 98 mDevice = device; 99 mService = svc; 100 mNativeInterface = nativeInterface; 101 102 mDisconnected = new Disconnected(); 103 mConnecting = new Connecting(); 104 mDisconnecting = new Disconnecting(); 105 mConnected = new Connected(); 106 107 addState(mDisconnected); 108 addState(mConnecting); 109 addState(mDisconnecting); 110 addState(mConnected); 111 112 setInitialState(mDisconnected); 113 } 114 make(BluetoothDevice device, HearingAidService svc, HearingAidNativeInterface nativeInterface, Looper looper)115 static HearingAidStateMachine make(BluetoothDevice device, HearingAidService svc, 116 HearingAidNativeInterface nativeInterface, Looper looper) { 117 Log.i(TAG, "make for device " + device); 118 HearingAidStateMachine HearingAidSm = new HearingAidStateMachine(device, svc, 119 nativeInterface, looper); 120 HearingAidSm.start(); 121 return HearingAidSm; 122 } 123 doQuit()124 public void doQuit() { 125 log("doQuit for device " + mDevice); 126 quitNow(); 127 } 128 cleanup()129 public void cleanup() { 130 log("cleanup for device " + mDevice); 131 } 132 133 @VisibleForTesting 134 class Disconnected extends State { 135 @Override enter()136 public void enter() { 137 Log.i(TAG, "Enter Disconnected(" + mDevice + "): " + messageWhatToString( 138 getCurrentMessage().what)); 139 mConnectionState = BluetoothProfile.STATE_DISCONNECTED; 140 141 removeDeferredMessages(DISCONNECT); 142 143 if (mLastConnectionState != -1) { 144 // Don't broadcast during startup 145 broadcastConnectionState(BluetoothProfile.STATE_DISCONNECTED, 146 mLastConnectionState); 147 } 148 } 149 150 @Override exit()151 public void exit() { 152 log("Exit Disconnected(" + mDevice + "): " + messageWhatToString( 153 getCurrentMessage().what)); 154 mLastConnectionState = BluetoothProfile.STATE_DISCONNECTED; 155 } 156 157 @Override processMessage(Message message)158 public boolean processMessage(Message message) { 159 log("Disconnected process message(" + mDevice + "): " + messageWhatToString( 160 message.what)); 161 162 switch (message.what) { 163 case CONNECT: 164 log("Connecting to " + mDevice); 165 if (!mNativeInterface.connectHearingAid(mDevice)) { 166 Log.e(TAG, "Disconnected: error connecting to " + mDevice); 167 break; 168 } 169 if (mService.okToConnect(mDevice)) { 170 transitionTo(mConnecting); 171 } else { 172 // Reject the request and stay in Disconnected state 173 Log.w(TAG, "Outgoing HearingAid Connecting request rejected: " + mDevice); 174 } 175 break; 176 case DISCONNECT: 177 Log.d(TAG, "Disconnected: DISCONNECT: call native disconnect for " + mDevice); 178 mNativeInterface.disconnectHearingAid(mDevice); 179 break; 180 case STACK_EVENT: 181 HearingAidStackEvent event = (HearingAidStackEvent) message.obj; 182 if (DBG) { 183 Log.d(TAG, "Disconnected: stack event: " + event); 184 } 185 if (!mDevice.equals(event.device)) { 186 Log.wtf(TAG, "Device(" + mDevice + "): event mismatch: " + event); 187 } 188 switch (event.type) { 189 case HearingAidStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED: 190 processConnectionEvent(event.valueInt1); 191 break; 192 default: 193 Log.e(TAG, "Disconnected: ignoring stack event: " + event); 194 break; 195 } 196 break; 197 default: 198 return NOT_HANDLED; 199 } 200 return HANDLED; 201 } 202 203 // in Disconnected state processConnectionEvent(int state)204 private void processConnectionEvent(int state) { 205 switch (state) { 206 case HearingAidStackEvent.CONNECTION_STATE_DISCONNECTED: 207 Log.w(TAG, "Ignore HearingAid DISCONNECTED event: " + mDevice); 208 break; 209 case HearingAidStackEvent.CONNECTION_STATE_CONNECTING: 210 if (mService.okToConnect(mDevice)) { 211 Log.i(TAG, "Incoming HearingAid Connecting request accepted: " + mDevice); 212 transitionTo(mConnecting); 213 } else { 214 // Reject the connection and stay in Disconnected state itself 215 Log.w(TAG, "Incoming HearingAid Connecting request rejected: " + mDevice); 216 mNativeInterface.disconnectHearingAid(mDevice); 217 } 218 break; 219 case HearingAidStackEvent.CONNECTION_STATE_CONNECTED: 220 Log.w(TAG, "HearingAid Connected from Disconnected state: " + mDevice); 221 if (mService.okToConnect(mDevice)) { 222 Log.i(TAG, "Incoming HearingAid Connected request accepted: " + mDevice); 223 transitionTo(mConnected); 224 } else { 225 // Reject the connection and stay in Disconnected state itself 226 Log.w(TAG, "Incoming HearingAid Connected request rejected: " + mDevice); 227 mNativeInterface.disconnectHearingAid(mDevice); 228 } 229 break; 230 case HearingAidStackEvent.CONNECTION_STATE_DISCONNECTING: 231 Log.w(TAG, "Ignore HearingAid DISCONNECTING event: " + mDevice); 232 break; 233 default: 234 Log.e(TAG, "Incorrect state: " + state + " device: " + mDevice); 235 break; 236 } 237 } 238 } 239 240 @VisibleForTesting 241 class Connecting extends State { 242 @Override enter()243 public void enter() { 244 Log.i(TAG, "Enter Connecting(" + mDevice + "): " 245 + messageWhatToString(getCurrentMessage().what)); 246 sendMessageDelayed(CONNECT_TIMEOUT, sConnectTimeoutMs); 247 mConnectionState = BluetoothProfile.STATE_CONNECTING; 248 broadcastConnectionState(BluetoothProfile.STATE_CONNECTING, mLastConnectionState); 249 } 250 251 @Override exit()252 public void exit() { 253 log("Exit Connecting(" + mDevice + "): " 254 + messageWhatToString(getCurrentMessage().what)); 255 mLastConnectionState = BluetoothProfile.STATE_CONNECTING; 256 removeMessages(CONNECT_TIMEOUT); 257 } 258 259 @Override processMessage(Message message)260 public boolean processMessage(Message message) { 261 log("Connecting process message(" + mDevice + "): " 262 + messageWhatToString(message.what)); 263 264 switch (message.what) { 265 case CONNECT: 266 deferMessage(message); 267 break; 268 case CONNECT_TIMEOUT: 269 Log.w(TAG, "Connecting connection timeout: " + mDevice); 270 mNativeInterface.disconnectHearingAid(mDevice); 271 if (mService.isConnectedPeerDevices(mDevice)) { 272 Log.w(TAG, "One side connection timeout: " + mDevice + ". Try acceptlist"); 273 mNativeInterface.addToAcceptlist(mDevice); 274 } 275 HearingAidStackEvent disconnectEvent = 276 new HearingAidStackEvent( 277 HearingAidStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED); 278 disconnectEvent.device = mDevice; 279 disconnectEvent.valueInt1 = HearingAidStackEvent.CONNECTION_STATE_DISCONNECTED; 280 sendMessage(STACK_EVENT, disconnectEvent); 281 break; 282 case DISCONNECT: 283 log("Connecting: connection canceled to " + mDevice); 284 mNativeInterface.disconnectHearingAid(mDevice); 285 transitionTo(mDisconnected); 286 break; 287 case STACK_EVENT: 288 HearingAidStackEvent event = (HearingAidStackEvent) message.obj; 289 log("Connecting: stack event: " + event); 290 if (!mDevice.equals(event.device)) { 291 Log.wtf(TAG, "Device(" + mDevice + "): event mismatch: " + event); 292 } 293 switch (event.type) { 294 case HearingAidStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED: 295 processConnectionEvent(event.valueInt1); 296 break; 297 default: 298 Log.e(TAG, "Connecting: ignoring stack event: " + event); 299 break; 300 } 301 break; 302 default: 303 return NOT_HANDLED; 304 } 305 return HANDLED; 306 } 307 308 // in Connecting state processConnectionEvent(int state)309 private void processConnectionEvent(int state) { 310 switch (state) { 311 case HearingAidStackEvent.CONNECTION_STATE_DISCONNECTED: 312 Log.w(TAG, "Connecting device disconnected: " + mDevice); 313 transitionTo(mDisconnected); 314 break; 315 case HearingAidStackEvent.CONNECTION_STATE_CONNECTED: 316 transitionTo(mConnected); 317 break; 318 case HearingAidStackEvent.CONNECTION_STATE_CONNECTING: 319 break; 320 case HearingAidStackEvent.CONNECTION_STATE_DISCONNECTING: 321 Log.w(TAG, "Connecting interrupted: device is disconnecting: " + mDevice); 322 transitionTo(mDisconnecting); 323 break; 324 default: 325 Log.e(TAG, "Incorrect state: " + state); 326 break; 327 } 328 } 329 } 330 331 @VisibleForTesting 332 class Disconnecting extends State { 333 @Override enter()334 public void enter() { 335 Log.i(TAG, "Enter Disconnecting(" + mDevice + "): " 336 + messageWhatToString(getCurrentMessage().what)); 337 sendMessageDelayed(CONNECT_TIMEOUT, sConnectTimeoutMs); 338 mConnectionState = BluetoothProfile.STATE_DISCONNECTING; 339 broadcastConnectionState(BluetoothProfile.STATE_DISCONNECTING, mLastConnectionState); 340 } 341 342 @Override exit()343 public void exit() { 344 log("Exit Disconnecting(" + mDevice + "): " 345 + messageWhatToString(getCurrentMessage().what)); 346 mLastConnectionState = BluetoothProfile.STATE_DISCONNECTING; 347 removeMessages(CONNECT_TIMEOUT); 348 } 349 350 @Override processMessage(Message message)351 public boolean processMessage(Message message) { 352 log("Disconnecting process message(" + mDevice + "): " 353 + messageWhatToString(message.what)); 354 355 switch (message.what) { 356 case CONNECT: 357 deferMessage(message); 358 break; 359 case CONNECT_TIMEOUT: { 360 Log.w(TAG, "Disconnecting connection timeout: " + mDevice); 361 mNativeInterface.disconnectHearingAid(mDevice); 362 HearingAidStackEvent disconnectEvent = 363 new HearingAidStackEvent( 364 HearingAidStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED); 365 disconnectEvent.device = mDevice; 366 disconnectEvent.valueInt1 = HearingAidStackEvent.CONNECTION_STATE_DISCONNECTED; 367 sendMessage(STACK_EVENT, disconnectEvent); 368 break; 369 } 370 case DISCONNECT: 371 deferMessage(message); 372 break; 373 case STACK_EVENT: 374 HearingAidStackEvent event = (HearingAidStackEvent) message.obj; 375 log("Disconnecting: stack event: " + event); 376 if (!mDevice.equals(event.device)) { 377 Log.wtf(TAG, "Device(" + mDevice + "): event mismatch: " + event); 378 } 379 switch (event.type) { 380 case HearingAidStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED: 381 processConnectionEvent(event.valueInt1); 382 break; 383 default: 384 Log.e(TAG, "Disconnecting: ignoring stack event: " + event); 385 break; 386 } 387 break; 388 default: 389 return NOT_HANDLED; 390 } 391 return HANDLED; 392 } 393 394 // in Disconnecting state processConnectionEvent(int state)395 private void processConnectionEvent(int state) { 396 switch (state) { 397 case HearingAidStackEvent.CONNECTION_STATE_DISCONNECTED: 398 Log.i(TAG, "Disconnected: " + mDevice); 399 transitionTo(mDisconnected); 400 break; 401 case HearingAidStackEvent.CONNECTION_STATE_CONNECTED: 402 if (mService.okToConnect(mDevice)) { 403 Log.w(TAG, "Disconnecting interrupted: device is connected: " + mDevice); 404 transitionTo(mConnected); 405 } else { 406 // Reject the connection and stay in Disconnecting state 407 Log.w(TAG, "Incoming HearingAid Connected request rejected: " + mDevice); 408 mNativeInterface.disconnectHearingAid(mDevice); 409 } 410 break; 411 case HearingAidStackEvent.CONNECTION_STATE_CONNECTING: 412 if (mService.okToConnect(mDevice)) { 413 Log.i(TAG, "Disconnecting interrupted: try to reconnect: " + mDevice); 414 transitionTo(mConnecting); 415 } else { 416 // Reject the connection and stay in Disconnecting state 417 Log.w(TAG, "Incoming HearingAid Connecting request rejected: " + mDevice); 418 mNativeInterface.disconnectHearingAid(mDevice); 419 } 420 break; 421 case HearingAidStackEvent.CONNECTION_STATE_DISCONNECTING: 422 break; 423 default: 424 Log.e(TAG, "Incorrect state: " + state); 425 break; 426 } 427 } 428 } 429 430 @VisibleForTesting 431 class Connected extends State { 432 @Override enter()433 public void enter() { 434 Log.i(TAG, "Enter Connected(" + mDevice + "): " 435 + messageWhatToString(getCurrentMessage().what)); 436 mConnectionState = BluetoothProfile.STATE_CONNECTED; 437 removeDeferredMessages(CONNECT); 438 broadcastConnectionState(BluetoothProfile.STATE_CONNECTED, mLastConnectionState); 439 } 440 441 @Override exit()442 public void exit() { 443 log("Exit Connected(" + mDevice + "): " 444 + messageWhatToString(getCurrentMessage().what)); 445 mLastConnectionState = BluetoothProfile.STATE_CONNECTED; 446 } 447 448 @Override processMessage(Message message)449 public boolean processMessage(Message message) { 450 log("Connected process message(" + mDevice + "): " 451 + messageWhatToString(message.what)); 452 453 switch (message.what) { 454 case CONNECT: 455 Log.w(TAG, "Connected: CONNECT ignored: " + mDevice); 456 break; 457 case DISCONNECT: 458 log("Disconnecting from " + mDevice); 459 if (!mNativeInterface.disconnectHearingAid(mDevice)) { 460 // If error in the native stack, transition directly to Disconnected state. 461 Log.e(TAG, "Connected: error disconnecting from " + mDevice); 462 transitionTo(mDisconnected); 463 break; 464 } 465 transitionTo(mDisconnecting); 466 break; 467 case STACK_EVENT: 468 HearingAidStackEvent event = (HearingAidStackEvent) message.obj; 469 log("Connected: stack event: " + event); 470 if (!mDevice.equals(event.device)) { 471 Log.wtf(TAG, "Device(" + mDevice + "): event mismatch: " + event); 472 } 473 switch (event.type) { 474 case HearingAidStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED: 475 processConnectionEvent(event.valueInt1); 476 break; 477 default: 478 Log.e(TAG, "Connected: ignoring stack event: " + event); 479 break; 480 } 481 break; 482 default: 483 return NOT_HANDLED; 484 } 485 return HANDLED; 486 } 487 488 // in Connected state processConnectionEvent(int state)489 private void processConnectionEvent(int state) { 490 switch (state) { 491 case HearingAidStackEvent.CONNECTION_STATE_DISCONNECTED: 492 Log.i(TAG, "Disconnected from " + mDevice + " but still in Acceptlist"); 493 transitionTo(mDisconnected); 494 break; 495 case HearingAidStackEvent.CONNECTION_STATE_DISCONNECTING: 496 Log.i(TAG, "Disconnecting from " + mDevice); 497 transitionTo(mDisconnecting); 498 break; 499 default: 500 Log.e(TAG, "Connection State Device: " + mDevice + " bad state: " + state); 501 break; 502 } 503 } 504 } 505 getConnectionState()506 int getConnectionState() { 507 return mConnectionState; 508 } 509 getDevice()510 BluetoothDevice getDevice() { 511 return mDevice; 512 } 513 isConnected()514 synchronized boolean isConnected() { 515 return (getConnectionState() == BluetoothProfile.STATE_CONNECTED); 516 } 517 518 // This method does not check for error condition (newState == prevState) broadcastConnectionState(int newState, int prevState)519 private void broadcastConnectionState(int newState, int prevState) { 520 log("Connection state " + mDevice + ": " + profileStateToString(prevState) 521 + "->" + profileStateToString(newState)); 522 523 Intent intent = new Intent(BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED); 524 intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, prevState); 525 intent.putExtra(BluetoothProfile.EXTRA_STATE, newState); 526 intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice); 527 intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT 528 | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND); 529 mService.sendBroadcast(intent, BLUETOOTH_CONNECT, Utils.getTempAllowlistBroadcastOptions()); 530 } 531 messageWhatToString(int what)532 private static String messageWhatToString(int what) { 533 switch (what) { 534 case CONNECT: 535 return "CONNECT"; 536 case DISCONNECT: 537 return "DISCONNECT"; 538 case STACK_EVENT: 539 return "STACK_EVENT"; 540 case CONNECT_TIMEOUT: 541 return "CONNECT_TIMEOUT"; 542 default: 543 break; 544 } 545 return Integer.toString(what); 546 } 547 profileStateToString(int state)548 private static String profileStateToString(int state) { 549 switch (state) { 550 case BluetoothProfile.STATE_DISCONNECTED: 551 return "DISCONNECTED"; 552 case BluetoothProfile.STATE_CONNECTING: 553 return "CONNECTING"; 554 case BluetoothProfile.STATE_CONNECTED: 555 return "CONNECTED"; 556 case BluetoothProfile.STATE_DISCONNECTING: 557 return "DISCONNECTING"; 558 default: 559 break; 560 } 561 return Integer.toString(state); 562 } 563 dump(StringBuilder sb)564 public void dump(StringBuilder sb) { 565 ProfileService.println(sb, "mDevice: " + mDevice); 566 ProfileService.println(sb, " StateMachine: " + this); 567 // Dump the state machine logs 568 StringWriter stringWriter = new StringWriter(); 569 PrintWriter printWriter = new PrintWriter(stringWriter); 570 super.dump(new FileDescriptor(), printWriter, new String[]{}); 571 printWriter.flush(); 572 stringWriter.flush(); 573 ProfileService.println(sb, " StateMachineLog:"); 574 Scanner scanner = new Scanner(stringWriter.toString()); 575 while (scanner.hasNextLine()) { 576 String line = scanner.nextLine(); 577 ProfileService.println(sb, " " + line); 578 } 579 scanner.close(); 580 } 581 582 @Override log(String msg)583 protected void log(String msg) { 584 if (DBG) { 585 super.log(msg); 586 } 587 } 588 } 589