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