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