1 /* 2 * Copyright (C) 2020 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.car.connecteddevice.ble; 18 19 import static com.android.car.connecteddevice.util.SafeLog.logd; 20 import static com.android.car.connecteddevice.util.SafeLog.loge; 21 import static com.android.car.connecteddevice.util.SafeLog.logw; 22 import static com.android.car.connecteddevice.util.ScanDataAnalyzer.containsUuidsInOverflow; 23 24 import android.annotation.NonNull; 25 import android.bluetooth.BluetoothDevice; 26 import android.bluetooth.BluetoothGatt; 27 import android.bluetooth.BluetoothGattCallback; 28 import android.bluetooth.BluetoothGattCharacteristic; 29 import android.bluetooth.BluetoothGattDescriptor; 30 import android.bluetooth.BluetoothGattService; 31 import android.bluetooth.BluetoothProfile; 32 import android.bluetooth.le.ScanCallback; 33 import android.bluetooth.le.ScanRecord; 34 import android.bluetooth.le.ScanResult; 35 import android.bluetooth.le.ScanSettings; 36 import android.content.Context; 37 import android.os.ParcelUuid; 38 39 import com.android.car.connecteddevice.storage.ConnectedDeviceStorage; 40 41 import java.math.BigInteger; 42 import java.util.List; 43 import java.util.UUID; 44 import java.util.concurrent.CopyOnWriteArraySet; 45 46 /** 47 * Communication manager for a car that maintains continuous connections with all devices in the car 48 * for the duration of a drive. 49 */ 50 public class CarBleCentralManager extends CarBleManager { 51 52 private static final String TAG = "CarBleCentralManager"; 53 54 // system/bt/internal_include/bt_target.h#GATT_MAX_PHY_CHANNEL 55 private static final int MAX_CONNECTIONS = 7; 56 57 private static final UUID CHARACTERISTIC_CONFIG = 58 UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"); 59 60 private static final int STATUS_FORCED_DISCONNECT = -1; 61 62 private final ScanSettings mScanSettings = new ScanSettings.Builder() 63 .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) 64 .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) 65 .setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE) 66 .build(); 67 68 private final CopyOnWriteArraySet<BleDevice> mIgnoredDevices = new CopyOnWriteArraySet<>(); 69 70 private final Context mContext; 71 72 private final BleCentralManager mBleCentralManager; 73 74 private final UUID mServiceUuid; 75 76 private final UUID mWriteCharacteristicUuid; 77 78 private final UUID mReadCharacteristicUuid; 79 80 private final BigInteger mParsedBgServiceBitMask; 81 82 /** 83 * Create a new manager. 84 * 85 * @param context The caller's [Context]. 86 * @param bleCentralManager [BleCentralManager] for establishing connections. 87 * @param connectedDeviceStorage Shared [ConnectedDeviceStorage] for companion features. 88 * @param serviceUuid [UUID] of peripheral's service. 89 * @param bgServiceMask iOS overflow bit mask for service UUID. 90 * @param writeCharacteristicUuid [UUID] of characteristic the car will write to. 91 * @param readCharacteristicUuid [UUID] of characteristic the device will write to. 92 */ CarBleCentralManager( @onNull Context context, @NonNull BleCentralManager bleCentralManager, @NonNull ConnectedDeviceStorage connectedDeviceStorage, @NonNull UUID serviceUuid, @NonNull String bgServiceMask, @NonNull UUID writeCharacteristicUuid, @NonNull UUID readCharacteristicUuid)93 public CarBleCentralManager( 94 @NonNull Context context, 95 @NonNull BleCentralManager bleCentralManager, 96 @NonNull ConnectedDeviceStorage connectedDeviceStorage, 97 @NonNull UUID serviceUuid, 98 @NonNull String bgServiceMask, 99 @NonNull UUID writeCharacteristicUuid, 100 @NonNull UUID readCharacteristicUuid) { 101 super(connectedDeviceStorage); 102 mContext = context; 103 mBleCentralManager = bleCentralManager; 104 mServiceUuid = serviceUuid; 105 mWriteCharacteristicUuid = writeCharacteristicUuid; 106 mReadCharacteristicUuid = readCharacteristicUuid; 107 mParsedBgServiceBitMask = new BigInteger(bgServiceMask, 16); 108 } 109 110 @Override start()111 public void start() { 112 super.start(); 113 mBleCentralManager.startScanning(/* filters = */ null, mScanSettings, mScanCallback); 114 } 115 116 @Override stop()117 public void stop() { 118 super.stop(); 119 mBleCentralManager.stopScanning(); 120 } 121 122 @Override disconnectDevice(String deviceId)123 public void disconnectDevice(String deviceId) { 124 logd(TAG, "Request to disconnect from device " + deviceId + "."); 125 BleDevice device = getConnectedDevice(deviceId); 126 if (device == null) { 127 return; 128 } 129 130 deviceDisconnected(device, STATUS_FORCED_DISCONNECT); 131 } 132 ignoreDevice(@onNull BleDevice device)133 private void ignoreDevice(@NonNull BleDevice device) { 134 mIgnoredDevices.add(device); 135 } 136 isDeviceIgnored(@onNull BluetoothDevice device)137 private boolean isDeviceIgnored(@NonNull BluetoothDevice device) { 138 for (BleDevice bleDevice : mIgnoredDevices) { 139 if (device.equals(bleDevice.mDevice)) { 140 return true; 141 } 142 } 143 return false; 144 } 145 shouldAttemptConnection(@onNull ScanResult result)146 private boolean shouldAttemptConnection(@NonNull ScanResult result) { 147 // Ignore any results that are not connectable. 148 if (!result.isConnectable()) { 149 return false; 150 } 151 152 // Do not attempt to connect if we have already hit our max. This should rarely happen 153 // and is protecting against a race condition of scanning stopped and new results coming in. 154 if (getConnectedDevicesCount() >= MAX_CONNECTIONS) { 155 return false; 156 } 157 158 BluetoothDevice device = result.getDevice(); 159 160 // Do not connect if device has already been ignored. 161 if (isDeviceIgnored(device)) { 162 return false; 163 } 164 165 // Check if already attempting to connect to this device. 166 if (getConnectedDevice(device) != null) { 167 return false; 168 } 169 170 171 // Ignore any device without a scan record. 172 ScanRecord scanRecord = result.getScanRecord(); 173 if (scanRecord == null) { 174 return false; 175 } 176 177 // Connect to any device that is advertising our service UUID. 178 List<ParcelUuid> serviceUuids = scanRecord.getServiceUuids(); 179 if (serviceUuids != null) { 180 for (ParcelUuid serviceUuid : serviceUuids) { 181 if (serviceUuid.getUuid().equals(mServiceUuid)) { 182 return true; 183 } 184 } 185 } 186 if (containsUuidsInOverflow(scanRecord.getBytes(), mParsedBgServiceBitMask)) { 187 return true; 188 } 189 190 // Can safely ignore devices advertising unrecognized service uuids. 191 if (serviceUuids != null && !serviceUuids.isEmpty()) { 192 return false; 193 } 194 195 // TODO(b/139066293): Current implementation quickly exhausts connections resulting in 196 // greatly reduced performance for connecting to devices we know we want to connect to. 197 // Return true once fixed. 198 return false; 199 } 200 startDeviceConnection(@onNull BluetoothDevice device)201 private void startDeviceConnection(@NonNull BluetoothDevice device) { 202 BluetoothGatt gatt = device.connectGatt(mContext, /* autoConnect = */ false, 203 mConnectionCallback, BluetoothDevice.TRANSPORT_LE); 204 if (gatt == null) { 205 return; 206 } 207 208 BleDevice bleDevice = new BleDevice(device, gatt); 209 bleDevice.mState = BleDeviceState.CONNECTING; 210 addConnectedDevice(bleDevice); 211 212 // Stop scanning if we have reached the maximum number of connections. 213 if (getConnectedDevicesCount() >= MAX_CONNECTIONS) { 214 mBleCentralManager.stopScanning(); 215 } 216 } 217 deviceConnected(@onNull BleDevice device)218 private void deviceConnected(@NonNull BleDevice device) { 219 if (device.mGatt == null) { 220 loge(TAG, "Device connected with null gatt. Disconnecting."); 221 deviceDisconnected(device, BluetoothProfile.STATE_DISCONNECTED); 222 return; 223 } 224 device.mState = BleDeviceState.PENDING_VERIFICATION; 225 device.mGatt.discoverServices(); 226 logd(TAG, "New device connected: " + device.mGatt.getDevice().getAddress() 227 + ". Active connections: " + getConnectedDevicesCount() + "."); 228 } 229 deviceDisconnected(@onNull BleDevice device, int status)230 private void deviceDisconnected(@NonNull BleDevice device, int status) { 231 removeConnectedDevice(device); 232 if (device.mGatt != null) { 233 device.mGatt.close(); 234 } 235 if (device.mDeviceId != null) { 236 mCallbacks.invoke(callback -> callback.onDeviceDisconnected(device.mDeviceId)); 237 } 238 logd(TAG, "Device with id " + device.mDeviceId + " disconnected with state " + status 239 + ". Remaining active connections: " + getConnectedDevicesCount() + "."); 240 } 241 242 private final ScanCallback mScanCallback = new ScanCallback() { 243 @Override 244 public void onScanResult(int callbackType, ScanResult result) { 245 super.onScanResult(callbackType, result); 246 if (shouldAttemptConnection(result)) { 247 startDeviceConnection(result.getDevice()); 248 } 249 } 250 251 @Override 252 public void onScanFailed(int errorCode) { 253 super.onScanFailed(errorCode); 254 loge(TAG, "BLE scanning failed with error code: " + errorCode); 255 } 256 }; 257 258 private final BluetoothGattCallback mConnectionCallback = new BluetoothGattCallback() { 259 @Override 260 public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { 261 super.onConnectionStateChange(gatt, status, newState); 262 if (gatt == null) { 263 logw(TAG, "Null gatt passed to onConnectionStateChange. Ignoring."); 264 return; 265 } 266 267 BleDevice connectedDevice = getConnectedDevice(gatt); 268 if (connectedDevice == null) { 269 return; 270 } 271 272 switch (newState) { 273 case BluetoothProfile.STATE_CONNECTED: 274 deviceConnected(connectedDevice); 275 break; 276 case BluetoothProfile.STATE_DISCONNECTED: 277 deviceDisconnected(connectedDevice, status); 278 break; 279 default: 280 logd(TAG, "Connection state changed. New state: " + newState + " status: " 281 + status); 282 } 283 } 284 285 @Override 286 public void onServicesDiscovered(BluetoothGatt gatt, int status) { 287 super.onServicesDiscovered(gatt, status); 288 if (gatt == null) { 289 logw(TAG, "Null gatt passed to onServicesDiscovered. Ignoring."); 290 return; 291 } 292 293 BleDevice connectedDevice = getConnectedDevice(gatt); 294 if (connectedDevice == null) { 295 return; 296 } 297 BluetoothGattService service = gatt.getService(mServiceUuid); 298 if (service == null) { 299 ignoreDevice(connectedDevice); 300 gatt.disconnect(); 301 return; 302 } 303 304 connectedDevice.mState = BleDeviceState.CONNECTED; 305 BluetoothGattCharacteristic writeCharacteristic = 306 service.getCharacteristic(mWriteCharacteristicUuid); 307 BluetoothGattCharacteristic readCharacteristic = 308 service.getCharacteristic(mReadCharacteristicUuid); 309 if (writeCharacteristic == null || readCharacteristic == null) { 310 logw(TAG, "Unable to find expected characteristics on peripheral."); 311 gatt.disconnect(); 312 return; 313 } 314 315 // Turn on notifications for read characteristic. 316 BluetoothGattDescriptor descriptor = 317 readCharacteristic.getDescriptor(CHARACTERISTIC_CONFIG); 318 descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); 319 if (!gatt.writeDescriptor(descriptor)) { 320 loge(TAG, "Write descriptor to read characteristic failed."); 321 gatt.disconnect(); 322 return; 323 } 324 325 if (!gatt.setCharacteristicNotification(readCharacteristic, /* enable = */ true)) { 326 loge(TAG, "Set notifications to read characteristic failed."); 327 gatt.disconnect(); 328 return; 329 } 330 331 logd(TAG, "Service and characteristics successfully discovered."); 332 } 333 334 @Override 335 public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, 336 int status) { 337 super.onDescriptorWrite(gatt, descriptor, status); 338 if (gatt == null) { 339 logw(TAG, "Null gatt passed to onDescriptorWrite. Ignoring."); 340 return; 341 } 342 // TODO(b/141312136): Create SecureBleChannel and assign to connectedDevice. 343 } 344 }; 345 } 346