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.BluetoothProfile.CONNECTION_POLICY_ALLOWED; 20 import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; 21 import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_UNKNOWN; 22 import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTED; 23 24 import static java.util.Objects.requireNonNull; 25 26 import android.bluetooth.BluetoothDevice; 27 import android.bluetooth.BluetoothProfile; 28 import android.bluetooth.BluetoothUuid; 29 import android.os.Handler; 30 import android.os.HandlerThread; 31 import android.os.Looper; 32 import android.os.ParcelUuid; 33 import android.sysprop.BluetoothProperties; 34 import android.util.Log; 35 36 import com.android.bluetooth.Utils; 37 import com.android.bluetooth.btservice.AdapterService; 38 import com.android.bluetooth.btservice.ProfileService; 39 import com.android.bluetooth.btservice.storage.DatabaseManager; 40 import com.android.internal.annotations.GuardedBy; 41 import com.android.internal.annotations.VisibleForTesting; 42 43 import java.util.ArrayList; 44 import java.util.HashMap; 45 import java.util.List; 46 import java.util.Map; 47 48 /** A profile service that connects to the Battery service (BAS) of BLE devices */ 49 public class BatteryService extends ProfileService { 50 private static final String TAG = BatteryService.class.getSimpleName(); 51 52 // Timeout for state machine thread join, to prevent potential ANR. 53 private static final int SM_THREAD_JOIN_TIMEOUT_MS = 1_000; 54 55 private static BatteryService sBatteryService; 56 57 private final AdapterService mAdapterService; 58 private final DatabaseManager mDatabaseManager; 59 private final HandlerThread mStateMachinesThread; 60 private final Handler mHandler; 61 62 @GuardedBy("mStateMachines") 63 private final Map<BluetoothDevice, BatteryStateMachine> mStateMachines = new HashMap<>(); 64 BatteryService(AdapterService adapterService)65 public BatteryService(AdapterService adapterService) { 66 this(adapterService, Looper.getMainLooper()); 67 } 68 69 @VisibleForTesting BatteryService(AdapterService adapterService, Looper looper)70 BatteryService(AdapterService adapterService, Looper looper) { 71 super(requireNonNull(adapterService)); 72 mAdapterService = adapterService; 73 mDatabaseManager = requireNonNull(mAdapterService.getDatabase()); 74 mHandler = new Handler(requireNonNull(looper)); 75 76 mStateMachinesThread = new HandlerThread("BatteryService.StateMachines"); 77 mStateMachinesThread.start(); 78 setBatteryService(this); 79 } 80 isEnabled()81 public static boolean isEnabled() { 82 return BluetoothProperties.isProfileBasClientEnabled().orElse(false); 83 } 84 85 @Override initBinder()86 protected IProfileServiceBinder initBinder() { 87 return null; 88 } 89 90 @Override cleanup()91 public void cleanup() { 92 Log.i(TAG, "Cleanup Battery Service"); 93 94 setBatteryService(null); 95 96 // Destroy state machines and stop handler thread 97 synchronized (mStateMachines) { 98 for (BatteryStateMachine sm : mStateMachines.values()) { 99 sm.doQuit(); 100 sm.cleanup(); 101 } 102 mStateMachines.clear(); 103 } 104 105 try { 106 mStateMachinesThread.quitSafely(); 107 mStateMachinesThread.join(SM_THREAD_JOIN_TIMEOUT_MS); 108 } catch (InterruptedException e) { 109 // Do not rethrow as we are shutting down anyway 110 } 111 112 mHandler.removeCallbacksAndMessages(null); 113 } 114 115 /** Gets the BatteryService instance */ getBatteryService()116 public static synchronized BatteryService getBatteryService() { 117 if (sBatteryService == null) { 118 Log.w(TAG, "getBatteryService(): service is NULL"); 119 return null; 120 } 121 122 if (!sBatteryService.isAvailable()) { 123 Log.w(TAG, "getBatteryService(): service is not available"); 124 return null; 125 } 126 return sBatteryService; 127 } 128 129 /** Sets the battery service instance. It should be called only for testing purpose. */ 130 @VisibleForTesting setBatteryService(BatteryService instance)131 public static synchronized void setBatteryService(BatteryService instance) { 132 Log.d(TAG, "setBatteryService(): set to: " + instance); 133 sBatteryService = instance; 134 } 135 136 /** Connects to the battery service of the given device. */ connect(BluetoothDevice device)137 public boolean connect(BluetoothDevice device) { 138 Log.d(TAG, "connect(): " + device); 139 if (device == null) { 140 Log.w(TAG, "Ignore connecting to null device"); 141 return false; 142 } 143 144 if (getConnectionPolicy(device) == CONNECTION_POLICY_FORBIDDEN) { 145 Log.w(TAG, "Cannot connect to " + device + " : policy forbidden"); 146 return false; 147 } 148 final ParcelUuid[] featureUuids = mAdapterService.getRemoteUuids(device); 149 if (!Utils.arrayContains(featureUuids, BluetoothUuid.BATTERY)) { 150 Log.e(TAG, "Cannot connect to " + device + " : Remote does not have Battery UUID"); 151 return false; 152 } 153 154 synchronized (mStateMachines) { 155 BatteryStateMachine sm = getOrCreateStateMachine(device); 156 if (sm == null) { 157 Log.e(TAG, "Cannot connect to " + device + " : no state machine"); 158 return false; 159 } 160 sm.sendMessage(BatteryStateMachine.MESSAGE_CONNECT); 161 } 162 163 return true; 164 } 165 166 /** 167 * Connects to the battery service of the given device if possible. If it's impossible, it 168 * doesn't try without logging errors. 169 */ connectIfPossible(BluetoothDevice device)170 public boolean connectIfPossible(BluetoothDevice device) { 171 if (device == null 172 || getConnectionPolicy(device) == CONNECTION_POLICY_FORBIDDEN 173 || !Utils.arrayContains( 174 mAdapterService.getRemoteUuids(device), BluetoothUuid.BATTERY)) { 175 return false; 176 } 177 return connect(device); 178 } 179 180 /** Disconnects from the battery service of the given device. */ disconnect(BluetoothDevice device)181 public boolean disconnect(BluetoothDevice device) { 182 Log.d(TAG, "disconnect(): " + device); 183 if (device == null) { 184 Log.w(TAG, "Ignore disconnecting to null device"); 185 return false; 186 } 187 synchronized (mStateMachines) { 188 BatteryStateMachine sm = getOrCreateStateMachine(device); 189 if (sm != null) { 190 sm.sendMessage(BatteryStateMachine.MESSAGE_DISCONNECT); 191 } 192 } 193 194 return true; 195 } 196 197 /** Gets devices that battery service is connected. */ getConnectedDevices()198 public List<BluetoothDevice> getConnectedDevices() { 199 synchronized (mStateMachines) { 200 List<BluetoothDevice> devices = new ArrayList<>(); 201 for (BatteryStateMachine sm : mStateMachines.values()) { 202 if (sm.isConnected()) { 203 devices.add(sm.getDevice()); 204 } 205 } 206 return devices; 207 } 208 } 209 210 /** 211 * Check whether it can connect to a peer device. The check considers a number of factors during 212 * the evaluation. 213 */ canConnect(BluetoothDevice device)214 boolean canConnect(BluetoothDevice device) { 215 // Check connectionPolicy and accept or reject the connection. 216 int connectionPolicy = getConnectionPolicy(device); 217 int bondState = mAdapterService.getBondState(device); 218 // Allow this connection only if the device is bonded. Any attempt to connect while 219 // bonding would potentially lead to an unauthorized connection. 220 if (bondState != BluetoothDevice.BOND_BONDED) { 221 Log.w(TAG, "canConnect: return false, bondState=" + bondState); 222 return false; 223 } else if (connectionPolicy != CONNECTION_POLICY_UNKNOWN 224 && connectionPolicy != CONNECTION_POLICY_ALLOWED) { 225 // Otherwise, reject the connection if connectionPolicy is not valid. 226 Log.w(TAG, "canConnect: return false, connectionPolicy=" + connectionPolicy); 227 return false; 228 } 229 return true; 230 } 231 232 /** Called when the connection state of a state machine is changed */ handleConnectionStateChanged(BluetoothDevice device, int fromState, int toState)233 void handleConnectionStateChanged(BluetoothDevice device, int fromState, int toState) { 234 if (fromState == toState) { 235 Log.e( 236 TAG, 237 "connectionStateChanged: unexpected invocation. device=" 238 + device 239 + " fromState=" 240 + fromState 241 + " toState=" 242 + toState); 243 return; 244 } 245 246 // Check if the device is disconnected - if unbonded, remove the state machine 247 if (toState == STATE_DISCONNECTED) { 248 int bondState = mAdapterService.getBondState(device); 249 if (bondState == BluetoothDevice.BOND_NONE) { 250 Log.d(TAG, device + " is unbonded. Remove state machine"); 251 removeStateMachine(device); 252 } 253 } 254 } 255 getDevicesMatchingConnectionStates(int[] states)256 List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) { 257 ArrayList<BluetoothDevice> devices = new ArrayList<>(); 258 if (states == null) { 259 return devices; 260 } 261 final BluetoothDevice[] bondedDevices = mAdapterService.getBondedDevices(); 262 if (bondedDevices == null) { 263 return devices; 264 } 265 synchronized (mStateMachines) { 266 for (BluetoothDevice device : bondedDevices) { 267 int connectionState = STATE_DISCONNECTED; 268 BatteryStateMachine sm = mStateMachines.get(device); 269 if (sm != null) { 270 connectionState = sm.getConnectionState(); 271 } 272 for (int state : states) { 273 if (connectionState == state) { 274 devices.add(device); 275 break; 276 } 277 } 278 } 279 return devices; 280 } 281 } 282 283 /** 284 * Get the list of devices that have state machines. 285 * 286 * @return the list of devices that have state machines 287 */ 288 @VisibleForTesting getDevices()289 List<BluetoothDevice> getDevices() { 290 List<BluetoothDevice> devices = new ArrayList<>(); 291 synchronized (mStateMachines) { 292 for (BatteryStateMachine sm : mStateMachines.values()) { 293 devices.add(sm.getDevice()); 294 } 295 return devices; 296 } 297 } 298 299 /** Gets the connection state of the given device's battery service */ getConnectionState(BluetoothDevice device)300 public int getConnectionState(BluetoothDevice device) { 301 synchronized (mStateMachines) { 302 BatteryStateMachine sm = mStateMachines.get(device); 303 if (sm == null) { 304 return STATE_DISCONNECTED; 305 } 306 return sm.getConnectionState(); 307 } 308 } 309 310 /** 311 * Set connection policy of the profile and connects it if connectionPolicy is {@link 312 * BluetoothProfile#CONNECTION_POLICY_ALLOWED} or disconnects if connectionPolicy is {@link 313 * BluetoothProfile#CONNECTION_POLICY_FORBIDDEN} 314 * 315 * <p>The device should already be paired. Connection policy can be one of: {@link 316 * BluetoothProfile#CONNECTION_POLICY_ALLOWED}, {@link 317 * BluetoothProfile#CONNECTION_POLICY_FORBIDDEN}, {@link 318 * BluetoothProfile#CONNECTION_POLICY_UNKNOWN} 319 * 320 * @param device the remote device 321 * @param connectionPolicy is the connection policy to set to for this profile 322 * @return true on success, otherwise false 323 */ setConnectionPolicy(BluetoothDevice device, int connectionPolicy)324 public boolean setConnectionPolicy(BluetoothDevice device, int connectionPolicy) { 325 Log.d(TAG, "Saved connectionPolicy " + device + " = " + connectionPolicy); 326 mDatabaseManager.setProfileConnectionPolicy( 327 device, BluetoothProfile.BATTERY, connectionPolicy); 328 if (connectionPolicy == CONNECTION_POLICY_ALLOWED) { 329 connect(device); 330 } else if (connectionPolicy == CONNECTION_POLICY_FORBIDDEN) { 331 disconnect(device); 332 } 333 return true; 334 } 335 336 /** Gets the connection policy for the battery service of the given device. */ getConnectionPolicy(BluetoothDevice device)337 public int getConnectionPolicy(BluetoothDevice device) { 338 return mDatabaseManager.getProfileConnectionPolicy(device, BluetoothProfile.BATTERY); 339 } 340 341 /** Called when the battery level of the device is notified. */ handleBatteryChanged(BluetoothDevice device, int batteryLevel)342 void handleBatteryChanged(BluetoothDevice device, int batteryLevel) { 343 mAdapterService.setBatteryLevel(device, batteryLevel, /* isBas= */ true); 344 } 345 getOrCreateStateMachine(BluetoothDevice device)346 private BatteryStateMachine getOrCreateStateMachine(BluetoothDevice device) { 347 synchronized (mStateMachines) { 348 BatteryStateMachine sm = mStateMachines.get(device); 349 if (sm != null) { 350 return sm; 351 } 352 353 Log.d(TAG, "Creating a new state machine for " + device); 354 sm = new BatteryStateMachine(this, device, mStateMachinesThread.getLooper()); 355 mStateMachines.put(device, sm); 356 return sm; 357 } 358 } 359 360 /** Process a change in the bonding state for a device */ handleBondStateChanged(BluetoothDevice device, int fromState, int toState)361 public void handleBondStateChanged(BluetoothDevice device, int fromState, int toState) { 362 mHandler.post(() -> bondStateChanged(device, toState)); 363 } 364 365 /** Remove state machine if the bonding for a device is removed */ bondStateChanged(BluetoothDevice device, int bondState)366 private void bondStateChanged(BluetoothDevice device, int bondState) { 367 Log.d(TAG, "Bond state changed for device: " + device + " state: " + bondState); 368 // Remove state machine if the bonding for a device is removed 369 if (bondState != BluetoothDevice.BOND_NONE) { 370 return; 371 } 372 373 synchronized (mStateMachines) { 374 BatteryStateMachine sm = mStateMachines.get(device); 375 if (sm == null) { 376 return; 377 } 378 if (sm.getConnectionState() != STATE_DISCONNECTED) { 379 return; 380 } 381 removeStateMachine(device); 382 } 383 } 384 removeStateMachine(BluetoothDevice device)385 private void removeStateMachine(BluetoothDevice device) { 386 synchronized (mStateMachines) { 387 BatteryStateMachine sm = mStateMachines.remove(device); 388 if (sm == null) { 389 Log.w(TAG, "removeStateMachine: " + device + " does not have a state machine"); 390 return; 391 } 392 Log.i(TAG, "removeGatt: removing bluetooth gatt for device: " + device); 393 sm.doQuit(); 394 sm.cleanup(); 395 } 396 } 397 398 @Override dump(StringBuilder sb)399 public void dump(StringBuilder sb) { 400 super.dump(sb); 401 synchronized (mStateMachines) { 402 for (BatteryStateMachine sm : mStateMachines.values()) { 403 sm.dump(sb); 404 } 405 } 406 } 407 } 408