1 /* 2 * Copyright 2022 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 com.android.bluetooth.bas; 18 19 import static android.bluetooth.BluetoothDevice.PHY_LE_1M_MASK; 20 import static android.bluetooth.BluetoothDevice.PHY_LE_2M_MASK; 21 import static android.bluetooth.BluetoothDevice.TRANSPORT_LE; 22 23 import android.bluetooth.BluetoothDevice; 24 import android.bluetooth.BluetoothGatt; 25 import android.bluetooth.BluetoothGattCallback; 26 import android.bluetooth.BluetoothGattCharacteristic; 27 import android.bluetooth.BluetoothGattDescriptor; 28 import android.bluetooth.BluetoothGattService; 29 import android.bluetooth.BluetoothProfile; 30 import android.os.Looper; 31 import android.os.Message; 32 import android.util.Log; 33 34 import com.android.bluetooth.btservice.ProfileService; 35 import com.android.internal.annotations.VisibleForTesting; 36 import com.android.internal.util.State; 37 import com.android.internal.util.StateMachine; 38 39 import java.io.FileDescriptor; 40 import java.io.PrintWriter; 41 import java.io.StringWriter; 42 import java.lang.ref.WeakReference; 43 import java.util.Scanner; 44 import java.util.UUID; 45 46 /** 47 * It manages Battery service of a BLE device 48 */ 49 public class BatteryStateMachine extends StateMachine { 50 private static final boolean DBG = false; 51 private static final String TAG = "BatteryStateMachine"; 52 53 static final UUID GATT_BATTERY_SERVICE_UUID = 54 UUID.fromString("0000180f-0000-1000-8000-00805f9b34fb"); 55 static final UUID GATT_BATTERY_LEVEL_CHARACTERISTIC_UUID = 56 UUID.fromString("00002a19-0000-1000-8000-00805f9b34fb"); 57 static final UUID CLIENT_CHARACTERISTIC_CONFIG_DESCRIPTOR_UUID = 58 UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"); 59 60 static final int CONNECT = 1; 61 static final int DISCONNECT = 2; 62 static final int CONNECTION_STATE_CHANGED = 3; 63 private static final int CONNECT_TIMEOUT = 201; 64 65 // NOTE: the value is not "final" - it is modified in the unit tests 66 @VisibleForTesting 67 static int sConnectTimeoutMs = 30000; // 30s 68 69 private Disconnected mDisconnected; 70 private Connecting mConnecting; 71 private Connected mConnected; 72 private Disconnecting mDisconnecting; 73 private int mLastConnectionState = BluetoothProfile.STATE_DISCONNECTED; 74 75 WeakReference<BatteryService> mServiceRef; 76 77 BluetoothGatt mBluetoothGatt; 78 GattCallback mGattCallback; 79 final BluetoothDevice mDevice; 80 BatteryStateMachine(BluetoothDevice device, BatteryService service, Looper looper)81 BatteryStateMachine(BluetoothDevice device, BatteryService service, Looper looper) { 82 super(TAG, looper); 83 mDevice = device; 84 mServiceRef = new WeakReference<>(service); 85 86 mDisconnected = new Disconnected(); 87 mConnecting = new Connecting(); 88 mConnected = new Connected(); 89 mDisconnecting = new Disconnecting(); 90 91 addState(mDisconnected); 92 addState(mConnecting); 93 addState(mDisconnecting); 94 addState(mConnected); 95 96 setInitialState(mDisconnected); 97 } 98 make(BluetoothDevice device, BatteryService service, Looper looper)99 static BatteryStateMachine make(BluetoothDevice device, BatteryService service, Looper looper) { 100 Log.i(TAG, "make for device " + device); 101 BatteryStateMachine sm = new BatteryStateMachine(device, service, looper); 102 sm.start(); 103 return sm; 104 } 105 106 /** 107 * Quits the state machine 108 */ doQuit()109 public void doQuit() { 110 log("doQuit for device " + mDevice); 111 quitNow(); 112 } 113 114 /** 115 * Cleans up the resources the state machine held. 116 */ cleanup()117 public void cleanup() { 118 log("cleanup for device " + mDevice); 119 if (mBluetoothGatt != null) { 120 mBluetoothGatt.close(); 121 mBluetoothGatt = null; 122 mGattCallback = null; 123 } 124 } 125 getDevice()126 BluetoothDevice getDevice() { 127 return mDevice; 128 } 129 isConnected()130 synchronized boolean isConnected() { 131 return getCurrentState() == mConnected; 132 } 133 messageWhatToString(int what)134 private static String messageWhatToString(int what) { 135 switch (what) { 136 case CONNECT: 137 return "CONNECT"; 138 case DISCONNECT: 139 return "DISCONNECT"; 140 case CONNECTION_STATE_CHANGED: 141 return "CONNECTION_STATE_CHANGED"; 142 case CONNECT_TIMEOUT: 143 return "CONNECT_TIMEOUT"; 144 default: 145 break; 146 } 147 return Integer.toString(what); 148 } 149 profileStateToString(int state)150 private static String profileStateToString(int state) { 151 switch (state) { 152 case BluetoothProfile.STATE_DISCONNECTED: 153 return "DISCONNECTED"; 154 case BluetoothProfile.STATE_CONNECTING: 155 return "CONNECTING"; 156 case BluetoothProfile.STATE_CONNECTED: 157 return "CONNECTED"; 158 case BluetoothProfile.STATE_DISCONNECTING: 159 return "DISCONNECTING"; 160 default: 161 break; 162 } 163 return Integer.toString(state); 164 } 165 166 /** 167 * Dumps battery state machine state. 168 */ dump(StringBuilder sb)169 public void dump(StringBuilder sb) { 170 ProfileService.println(sb, "mDevice: " + mDevice); 171 ProfileService.println(sb, " StateMachine: " + this); 172 ProfileService.println(sb, " BluetoothGatt: " + mBluetoothGatt); 173 // Dump the state machine logs 174 StringWriter stringWriter = new StringWriter(); 175 PrintWriter printWriter = new PrintWriter(stringWriter); 176 super.dump(new FileDescriptor(), printWriter, new String[]{}); 177 printWriter.flush(); 178 stringWriter.flush(); 179 ProfileService.println(sb, " StateMachineLog:"); 180 Scanner scanner = new Scanner(stringWriter.toString()); 181 while (scanner.hasNextLine()) { 182 String line = scanner.nextLine(); 183 ProfileService.println(sb, " " + line); 184 } 185 scanner.close(); 186 } 187 188 @BluetoothProfile.BtProfileState getConnectionState()189 int getConnectionState() { 190 String currentState = getCurrentState().getName(); 191 switch (currentState) { 192 case "Disconnected": 193 return BluetoothProfile.STATE_DISCONNECTED; 194 case "Connecting": 195 return BluetoothProfile.STATE_CONNECTING; 196 case "Connected": 197 return BluetoothProfile.STATE_CONNECTED; 198 case "Disconnecting": 199 return BluetoothProfile.STATE_DISCONNECTING; 200 default: 201 Log.e(TAG, "Bad currentState: " + currentState); 202 return BluetoothProfile.STATE_DISCONNECTED; 203 } 204 } 205 dispatchConnectionStateChanged(int fromState, int toState)206 void dispatchConnectionStateChanged(int fromState, int toState) { 207 log("Connection state " + mDevice + ": " + profileStateToString(fromState) 208 + "->" + profileStateToString(toState)); 209 210 BatteryService service = mServiceRef.get(); 211 if (service != null) { 212 service.handleConnectionStateChanged(this, fromState, toState); 213 } 214 } 215 216 /** 217 * Connects to the GATT server of the device. 218 * 219 * @return {@code true} if it successfully connects to the GATT server. 220 */ 221 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) connectGatt()222 public boolean connectGatt() { 223 BatteryService service = mServiceRef.get(); 224 if (service == null) { 225 return false; 226 } 227 228 if (mGattCallback == null) { 229 mGattCallback = new GattCallback(); 230 } 231 if (mBluetoothGatt != null) { 232 Log.w(TAG, "Trying connectGatt with existing BluetoothGatt instance."); 233 mBluetoothGatt.close(); 234 } 235 mBluetoothGatt = mDevice.connectGatt(service, /*autoConnect=*/false, 236 mGattCallback, TRANSPORT_LE, /*opportunistic=*/true, 237 PHY_LE_1M_MASK | PHY_LE_2M_MASK, getHandler()); 238 return mBluetoothGatt != null; 239 } 240 241 @Override log(String msg)242 protected void log(String msg) { 243 if (DBG) { 244 super.log(msg); 245 } 246 } 247 log(String tag, String msg)248 static void log(String tag, String msg) { 249 if (DBG) { 250 Log.d(tag, msg); 251 } 252 } 253 254 @VisibleForTesting 255 class Disconnected extends State { 256 private static final String TAG = "BASM_Disconnected"; 257 258 @Override enter()259 public void enter() { 260 log(TAG, "Enter (" + mDevice + "): " + messageWhatToString( 261 getCurrentMessage().what)); 262 263 if (mBluetoothGatt != null) { 264 mBluetoothGatt.close(); 265 mBluetoothGatt = null; 266 } 267 268 if (mLastConnectionState != BluetoothProfile.STATE_DISCONNECTED) { 269 // Don't broadcast during startup 270 dispatchConnectionStateChanged(mLastConnectionState, 271 BluetoothProfile.STATE_DISCONNECTED); 272 } 273 } 274 275 @Override exit()276 public void exit() { 277 log(TAG, "Exit (" + mDevice + "): " + messageWhatToString( 278 getCurrentMessage().what)); 279 mLastConnectionState = BluetoothProfile.STATE_DISCONNECTED; 280 } 281 282 @Override processMessage(Message message)283 public boolean processMessage(Message message) { 284 log(TAG, "Process message(" + mDevice + "): " + messageWhatToString( 285 message.what)); 286 287 BatteryService service = mServiceRef.get(); 288 switch (message.what) { 289 case CONNECT: 290 log(TAG, "Connecting to " + mDevice); 291 if (service != null && service.canConnect(mDevice)) { 292 if (connectGatt()) { 293 transitionTo(mConnecting); 294 } else { 295 Log.w(TAG, "Battery connecting request rejected due to " 296 + "GATT connection rejection: " + mDevice); 297 } 298 } else { 299 // Reject the request and stay in Disconnected state 300 Log.w(TAG, "Battery connecting request rejected: " 301 + mDevice); 302 } 303 break; 304 case DISCONNECT: 305 Log.w(TAG, "DISCONNECT ignored: " + mDevice); 306 break; 307 case CONNECTION_STATE_CHANGED: 308 processConnectionEvent(message.arg1); 309 break; 310 default: 311 return NOT_HANDLED; 312 } 313 return HANDLED; 314 } 315 316 // in Disconnected state processConnectionEvent(int state)317 private void processConnectionEvent(int state) { 318 switch (state) { 319 case BluetoothGatt.STATE_DISCONNECTED: 320 Log.w(TAG, "Ignore Battery DISCONNECTED event: " + mDevice); 321 break; 322 default: 323 Log.e(TAG, "Incorrect state: " + state + " device: " + mDevice); 324 break; 325 } 326 } 327 } 328 329 @VisibleForTesting 330 class Connecting extends State { 331 private static final String TAG = "BASM_Connecting"; 332 @Override enter()333 public void enter() { 334 log(TAG, "Enter (" + mDevice + "): " 335 + messageWhatToString(getCurrentMessage().what)); 336 sendMessageDelayed(CONNECT_TIMEOUT, sConnectTimeoutMs); 337 dispatchConnectionStateChanged(mLastConnectionState, BluetoothProfile.STATE_CONNECTING); 338 } 339 340 @Override exit()341 public void exit() { 342 log(TAG, "Exit (" + mDevice + "): " 343 + messageWhatToString(getCurrentMessage().what)); 344 mLastConnectionState = BluetoothProfile.STATE_CONNECTING; 345 removeMessages(CONNECT_TIMEOUT); 346 } 347 348 @Override processMessage(Message message)349 public boolean processMessage(Message message) { 350 log(TAG, "process message(" + mDevice + "): " 351 + messageWhatToString(message.what)); 352 353 switch (message.what) { 354 case CONNECT: 355 Log.w(TAG, "CONNECT ignored: " + mDevice); 356 break; 357 case CONNECT_TIMEOUT: 358 Log.w(TAG, "Connection timeout: " + mDevice); 359 // fall through 360 case DISCONNECT: 361 log(TAG, "Connection canceled to " + mDevice); 362 if (mBluetoothGatt != null) { 363 mBluetoothGatt.disconnect(); 364 transitionTo(mDisconnecting); 365 } else { 366 transitionTo(mDisconnected); 367 } 368 break; 369 case CONNECTION_STATE_CHANGED: 370 processConnectionEvent(message.arg1); 371 break; 372 default: 373 return NOT_HANDLED; 374 } 375 return HANDLED; 376 } 377 378 // in Connecting state processConnectionEvent(int state)379 private void processConnectionEvent(int state) { 380 switch (state) { 381 case BluetoothGatt.STATE_DISCONNECTED: 382 Log.w(TAG, "Device disconnected: " + mDevice); 383 transitionTo(mDisconnected); 384 break; 385 case BluetoothGatt.STATE_CONNECTED: 386 transitionTo(mConnected); 387 break; 388 default: 389 Log.e(TAG, "Incorrect state: " + state); 390 break; 391 } 392 } 393 } 394 395 @VisibleForTesting 396 class Disconnecting extends State { 397 private static final String TAG = "BASM_Disconnecting"; 398 @Override enter()399 public void enter() { 400 log(TAG, "Enter (" + mDevice + "): " 401 + messageWhatToString(getCurrentMessage().what)); 402 sendMessageDelayed(CONNECT_TIMEOUT, sConnectTimeoutMs); 403 dispatchConnectionStateChanged(mLastConnectionState, 404 BluetoothProfile.STATE_DISCONNECTING); 405 } 406 407 @Override exit()408 public void exit() { 409 log(TAG, "Exit (" + mDevice + "): " 410 + messageWhatToString(getCurrentMessage().what)); 411 mLastConnectionState = BluetoothProfile.STATE_DISCONNECTING; 412 removeMessages(CONNECT_TIMEOUT); 413 } 414 415 @Override processMessage(Message message)416 public boolean processMessage(Message message) { 417 log(TAG, "Process message(" + mDevice + "): " 418 + messageWhatToString(message.what)); 419 420 switch (message.what) { 421 //TODO: Check if connect while disconnecting is okay. 422 // It is related to CONNECT_TIMEOUT as well. 423 case CONNECT: 424 Log.w(TAG, "CONNECT ignored: " + mDevice); 425 break; 426 case DISCONNECT: 427 Log.w(TAG, "DISCONNECT ignored: " + mDevice); 428 break; 429 case CONNECT_TIMEOUT: 430 Log.w(TAG, "Connection timeout: " + mDevice); 431 transitionTo(mDisconnected); 432 break; 433 case CONNECTION_STATE_CHANGED: 434 processConnectionEvent(message.arg1); 435 break; 436 default: 437 return NOT_HANDLED; 438 } 439 return HANDLED; 440 } 441 442 // in Disconnecting state processConnectionEvent(int state)443 private void processConnectionEvent(int state) { 444 switch (state) { 445 case BluetoothGatt.STATE_DISCONNECTED: 446 Log.i(TAG, "Disconnected: " + mDevice); 447 transitionTo(mDisconnected); 448 break; 449 case BluetoothGatt.STATE_CONNECTED: { 450 // Reject the connection and stay in Disconnecting state 451 Log.w(TAG, "Incoming Battery connected request rejected: " 452 + mDevice); 453 if (mBluetoothGatt != null) { 454 mBluetoothGatt.disconnect(); 455 } else { 456 transitionTo(mDisconnected); 457 } 458 break; 459 } 460 default: 461 Log.e(TAG, "Incorrect state: " + state); 462 break; 463 } 464 } 465 } 466 467 @VisibleForTesting 468 class Connected extends State { 469 private static final String TAG = "BASM_Connected"; 470 @Override enter()471 public void enter() { 472 log(TAG, "Enter (" + mDevice + "): " 473 + messageWhatToString(getCurrentMessage().what)); 474 dispatchConnectionStateChanged(mLastConnectionState, BluetoothProfile.STATE_CONNECTED); 475 476 if (mBluetoothGatt != null) { 477 mBluetoothGatt.discoverServices(); 478 } 479 } 480 481 @Override exit()482 public void exit() { 483 log(TAG, "Exit (" + mDevice + "): " 484 + messageWhatToString(getCurrentMessage().what)); 485 mLastConnectionState = BluetoothProfile.STATE_CONNECTED; 486 } 487 488 @Override processMessage(Message message)489 public boolean processMessage(Message message) { 490 log(TAG, "Process message(" + mDevice + "): " 491 + messageWhatToString(message.what)); 492 493 switch (message.what) { 494 case CONNECT: 495 Log.w(TAG, "CONNECT ignored: " + mDevice); 496 break; 497 case DISCONNECT: 498 log(TAG, "Disconnecting from " + mDevice); 499 if (mBluetoothGatt != null) { 500 mBluetoothGatt.disconnect(); 501 transitionTo(mDisconnecting); 502 } else { 503 transitionTo(mDisconnected); 504 } 505 break; 506 case CONNECTION_STATE_CHANGED: 507 processConnectionEvent(message.arg1); 508 break; 509 default: 510 return NOT_HANDLED; 511 } 512 return HANDLED; 513 } 514 515 // in Connected state processConnectionEvent(int state)516 private void processConnectionEvent(int state) { 517 switch (state) { 518 case BluetoothGatt.STATE_DISCONNECTED: 519 Log.i(TAG, "Disconnected from " + mDevice); 520 transitionTo(mDisconnected); 521 break; 522 case BluetoothGatt.STATE_CONNECTED: 523 Log.w(TAG, "Ignore CONNECTED event: " + mDevice); 524 break; 525 default: 526 Log.e(TAG, "Connection State Device: " + mDevice + " bad state: " + state); 527 break; 528 } 529 } 530 } 531 532 final class GattCallback extends BluetoothGattCallback { 533 @Override onConnectionStateChange(BluetoothGatt gatt, int status, int newState)534 public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { 535 sendMessage(CONNECTION_STATE_CHANGED, newState); 536 } 537 538 @Override onServicesDiscovered(BluetoothGatt gatt, int status)539 public void onServicesDiscovered(BluetoothGatt gatt, int status) { 540 if (status != BluetoothGatt.GATT_SUCCESS) { 541 Log.e(TAG, "No gatt service"); 542 return; 543 } 544 545 final BluetoothGattService batteryService = gatt.getService(GATT_BATTERY_SERVICE_UUID); 546 if (batteryService == null) { 547 Log.e(TAG, "No battery service"); 548 return; 549 } 550 551 final BluetoothGattCharacteristic batteryLevel = 552 batteryService.getCharacteristic(GATT_BATTERY_LEVEL_CHARACTERISTIC_UUID); 553 if (batteryLevel == null) { 554 Log.e(TAG, "No battery level characteristic"); 555 return; 556 } 557 558 // This may not trigger onCharacteristicRead if CCCD is already set but then 559 // onCharacteristicChanged will be triggered soon. 560 gatt.readCharacteristic(batteryLevel); 561 } 562 563 @Override onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, byte[] value)564 public void onCharacteristicChanged(BluetoothGatt gatt, 565 BluetoothGattCharacteristic characteristic, byte[] value) { 566 if (GATT_BATTERY_LEVEL_CHARACTERISTIC_UUID.equals(characteristic.getUuid())) { 567 updateBatteryLevel(value); 568 } 569 } 570 571 @Override onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, byte[] value, int status)572 public void onCharacteristicRead(BluetoothGatt gatt, 573 BluetoothGattCharacteristic characteristic, byte[] value, int status) { 574 if (status != BluetoothGatt.GATT_SUCCESS) { 575 Log.e(TAG, "Read characteristic failure on " + gatt + " " + characteristic); 576 return; 577 } 578 579 if (GATT_BATTERY_LEVEL_CHARACTERISTIC_UUID.equals(characteristic.getUuid())) { 580 updateBatteryLevel(value); 581 BluetoothGattDescriptor cccd = 582 characteristic.getDescriptor(CLIENT_CHARACTERISTIC_CONFIG_DESCRIPTOR_UUID); 583 if (cccd != null) { 584 gatt.setCharacteristicNotification(characteristic, /*enable=*/true); 585 gatt.writeDescriptor(cccd, BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); 586 } else { 587 Log.w(TAG, "No CCCD for battery level characteristic, " 588 + "it won't be notified"); 589 } 590 } 591 } 592 593 @Override onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status)594 public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, 595 int status) { 596 if (status != BluetoothGatt.GATT_SUCCESS) { 597 Log.w(TAG, "Failed to write descriptor " + descriptor.getUuid()); 598 } 599 } 600 601 @VisibleForTesting updateBatteryLevel(byte[] value)602 void updateBatteryLevel(byte[] value) { 603 if (value.length <= 0) { 604 return; 605 } 606 int batteryLevel = value[0] & 0xFF; 607 608 BatteryService service = mServiceRef.get(); 609 if (service != null) { 610 service.handleBatteryChanged(mDevice, batteryLevel); 611 } 612 } 613 } 614 } 615