1 /* 2 * Copyright 2021 HIMSA II K/S - www.himsa.com. 3 * Represented by EHIMA - www.ehima.com 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18 // Bluetooth Hap Client 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 // (Disconnected) 24 // | ^ 25 // CONNECT | | DISCONNECTED 26 // V | 27 // (Connecting)<--->(Disconnecting) 28 // | ^ 29 // CONNECTED | | DISCONNECT 30 // V | 31 // (Connected) 32 // NOTES: 33 // - If state machine is in "Connecting" state and the remote device sends 34 // DISCONNECT request, the state machine transitions to "Disconnecting" state. 35 // - Similarly, if the state machine is in "Disconnecting" state and the remote device 36 // sends CONNECT request, the state machine transitions to "Connecting" state. 37 38 // DISCONNECT 39 // (Connecting) ---------------> (Disconnecting) 40 // <--------------- 41 // CONNECT 42 package com.android.bluetooth.hap; 43 44 import static android.Manifest.permission.BLUETOOTH_CONNECT; 45 import static android.Manifest.permission.BLUETOOTH_PRIVILEGED; 46 import static android.bluetooth.BluetoothProfile.STATE_CONNECTED; 47 import static android.bluetooth.BluetoothProfile.STATE_CONNECTING; 48 import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTED; 49 import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTING; 50 import static android.bluetooth.BluetoothProfile.getConnectionStateName; 51 52 import android.bluetooth.BluetoothDevice; 53 import android.bluetooth.BluetoothHapClient; 54 import android.bluetooth.BluetoothProfile; 55 import android.content.Intent; 56 import android.os.Looper; 57 import android.os.Message; 58 import android.util.Log; 59 60 import com.android.bluetooth.btservice.ProfileService; 61 import com.android.internal.annotations.VisibleForTesting; 62 import com.android.internal.util.State; 63 import com.android.internal.util.StateMachine; 64 65 import java.io.FileDescriptor; 66 import java.io.PrintWriter; 67 import java.io.StringWriter; 68 import java.time.Duration; 69 import java.util.Scanner; 70 71 final class HapClientStateMachine extends StateMachine { 72 private static final String TAG = HapClientStateMachine.class.getSimpleName(); 73 74 static final int MESSAGE_CONNECT = 1; 75 static final int MESSAGE_DISCONNECT = 2; 76 static final int MESSAGE_STACK_EVENT = 101; 77 @VisibleForTesting static final int MESSAGE_CONNECT_TIMEOUT = 201; 78 79 @VisibleForTesting static final Duration CONNECT_TIMEOUT = Duration.ofSeconds(30); 80 81 private final Disconnected mDisconnected; 82 private final Connecting mConnecting; 83 private final Disconnecting mDisconnecting; 84 private final Connected mConnected; 85 86 private int mConnectionState = STATE_DISCONNECTED; 87 private int mLastConnectionState = -1; 88 89 private final HapClientService mService; 90 private final HapClientNativeInterface mNativeInterface; 91 private final BluetoothDevice mDevice; 92 HapClientStateMachine( HapClientService svc, BluetoothDevice device, HapClientNativeInterface gattInterface, Looper looper)93 HapClientStateMachine( 94 HapClientService svc, 95 BluetoothDevice device, 96 HapClientNativeInterface gattInterface, 97 Looper looper) { 98 super(TAG, looper); 99 mDevice = device; 100 mService = svc; 101 mNativeInterface = gattInterface; 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 start(); 114 } 115 messageWhatToString(int what)116 private static String messageWhatToString(int what) { 117 return switch (what) { 118 case MESSAGE_CONNECT -> "CONNECT"; 119 case MESSAGE_DISCONNECT -> "DISCONNECT"; 120 case MESSAGE_STACK_EVENT -> "STACK_EVENT"; 121 case MESSAGE_CONNECT_TIMEOUT -> "CONNECT_TIMEOUT"; 122 default -> Integer.toString(what); 123 }; 124 } 125 doQuit()126 public void doQuit() { 127 Log.d(TAG, "doQuit for " + mDevice); 128 quitNow(); 129 } 130 getConnectionState()131 int getConnectionState() { 132 return mConnectionState; 133 } 134 getDevice()135 BluetoothDevice getDevice() { 136 return mDevice; 137 } 138 isConnected()139 synchronized boolean isConnected() { 140 return (getConnectionState() == STATE_CONNECTED); 141 } 142 broadcastConnectionState()143 private void broadcastConnectionState() { 144 Log.d( 145 TAG, 146 ("Connection state " + mDevice + ": ") 147 + getConnectionStateName(mLastConnectionState) 148 + "->" 149 + getConnectionStateName(mConnectionState)); 150 151 mService.connectionStateChanged(mDevice, mLastConnectionState, mConnectionState); 152 Intent intent = 153 new Intent(BluetoothHapClient.ACTION_HAP_CONNECTION_STATE_CHANGED) 154 .putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, mLastConnectionState) 155 .putExtra(BluetoothProfile.EXTRA_STATE, mConnectionState) 156 .putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice) 157 .addFlags( 158 Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT 159 | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND); 160 mService.getBaseContext() 161 .sendBroadcastWithMultiplePermissions( 162 intent, new String[] {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED}); 163 } 164 dump(StringBuilder sb)165 public void dump(StringBuilder sb) { 166 ProfileService.println(sb, "mDevice: " + mDevice); 167 ProfileService.println(sb, " StateMachine: " + this); 168 // Dump the state machine logs 169 StringWriter stringWriter = new StringWriter(); 170 PrintWriter printWriter = new PrintWriter(stringWriter); 171 super.dump(new FileDescriptor(), printWriter, new String[] {}); 172 printWriter.flush(); 173 stringWriter.flush(); 174 ProfileService.println(sb, " StateMachineLog:"); 175 Scanner scanner = new Scanner(stringWriter.toString()); 176 while (scanner.hasNextLine()) { 177 String line = scanner.nextLine(); 178 ProfileService.println(sb, " " + line); 179 } 180 scanner.close(); 181 } 182 183 @VisibleForTesting 184 class Disconnected extends State { 185 private final String mStateLog = "Disconnected(" + mDevice + "): "; 186 187 @Override enter()188 public void enter() { 189 Log.i(TAG, "Enter " + mStateLog + messageWhatToString(getCurrentMessage().what)); 190 191 removeDeferredMessages(MESSAGE_DISCONNECT); 192 193 mConnectionState = STATE_DISCONNECTED; 194 if (mLastConnectionState != -1) { // Don't broadcast during startup 195 broadcastConnectionState(); 196 } 197 } 198 199 @Override exit()200 public void exit() { 201 Log.d(TAG, "Exit " + mStateLog + messageWhatToString(getCurrentMessage().what)); 202 mLastConnectionState = STATE_DISCONNECTED; 203 } 204 205 @Override processMessage(Message message)206 public boolean processMessage(Message message) { 207 Log.d(TAG, mStateLog + "processMessage: " + messageWhatToString(message.what)); 208 209 switch (message.what) { 210 case MESSAGE_CONNECT -> { 211 if (!mNativeInterface.connectHapClient(mDevice)) { 212 Log.e(TAG, mStateLog + "native cannot connect"); 213 break; 214 } 215 if (mService.okToConnect(mDevice)) { 216 transitionTo(mConnecting); 217 } else { 218 Log.w(TAG, mStateLog + "outgoing connect request rejected"); 219 } 220 } 221 case MESSAGE_DISCONNECT -> { 222 mNativeInterface.disconnectHapClient(mDevice); 223 } 224 case MESSAGE_STACK_EVENT -> { 225 HapClientStackEvent event = (HapClientStackEvent) message.obj; 226 switch (event.type) { 227 case HapClientStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED -> { 228 processConnectionEvent(event.valueInt1); 229 } 230 default -> Log.e(TAG, mStateLog + "ignoring stack event: " + event); 231 } 232 } 233 default -> { 234 Log.e(TAG, mStateLog + "not handled: " + messageWhatToString(message.what)); 235 return NOT_HANDLED; 236 } 237 } 238 return HANDLED; 239 } 240 241 // in Disconnected state processConnectionEvent(int state)242 private void processConnectionEvent(int state) { 243 switch (state) { 244 case STATE_CONNECTING -> { 245 if (mService.okToConnect(mDevice)) { 246 Log.i(TAG, mStateLog + "Incoming connecting request accepted"); 247 transitionTo(mConnecting); 248 } else { 249 Log.w(TAG, mStateLog + "Incoming connecting request rejected"); 250 mNativeInterface.disconnectHapClient(mDevice); 251 } 252 } 253 case STATE_CONNECTED -> { 254 Log.w(TAG, "HearingAccess Connected from Disconnected state: " + mDevice); 255 if (mService.okToConnect(mDevice)) { 256 Log.w(TAG, mStateLog + "Incoming connected transition accepted"); 257 transitionTo(mConnected); 258 } else { 259 Log.w(TAG, mStateLog + "Incoming connected transition rejected"); 260 mNativeInterface.disconnectHapClient(mDevice); 261 } 262 } 263 default -> Log.e(TAG, mStateLog + "Incorrect state: " + state); 264 } 265 } 266 } 267 268 @VisibleForTesting 269 class Connecting extends State { 270 private final String mStateLog = "Connecting(" + mDevice + "): "; 271 272 @Override enter()273 public void enter() { 274 Log.i(TAG, "Enter " + mStateLog + messageWhatToString(getCurrentMessage().what)); 275 sendMessageDelayed(MESSAGE_CONNECT_TIMEOUT, CONNECT_TIMEOUT.toMillis()); 276 mConnectionState = STATE_CONNECTING; 277 broadcastConnectionState(); 278 } 279 280 @Override exit()281 public void exit() { 282 Log.d(TAG, "Exit " + mStateLog + messageWhatToString(getCurrentMessage().what)); 283 mLastConnectionState = STATE_CONNECTING; 284 removeMessages(MESSAGE_CONNECT_TIMEOUT); 285 } 286 287 @Override processMessage(Message message)288 public boolean processMessage(Message message) { 289 Log.d(TAG, mStateLog + "processMessage: " + messageWhatToString(message.what)); 290 291 switch (message.what) { 292 case MESSAGE_CONNECT -> deferMessage(message); 293 case MESSAGE_CONNECT_TIMEOUT -> { 294 Log.w(TAG, mStateLog + "connection timeout"); 295 mNativeInterface.disconnectHapClient(mDevice); 296 HapClientStackEvent disconnectEvent = 297 new HapClientStackEvent( 298 HapClientStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED); 299 disconnectEvent.device = mDevice; 300 disconnectEvent.valueInt1 = STATE_DISCONNECTED; 301 sendMessage(MESSAGE_STACK_EVENT, disconnectEvent); 302 } 303 case MESSAGE_DISCONNECT -> { 304 Log.d(TAG, mStateLog + "connection canceled"); 305 mNativeInterface.disconnectHapClient(mDevice); 306 transitionTo(mDisconnected); 307 } 308 case MESSAGE_STACK_EVENT -> { 309 HapClientStackEvent event = (HapClientStackEvent) message.obj; 310 switch (event.type) { 311 case HapClientStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED -> { 312 processConnectionEvent(event.valueInt1); 313 } 314 default -> Log.e(TAG, mStateLog + "ignoring stack event: " + event); 315 } 316 } 317 default -> { 318 Log.e(TAG, mStateLog + "not handled: " + messageWhatToString(message.what)); 319 return NOT_HANDLED; 320 } 321 } 322 return HANDLED; 323 } 324 325 // in Connecting state processConnectionEvent(int state)326 private void processConnectionEvent(int state) { 327 switch (state) { 328 case STATE_DISCONNECTED -> { 329 Log.i(TAG, mStateLog + "device disconnected"); 330 transitionTo(mDisconnected); 331 } 332 case STATE_CONNECTED -> transitionTo(mConnected); 333 case STATE_DISCONNECTING -> { 334 Log.i(TAG, mStateLog + "device disconnecting"); 335 transitionTo(mDisconnecting); 336 } 337 default -> Log.e(TAG, mStateLog + "Incorrect state: " + state); 338 } 339 } 340 } 341 342 @VisibleForTesting 343 class Disconnecting extends State { 344 private final String mStateLog = "Disconnecting(" + mDevice + "): "; 345 346 @Override enter()347 public void enter() { 348 Log.i(TAG, "Enter " + mStateLog + messageWhatToString(getCurrentMessage().what)); 349 sendMessageDelayed(MESSAGE_CONNECT_TIMEOUT, CONNECT_TIMEOUT.toMillis()); 350 mConnectionState = STATE_DISCONNECTING; 351 broadcastConnectionState(); 352 } 353 354 @Override exit()355 public void exit() { 356 Log.d(TAG, "Exit " + mStateLog + messageWhatToString(getCurrentMessage().what)); 357 mLastConnectionState = STATE_DISCONNECTING; 358 removeMessages(MESSAGE_CONNECT_TIMEOUT); 359 } 360 361 @Override processMessage(Message message)362 public boolean processMessage(Message message) { 363 Log.d(TAG, mStateLog + "processMessage: " + messageWhatToString(message.what)); 364 365 switch (message.what) { 366 case MESSAGE_CONNECT, MESSAGE_DISCONNECT -> deferMessage(message); 367 case MESSAGE_CONNECT_TIMEOUT -> { 368 Log.w(TAG, mStateLog + "connection timeout"); 369 mNativeInterface.disconnectHapClient(mDevice); 370 371 HapClientStackEvent disconnectEvent = 372 new HapClientStackEvent( 373 HapClientStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED); 374 disconnectEvent.device = mDevice; 375 disconnectEvent.valueInt1 = STATE_DISCONNECTED; 376 sendMessage(MESSAGE_STACK_EVENT, disconnectEvent); 377 } 378 case MESSAGE_STACK_EVENT -> { 379 HapClientStackEvent event = (HapClientStackEvent) message.obj; 380 switch (event.type) { 381 case HapClientStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED -> { 382 processConnectionEvent(event.valueInt1); 383 } 384 default -> Log.e(TAG, mStateLog + "ignoring stack event: " + event); 385 } 386 } 387 default -> { 388 Log.e(TAG, mStateLog + "not handled: " + messageWhatToString(message.what)); 389 return NOT_HANDLED; 390 } 391 } 392 return HANDLED; 393 } 394 395 // in Disconnecting state processConnectionEvent(int state)396 private void processConnectionEvent(int state) { 397 switch (state) { 398 case STATE_DISCONNECTED -> { 399 Log.i(TAG, mStateLog + "Disconnected"); 400 transitionTo(mDisconnected); 401 } 402 case STATE_CONNECTED -> { 403 if (mService.okToConnect(mDevice)) { 404 Log.w(TAG, mStateLog + "interrupted: device is connected"); 405 transitionTo(mConnected); 406 } else { 407 // Reject the connection and stay in Disconnecting state 408 Log.w(TAG, mStateLog + "Incoming connect request rejected"); 409 mNativeInterface.disconnectHapClient(mDevice); 410 } 411 } 412 case STATE_CONNECTING -> { 413 if (mService.okToConnect(mDevice)) { 414 Log.i(TAG, mStateLog + "interrupted: device try to reconnect"); 415 transitionTo(mConnecting); 416 } else { 417 // Reject the connection and stay in Disconnecting state 418 Log.w(TAG, mStateLog + "Incoming connecting request rejected"); 419 mNativeInterface.disconnectHapClient(mDevice); 420 } 421 } 422 default -> Log.e(TAG, mStateLog + "Incorrect state: " + state); 423 } 424 } 425 } 426 427 @VisibleForTesting 428 class Connected extends State { 429 private final String mStateLog = "Connected(" + mDevice + "): "; 430 431 @Override enter()432 public void enter() { 433 Log.i(TAG, "Enter " + mStateLog + messageWhatToString(getCurrentMessage().what)); 434 removeDeferredMessages(MESSAGE_CONNECT); 435 mConnectionState = STATE_CONNECTED; 436 broadcastConnectionState(); 437 } 438 439 @Override exit()440 public void exit() { 441 Log.d(TAG, "Exit " + mStateLog + messageWhatToString(getCurrentMessage().what)); 442 mLastConnectionState = STATE_CONNECTED; 443 } 444 445 @Override processMessage(Message message)446 public boolean processMessage(Message message) { 447 Log.d(TAG, mStateLog + "processMessage: " + messageWhatToString(message.what)); 448 449 switch (message.what) { 450 case MESSAGE_DISCONNECT -> { 451 if (!mNativeInterface.disconnectHapClient(mDevice)) { 452 // If error in the native stack, transition directly to Disconnected state. 453 Log.e(TAG, mStateLog + "native cannot disconnect"); 454 transitionTo(mDisconnected); 455 break; 456 } 457 transitionTo(mDisconnecting); 458 } 459 case MESSAGE_STACK_EVENT -> { 460 HapClientStackEvent event = (HapClientStackEvent) message.obj; 461 switch (event.type) { 462 case HapClientStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED -> 463 processConnectionEvent(event.valueInt1); 464 default -> Log.e(TAG, mStateLog + "ignoring stack event: " + event); 465 } 466 } 467 default -> { 468 Log.e(TAG, mStateLog + "not handled: " + messageWhatToString(message.what)); 469 return NOT_HANDLED; 470 } 471 } 472 return HANDLED; 473 } 474 475 // in Connected state processConnectionEvent(int state)476 private void processConnectionEvent(int state) { 477 switch (state) { 478 case STATE_DISCONNECTED -> { 479 Log.i(TAG, mStateLog + "Disconnected but still in allowlist"); 480 transitionTo(mDisconnected); 481 } 482 case STATE_DISCONNECTING -> { 483 Log.i(TAG, mStateLog + "Disconnecting"); 484 transitionTo(mDisconnecting); 485 } 486 default -> Log.e(TAG, mStateLog + "Incorrect state: " + state); 487 } 488 } 489 } 490 } 491