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