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.Manifest.permission.BLUETOOTH_CONNECT; 20 21 import static com.android.bluetooth.Utils.enforceBluetoothPrivilegedPermission; 22 23 import android.annotation.RequiresPermission; 24 import android.bluetooth.BluetoothDevice; 25 import android.bluetooth.BluetoothProfile; 26 import android.bluetooth.BluetoothUuid; 27 import android.bluetooth.IBluetoothBattery; 28 import android.content.AttributionSource; 29 import android.content.Context; 30 import android.os.Handler; 31 import android.os.HandlerThread; 32 import android.os.Looper; 33 import android.os.ParcelUuid; 34 import android.sysprop.BluetoothProperties; 35 import android.util.Log; 36 37 import com.android.bluetooth.Utils; 38 import com.android.bluetooth.btservice.AdapterService; 39 import com.android.bluetooth.btservice.ProfileService; 40 import com.android.bluetooth.btservice.storage.DatabaseManager; 41 import com.android.internal.annotations.VisibleForTesting; 42 43 import java.lang.ref.WeakReference; 44 import java.util.ArrayList; 45 import java.util.Collections; 46 import java.util.HashMap; 47 import java.util.List; 48 import java.util.Map; 49 import java.util.Objects; 50 51 /** A profile service that connects to the Battery service (BAS) of BLE devices */ 52 public class BatteryService extends ProfileService { 53 private static final String TAG = "BatteryService"; 54 55 // Timeout for state machine thread join, to prevent potential ANR. 56 private static final int SM_THREAD_JOIN_TIMEOUT_MS = 1_000; 57 58 private static final int MAX_BATTERY_STATE_MACHINES = 10; 59 private static BatteryService sBatteryService; 60 private AdapterService mAdapterService; 61 private DatabaseManager mDatabaseManager; 62 private HandlerThread mStateMachinesThread; 63 private Handler mHandler; 64 private final Map<BluetoothDevice, BatteryStateMachine> mStateMachines = new HashMap<>(); 65 BatteryService(Context ctx)66 public BatteryService(Context ctx) { 67 super(ctx); 68 } 69 isEnabled()70 public static boolean isEnabled() { 71 return BluetoothProperties.isProfileBasClientEnabled().orElse(false); 72 } 73 74 @Override initBinder()75 protected IProfileServiceBinder initBinder() { 76 return new BluetoothBatteryBinder(this); 77 } 78 79 @Override start()80 public void start() { 81 Log.d(TAG, "start()"); 82 if (sBatteryService != null) { 83 throw new IllegalStateException("start() called twice"); 84 } 85 86 mAdapterService = 87 Objects.requireNonNull( 88 AdapterService.getAdapterService(), 89 "AdapterService cannot be null when BatteryService starts"); 90 mDatabaseManager = 91 Objects.requireNonNull( 92 mAdapterService.getDatabase(), 93 "DatabaseManager cannot be null when BatteryService starts"); 94 95 mHandler = new Handler(Looper.getMainLooper()); 96 mStateMachines.clear(); 97 mStateMachinesThread = new HandlerThread("BatteryService.StateMachines"); 98 mStateMachinesThread.start(); 99 100 setBatteryService(this); 101 } 102 103 @Override stop()104 public void stop() { 105 Log.d(TAG, "stop()"); 106 if (sBatteryService == null) { 107 Log.w(TAG, "stop() called before start()"); 108 return; 109 } 110 111 setBatteryService(null); 112 113 // Destroy state machines and stop handler thread 114 synchronized (mStateMachines) { 115 for (BatteryStateMachine sm : mStateMachines.values()) { 116 sm.doQuit(); 117 sm.cleanup(); 118 } 119 mStateMachines.clear(); 120 } 121 122 if (mStateMachinesThread != null) { 123 try { 124 mStateMachinesThread.quitSafely(); 125 mStateMachinesThread.join(SM_THREAD_JOIN_TIMEOUT_MS); 126 mStateMachinesThread = null; 127 } catch (InterruptedException e) { 128 // Do not rethrow as we are shutting down anyway 129 } 130 } 131 132 // Unregister Handler and stop all queued messages. 133 if (mHandler != null) { 134 mHandler.removeCallbacksAndMessages(null); 135 mHandler = null; 136 } 137 138 mAdapterService = null; 139 } 140 141 @Override cleanup()142 public void cleanup() { 143 Log.d(TAG, "cleanup()"); 144 } 145 146 /** Gets the BatteryService instance */ getBatteryService()147 public static synchronized BatteryService getBatteryService() { 148 if (sBatteryService == null) { 149 Log.w(TAG, "getBatteryService(): service is NULL"); 150 return null; 151 } 152 153 if (!sBatteryService.isAvailable()) { 154 Log.w(TAG, "getBatteryService(): service is not available"); 155 return null; 156 } 157 return sBatteryService; 158 } 159 160 /** Sets the battery service instance. It should be called only for testing purpose. */ 161 @VisibleForTesting setBatteryService(BatteryService instance)162 public static synchronized void setBatteryService(BatteryService instance) { 163 Log.d(TAG, "setBatteryService(): set to: " + instance); 164 sBatteryService = instance; 165 } 166 167 /** Connects to the battery service of the given device. */ 168 @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) connect(BluetoothDevice device)169 public boolean connect(BluetoothDevice device) { 170 enforceCallingOrSelfPermission( 171 BLUETOOTH_PRIVILEGED, "Need BLUETOOTH_PRIVILEGED permission"); 172 Log.d(TAG, "connect(): " + device); 173 if (device == null) { 174 Log.w(TAG, "Ignore connecting to null device"); 175 return false; 176 } 177 178 if (getConnectionPolicy(device) == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) { 179 Log.w(TAG, "Cannot connect to " + device + " : policy forbidden"); 180 return false; 181 } 182 ParcelUuid[] featureUuids = mAdapterService.getRemoteUuids(device); 183 if (!Utils.arrayContains(featureUuids, BluetoothUuid.BATTERY)) { 184 Log.e(TAG, "Cannot connect to " + device + " : Remote does not have Battery UUID"); 185 return false; 186 } 187 188 synchronized (mStateMachines) { 189 BatteryStateMachine sm = getOrCreateStateMachine(device); 190 if (sm == null) { 191 Log.e(TAG, "Cannot connect to " + device + " : no state machine"); 192 return false; 193 } 194 sm.sendMessage(BatteryStateMachine.CONNECT); 195 } 196 197 return true; 198 } 199 200 /** 201 * Connects to the battery service of the given device if possible. If it's impossible, it 202 * doesn't try without logging errors. 203 */ connectIfPossible(BluetoothDevice device)204 public boolean connectIfPossible(BluetoothDevice device) { 205 if (device == null 206 || getConnectionPolicy(device) == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN 207 || !Utils.arrayContains( 208 mAdapterService.getRemoteUuids(device), BluetoothUuid.BATTERY)) { 209 return false; 210 } 211 return connect(device); 212 } 213 214 /** Disconnects from the battery service of the given device. */ 215 @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) disconnect(BluetoothDevice device)216 public boolean disconnect(BluetoothDevice device) { 217 enforceCallingOrSelfPermission( 218 BLUETOOTH_PRIVILEGED, "Need BLUETOOTH_PRIVILEGED permission"); 219 Log.d(TAG, "disconnect(): " + device); 220 if (device == null) { 221 Log.w(TAG, "Ignore disconnecting to null device"); 222 return false; 223 } 224 synchronized (mStateMachines) { 225 BatteryStateMachine sm = getOrCreateStateMachine(device); 226 if (sm != null) { 227 sm.sendMessage(BatteryStateMachine.DISCONNECT); 228 } 229 } 230 231 return true; 232 } 233 234 /** Gets devices that battery service is connected. */ 235 @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) getConnectedDevices()236 public List<BluetoothDevice> getConnectedDevices() { 237 enforceCallingOrSelfPermission( 238 BLUETOOTH_PRIVILEGED, "Need BLUETOOTH_PRIVILEGED permission"); 239 synchronized (mStateMachines) { 240 List<BluetoothDevice> devices = new ArrayList<>(); 241 for (BatteryStateMachine sm : mStateMachines.values()) { 242 if (sm.isConnected()) { 243 devices.add(sm.getDevice()); 244 } 245 } 246 return devices; 247 } 248 } 249 250 /** 251 * Check whether it can connect to a peer device. The check considers a number of factors during 252 * the evaluation. 253 * 254 * @param device the peer device to connect to 255 * @return true if connection is allowed, otherwise false 256 */ 257 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) canConnect(BluetoothDevice device)258 public boolean canConnect(BluetoothDevice device) { 259 // Check connectionPolicy and accept or reject the connection. 260 int connectionPolicy = getConnectionPolicy(device); 261 int bondState = mAdapterService.getBondState(device); 262 // Allow this connection only if the device is bonded. Any attempt to connect while 263 // bonding would potentially lead to an unauthorized connection. 264 if (bondState != BluetoothDevice.BOND_BONDED) { 265 Log.w(TAG, "canConnect: return false, bondState=" + bondState); 266 return false; 267 } else if (connectionPolicy != BluetoothProfile.CONNECTION_POLICY_UNKNOWN 268 && connectionPolicy != BluetoothProfile.CONNECTION_POLICY_ALLOWED) { 269 // Otherwise, reject the connection if connectionPolicy is not valid. 270 Log.w(TAG, "canConnect: return false, connectionPolicy=" + connectionPolicy); 271 return false; 272 } 273 return true; 274 } 275 276 /** Called when the connection state of a state machine is changed */ 277 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) handleConnectionStateChanged(BatteryStateMachine sm, int fromState, int toState)278 public void handleConnectionStateChanged(BatteryStateMachine sm, int fromState, int toState) { 279 BluetoothDevice device = sm.getDevice(); 280 if ((sm == null) || (fromState == toState)) { 281 Log.e( 282 TAG, 283 "connectionStateChanged: unexpected invocation. device=" 284 + device 285 + " fromState=" 286 + fromState 287 + " toState=" 288 + toState); 289 return; 290 } 291 292 // Check if the device is disconnected - if unbonded, remove the state machine 293 if (toState == BluetoothProfile.STATE_DISCONNECTED) { 294 int bondState = mAdapterService.getBondState(device); 295 if (bondState == BluetoothDevice.BOND_NONE) { 296 Log.d(TAG, device + " is unbonded. Remove state machine"); 297 removeStateMachine(device); 298 } 299 } 300 } 301 302 @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) getDevicesMatchingConnectionStates(int[] states)303 List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) { 304 enforceCallingOrSelfPermission( 305 BLUETOOTH_PRIVILEGED, "Need BLUETOOTH_PRIVILEGED permission"); 306 ArrayList<BluetoothDevice> devices = new ArrayList<>(); 307 if (states == null) { 308 return devices; 309 } 310 final BluetoothDevice[] bondedDevices = mAdapterService.getBondedDevices(); 311 if (bondedDevices == null) { 312 return devices; 313 } 314 synchronized (mStateMachines) { 315 for (BluetoothDevice device : bondedDevices) { 316 int connectionState = BluetoothProfile.STATE_DISCONNECTED; 317 BatteryStateMachine sm = mStateMachines.get(device); 318 if (sm != null) { 319 connectionState = sm.getConnectionState(); 320 } 321 for (int state : states) { 322 if (connectionState == state) { 323 devices.add(device); 324 break; 325 } 326 } 327 } 328 return devices; 329 } 330 } 331 332 /** 333 * Get the list of devices that have state machines. 334 * 335 * @return the list of devices that have state machines 336 */ 337 @VisibleForTesting getDevices()338 List<BluetoothDevice> getDevices() { 339 List<BluetoothDevice> devices = new ArrayList<>(); 340 synchronized (mStateMachines) { 341 for (BatteryStateMachine sm : mStateMachines.values()) { 342 devices.add(sm.getDevice()); 343 } 344 return devices; 345 } 346 } 347 348 /** Gets the connection state of the given device's battery service */ 349 @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) getConnectionState(BluetoothDevice device)350 public int getConnectionState(BluetoothDevice device) { 351 enforceCallingOrSelfPermission(BLUETOOTH_CONNECT, "Need BLUETOOTH_CONNECT permission"); 352 synchronized (mStateMachines) { 353 BatteryStateMachine sm = mStateMachines.get(device); 354 if (sm == null) { 355 return BluetoothProfile.STATE_DISCONNECTED; 356 } 357 return sm.getConnectionState(); 358 } 359 } 360 361 /** 362 * Set connection policy of the profile and connects it if connectionPolicy is {@link 363 * BluetoothProfile#CONNECTION_POLICY_ALLOWED} or disconnects if connectionPolicy is {@link 364 * BluetoothProfile#CONNECTION_POLICY_FORBIDDEN} 365 * 366 * <p>The device should already be paired. Connection policy can be one of: {@link 367 * BluetoothProfile#CONNECTION_POLICY_ALLOWED}, {@link 368 * BluetoothProfile#CONNECTION_POLICY_FORBIDDEN}, {@link 369 * BluetoothProfile#CONNECTION_POLICY_UNKNOWN} 370 * 371 * @param device the remote device 372 * @param connectionPolicy is the connection policy to set to for this profile 373 * @return true on success, otherwise false 374 */ 375 @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) setConnectionPolicy(BluetoothDevice device, int connectionPolicy)376 public boolean setConnectionPolicy(BluetoothDevice device, int connectionPolicy) { 377 enforceCallingOrSelfPermission( 378 BLUETOOTH_PRIVILEGED, "Need BLUETOOTH_PRIVILEGED permission"); 379 Log.d(TAG, "Saved connectionPolicy " + device + " = " + connectionPolicy); 380 mDatabaseManager.setProfileConnectionPolicy( 381 device, BluetoothProfile.BATTERY, connectionPolicy); 382 if (connectionPolicy == BluetoothProfile.CONNECTION_POLICY_ALLOWED) { 383 connect(device); 384 } else if (connectionPolicy == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) { 385 disconnect(device); 386 } 387 return true; 388 } 389 390 /** Gets the connection policy for the battery service of the given device. */ 391 @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) getConnectionPolicy(BluetoothDevice device)392 public int getConnectionPolicy(BluetoothDevice device) { 393 enforceCallingOrSelfPermission( 394 BLUETOOTH_PRIVILEGED, "Need BLUETOOTH_PRIVILEGED permission"); 395 return mDatabaseManager.getProfileConnectionPolicy(device, BluetoothProfile.BATTERY); 396 } 397 398 /** Called when the battery level of the device is notified. */ 399 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) handleBatteryChanged(BluetoothDevice device, int batteryLevel)400 public void handleBatteryChanged(BluetoothDevice device, int batteryLevel) { 401 mAdapterService.setBatteryLevel(device, batteryLevel, /* isBas= */ true); 402 } 403 getOrCreateStateMachine(BluetoothDevice device)404 private BatteryStateMachine getOrCreateStateMachine(BluetoothDevice device) { 405 if (device == null) { 406 Log.e(TAG, "getOrCreateGatt failed: device cannot be null"); 407 return null; 408 } 409 synchronized (mStateMachines) { 410 BatteryStateMachine sm = mStateMachines.get(device); 411 if (sm != null) { 412 return sm; 413 } 414 // Limit the maximum number of state machines to avoid DoS attack 415 if (mStateMachines.size() >= MAX_BATTERY_STATE_MACHINES) { 416 Log.e( 417 TAG, 418 "Maximum number of Battery state machines reached: " 419 + MAX_BATTERY_STATE_MACHINES); 420 return null; 421 } 422 Log.d(TAG, "Creating a new state machine for " + device); 423 sm = BatteryStateMachine.make(device, this, mStateMachinesThread.getLooper()); 424 mStateMachines.put(device, sm); 425 return sm; 426 } 427 } 428 429 /** Process a change in the bonding state for a device */ handleBondStateChanged(BluetoothDevice device, int fromState, int toState)430 public void handleBondStateChanged(BluetoothDevice device, int fromState, int toState) { 431 mHandler.post(() -> bondStateChanged(device, toState)); 432 } 433 434 /** 435 * Remove state machine if the bonding for a device is removed 436 * 437 * @param device the device whose bonding state has changed 438 * @param bondState the new bond state for the device. Possible values are: {@link 439 * BluetoothDevice#BOND_NONE}, {@link BluetoothDevice#BOND_BONDING}, {@link 440 * BluetoothDevice#BOND_BONDED}, {@link BluetoothDevice#ERROR}. 441 */ 442 @VisibleForTesting bondStateChanged(BluetoothDevice device, int bondState)443 void bondStateChanged(BluetoothDevice device, int bondState) { 444 Log.d(TAG, "Bond state changed for device: " + device + " state: " + bondState); 445 // Remove state machine if the bonding for a device is removed 446 if (bondState != BluetoothDevice.BOND_NONE) { 447 return; 448 } 449 450 synchronized (mStateMachines) { 451 BatteryStateMachine sm = mStateMachines.get(device); 452 if (sm == null) { 453 return; 454 } 455 if (sm.getConnectionState() != BluetoothProfile.STATE_DISCONNECTED) { 456 return; 457 } 458 removeStateMachine(device); 459 } 460 } 461 removeStateMachine(BluetoothDevice device)462 private void removeStateMachine(BluetoothDevice device) { 463 if (device == null) { 464 Log.e(TAG, "removeStateMachine failed: device cannot be null"); 465 return; 466 } 467 synchronized (mStateMachines) { 468 BatteryStateMachine sm = mStateMachines.remove(device); 469 if (sm == null) { 470 Log.w( 471 TAG, 472 "removeStateMachine: device " + device + " does not have a state machine"); 473 return; 474 } 475 Log.i(TAG, "removeGatt: removing bluetooth gatt for device: " + device); 476 sm.doQuit(); 477 sm.cleanup(); 478 } 479 } 480 481 /** Binder object: must be a static class or memory leak may occur */ 482 @VisibleForTesting 483 static class BluetoothBatteryBinder extends IBluetoothBattery.Stub 484 implements IProfileServiceBinder { 485 private final WeakReference<BatteryService> mServiceRef; 486 487 @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) getService(AttributionSource source)488 private BatteryService getService(AttributionSource source) { 489 BatteryService service = mServiceRef.get(); 490 if (Utils.isInstrumentationTestMode()) { 491 return service; 492 } 493 494 if (!Utils.checkServiceAvailable(service, TAG) 495 || !Utils.checkCallerIsSystemOrActiveOrManagedUser(service, TAG) 496 || !Utils.checkConnectPermissionForDataDelivery(service, source, TAG)) { 497 return null; 498 } 499 return service; 500 } 501 BluetoothBatteryBinder(BatteryService svc)502 BluetoothBatteryBinder(BatteryService svc) { 503 mServiceRef = new WeakReference<>(svc); 504 } 505 506 @Override cleanup()507 public void cleanup() { 508 mServiceRef.clear(); 509 } 510 511 @Override connect(BluetoothDevice device, AttributionSource source)512 public boolean connect(BluetoothDevice device, AttributionSource source) { 513 BatteryService service = getService(source); 514 if (service == null) { 515 return false; 516 } 517 518 return service.connect(device); 519 } 520 521 @Override disconnect(BluetoothDevice device, AttributionSource source)522 public boolean disconnect(BluetoothDevice device, AttributionSource source) { 523 BatteryService service = getService(source); 524 if (service == null) { 525 return false; 526 } 527 528 return service.disconnect(device); 529 } 530 531 @Override getConnectedDevices(AttributionSource source)532 public List<BluetoothDevice> getConnectedDevices(AttributionSource source) { 533 BatteryService service = getService(source); 534 if (service == null) { 535 return Collections.emptyList(); 536 } 537 538 enforceBluetoothPrivilegedPermission(service); 539 return service.getConnectedDevices(); 540 } 541 542 @Override getDevicesMatchingConnectionStates( int[] states, AttributionSource source)543 public List<BluetoothDevice> getDevicesMatchingConnectionStates( 544 int[] states, AttributionSource source) { 545 BatteryService service = getService(source); 546 if (service == null) { 547 return Collections.emptyList(); 548 } 549 550 return service.getDevicesMatchingConnectionStates(states); 551 } 552 553 @Override getConnectionState(BluetoothDevice device, AttributionSource source)554 public int getConnectionState(BluetoothDevice device, AttributionSource source) { 555 BatteryService service = getService(source); 556 if (service == null) { 557 return BluetoothProfile.STATE_DISCONNECTED; 558 } 559 560 return service.getConnectionState(device); 561 } 562 563 @Override setConnectionPolicy( BluetoothDevice device, int connectionPolicy, AttributionSource source)564 public boolean setConnectionPolicy( 565 BluetoothDevice device, int connectionPolicy, AttributionSource source) { 566 BatteryService service = getService(source); 567 if (service == null) { 568 return false; 569 } 570 571 return service.setConnectionPolicy(device, connectionPolicy); 572 } 573 574 @Override getConnectionPolicy(BluetoothDevice device, AttributionSource source)575 public int getConnectionPolicy(BluetoothDevice device, AttributionSource source) { 576 BatteryService service = getService(source); 577 if (service == null) { 578 return BluetoothProfile.CONNECTION_POLICY_UNKNOWN; 579 } 580 581 return service.getConnectionPolicy(device); 582 } 583 } 584 585 @Override dump(StringBuilder sb)586 public void dump(StringBuilder sb) { 587 super.dump(sb); 588 for (BatteryStateMachine sm : mStateMachines.values()) { 589 sm.dump(sb); 590 } 591 } 592 } 593