blob: 21bec2b41a1ca91f4532945a10733df407f18663 [file] [log] [blame]
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.connecteddevice.ble;
import static com.android.car.connecteddevice.util.SafeLog.logd;
import static com.android.car.connecteddevice.util.SafeLog.loge;
import static com.android.car.connecteddevice.util.SafeLog.logw;
import static com.android.car.connecteddevice.util.ScanDataAnalyzer.containsUuidsInOverflow;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanRecord;
import android.bluetooth.le.ScanResult;
import android.bluetooth.le.ScanSettings;
import android.content.Context;
import android.os.ParcelUuid;
import androidx.annotation.NonNull;
import com.android.car.connecteddevice.storage.ConnectedDeviceStorage;
import java.math.BigInteger;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* Communication manager for a car that maintains continuous connections with all devices in the car
* for the duration of a drive.
*/
public class CarBleCentralManager extends CarBleManager {
private static final String TAG = "CarBleCentralManager";
// system/bt/internal_include/bt_target.h#GATT_MAX_PHY_CHANNEL
private static final int MAX_CONNECTIONS = 7;
private static final UUID CHARACTERISTIC_CONFIG =
UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
private static final int STATUS_FORCED_DISCONNECT = -1;
private final ScanSettings mScanSettings = new ScanSettings.Builder()
.setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE)
.build();
private final CopyOnWriteArraySet<BleDevice> mIgnoredDevices = new CopyOnWriteArraySet<>();
private final Context mContext;
private final BleCentralManager mBleCentralManager;
private final UUID mServiceUuid;
private final UUID mWriteCharacteristicUuid;
private final UUID mReadCharacteristicUuid;
private final BigInteger mParsedBgServiceBitMask;
/**
* Create a new manager.
*
* @param context The caller's [Context].
* @param bleCentralManager [BleCentralManager] for establishing connections.
* @param connectedDeviceStorage Shared [ConnectedDeviceStorage] for companion features.
* @param serviceUuid [UUID] of peripheral's service.
* @param bgServiceMask iOS overflow bit mask for service UUID.
* @param writeCharacteristicUuid [UUID] of characteristic the car will write to.
* @param readCharacteristicUuid [UUID] of characteristic the device will write to.
*/
public CarBleCentralManager(
@NonNull Context context,
@NonNull BleCentralManager bleCentralManager,
@NonNull ConnectedDeviceStorage connectedDeviceStorage,
@NonNull UUID serviceUuid,
@NonNull String bgServiceMask,
@NonNull UUID writeCharacteristicUuid,
@NonNull UUID readCharacteristicUuid) {
super(connectedDeviceStorage);
mContext = context;
mBleCentralManager = bleCentralManager;
mServiceUuid = serviceUuid;
mWriteCharacteristicUuid = writeCharacteristicUuid;
mReadCharacteristicUuid = readCharacteristicUuid;
mParsedBgServiceBitMask = new BigInteger(bgServiceMask, 16);
}
@Override
public void start() {
super.start();
mBleCentralManager.startScanning(/* filters= */ null, mScanSettings, mScanCallback);
}
@Override
public void stop() {
super.stop();
mBleCentralManager.stopScanning();
}
@Override
public void disconnectDevice(String deviceId) {
logd(TAG, "Request to disconnect from device " + deviceId + ".");
BleDevice device = getConnectedDevice(deviceId);
if (device == null) {
return;
}
deviceDisconnected(device, STATUS_FORCED_DISCONNECT);
}
private void ignoreDevice(@NonNull BleDevice device) {
mIgnoredDevices.add(device);
}
private boolean isDeviceIgnored(@NonNull BluetoothDevice device) {
for (BleDevice bleDevice : mIgnoredDevices) {
if (device.equals(bleDevice.mDevice)) {
return true;
}
}
return false;
}
private boolean shouldAttemptConnection(@NonNull ScanResult result) {
// Ignore any results that are not connectable.
if (!result.isConnectable()) {
return false;
}
// Do not attempt to connect if we have already hit our max. This should rarely happen
// and is protecting against a race condition of scanning stopped and new results coming in.
if (getConnectedDevicesCount() >= MAX_CONNECTIONS) {
return false;
}
BluetoothDevice device = result.getDevice();
// Do not connect if device has already been ignored.
if (isDeviceIgnored(device)) {
return false;
}
// Check if already attempting to connect to this device.
if (getConnectedDevice(device) != null) {
return false;
}
// Ignore any device without a scan record.
ScanRecord scanRecord = result.getScanRecord();
if (scanRecord == null) {
return false;
}
// Connect to any device that is advertising our service UUID.
List<ParcelUuid> serviceUuids = scanRecord.getServiceUuids();
if (serviceUuids != null) {
for (ParcelUuid serviceUuid : serviceUuids) {
if (serviceUuid.getUuid().equals(mServiceUuid)) {
return true;
}
}
}
if (containsUuidsInOverflow(scanRecord.getBytes(), mParsedBgServiceBitMask)) {
return true;
}
// Can safely ignore devices advertising unrecognized service uuids.
if (serviceUuids != null && !serviceUuids.isEmpty()) {
return false;
}
// TODO(b/139066293): Current implementation quickly exhausts connections resulting in
// greatly reduced performance for connecting to devices we know we want to connect to.
// Return true once fixed.
return false;
}
private void startDeviceConnection(@NonNull BluetoothDevice device) {
BluetoothGatt gatt = device.connectGatt(mContext, /* autoConnect= */ false,
mConnectionCallback, BluetoothDevice.TRANSPORT_LE);
if (gatt == null) {
return;
}
BleDevice bleDevice = new BleDevice(device, gatt);
bleDevice.mState = BleDeviceState.CONNECTING;
addConnectedDevice(bleDevice);
// Stop scanning if we have reached the maximum number of connections.
if (getConnectedDevicesCount() >= MAX_CONNECTIONS) {
mBleCentralManager.stopScanning();
}
}
private void deviceConnected(@NonNull BleDevice device) {
if (device.mGatt == null) {
loge(TAG, "Device connected with null gatt. Disconnecting.");
deviceDisconnected(device, BluetoothProfile.STATE_DISCONNECTED);
return;
}
device.mState = BleDeviceState.PENDING_VERIFICATION;
device.mGatt.discoverServices();
logd(TAG, "New device connected: " + device.mGatt.getDevice().getAddress()
+ ". Active connections: " + getConnectedDevicesCount() + ".");
}
private void deviceDisconnected(@NonNull BleDevice device, int status) {
removeConnectedDevice(device);
if (device.mGatt != null) {
device.mGatt.close();
}
if (device.mDeviceId != null) {
mCallbacks.invoke(callback -> callback.onDeviceDisconnected(device.mDeviceId));
}
logd(TAG, "Device with id " + device.mDeviceId + " disconnected with state " + status
+ ". Remaining active connections: " + getConnectedDevicesCount() + ".");
}
private final ScanCallback mScanCallback = new ScanCallback() {
@Override
public void onScanResult(int callbackType, ScanResult result) {
super.onScanResult(callbackType, result);
if (shouldAttemptConnection(result)) {
startDeviceConnection(result.getDevice());
}
}
@Override
public void onScanFailed(int errorCode) {
super.onScanFailed(errorCode);
loge(TAG, "BLE scanning failed with error code: " + errorCode);
}
};
private final BluetoothGattCallback mConnectionCallback = new BluetoothGattCallback() {
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
super.onConnectionStateChange(gatt, status, newState);
if (gatt == null) {
logw(TAG, "Null gatt passed to onConnectionStateChange. Ignoring.");
return;
}
BleDevice connectedDevice = getConnectedDevice(gatt);
if (connectedDevice == null) {
return;
}
switch (newState) {
case BluetoothProfile.STATE_CONNECTED:
deviceConnected(connectedDevice);
break;
case BluetoothProfile.STATE_DISCONNECTED:
deviceDisconnected(connectedDevice, status);
break;
default:
logd(TAG, "Connection state changed. New state: " + newState + " status: "
+ status);
}
}
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
super.onServicesDiscovered(gatt, status);
if (gatt == null) {
logw(TAG, "Null gatt passed to onServicesDiscovered. Ignoring.");
return;
}
BleDevice connectedDevice = getConnectedDevice(gatt);
if (connectedDevice == null) {
return;
}
BluetoothGattService service = gatt.getService(mServiceUuid);
if (service == null) {
ignoreDevice(connectedDevice);
gatt.disconnect();
return;
}
connectedDevice.mState = BleDeviceState.CONNECTED;
BluetoothGattCharacteristic writeCharacteristic =
service.getCharacteristic(mWriteCharacteristicUuid);
BluetoothGattCharacteristic readCharacteristic =
service.getCharacteristic(mReadCharacteristicUuid);
if (writeCharacteristic == null || readCharacteristic == null) {
logw(TAG, "Unable to find expected characteristics on peripheral.");
gatt.disconnect();
return;
}
// Turn on notifications for read characteristic.
BluetoothGattDescriptor descriptor =
readCharacteristic.getDescriptor(CHARACTERISTIC_CONFIG);
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
if (!gatt.writeDescriptor(descriptor)) {
loge(TAG, "Write descriptor to read characteristic failed.");
gatt.disconnect();
return;
}
if (!gatt.setCharacteristicNotification(readCharacteristic, /* enable= */ true)) {
loge(TAG, "Set notifications to read characteristic failed.");
gatt.disconnect();
return;
}
logd(TAG, "Service and characteristics successfully discovered.");
}
@Override
public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor,
int status) {
super.onDescriptorWrite(gatt, descriptor, status);
if (gatt == null) {
logw(TAG, "Null gatt passed to onDescriptorWrite. Ignoring.");
return;
}
// TODO(b/141312136): Create SecureBleChannel and assign to connectedDevice.
}
};
}