| /* |
| * 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.google.android.connecteddevice.connection.ble; |
| |
| import static com.google.android.connecteddevice.util.SafeLog.logd; |
| import static com.google.android.connecteddevice.util.SafeLog.loge; |
| import static com.google.android.connecteddevice.util.SafeLog.logw; |
| import static com.google.android.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.google.android.connecteddevice.connection.AssociationCallback; |
| import com.google.android.connecteddevice.connection.CarBluetoothManager; |
| import com.google.android.connecteddevice.oob.OobChannel; |
| import com.google.android.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 CarBluetoothManager { |
| |
| 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 scanSettings = |
| new ScanSettings.Builder() |
| .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) |
| .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) |
| .setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE) |
| .build(); |
| |
| private final CopyOnWriteArraySet<ConnectedRemoteDevice> ignoredDevices = |
| new CopyOnWriteArraySet<>(); |
| |
| private final Context context; |
| |
| private final BleCentralManager bleCentralManager; |
| |
| private final UUID serviceUuid; |
| |
| private final UUID writeCharacteristicUuid; |
| |
| private final UUID readCharacteristicUuid; |
| |
| private final BigInteger parsedBgServiceBitMask; |
| |
| /** |
| * 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. |
| * @param enableCompression Enable compression on outgoing messages. |
| */ |
| public CarBleCentralManager( |
| @NonNull Context context, |
| @NonNull BleCentralManager bleCentralManager, |
| @NonNull ConnectedDeviceStorage connectedDeviceStorage, |
| @NonNull UUID serviceUuid, |
| @NonNull String bgServiceMask, |
| @NonNull UUID writeCharacteristicUuid, |
| @NonNull UUID readCharacteristicUuid, |
| boolean enableCompression) { |
| super(connectedDeviceStorage, enableCompression); |
| this.context = context; |
| this.bleCentralManager = bleCentralManager; |
| this.serviceUuid = serviceUuid; |
| this.writeCharacteristicUuid = writeCharacteristicUuid; |
| this.readCharacteristicUuid = readCharacteristicUuid; |
| parsedBgServiceBitMask = new BigInteger(bgServiceMask, 16); |
| } |
| |
| @Override |
| public void start() { |
| super.start(); |
| bleCentralManager.startScanning(/* filters= */ null, scanSettings, scanCallback); |
| } |
| |
| @Override |
| public void stop() { |
| super.stop(); |
| bleCentralManager.stopScanning(); |
| } |
| |
| @Override |
| public void disconnectDevice(String deviceId) { |
| logd(TAG, "Request to disconnect from device " + deviceId + "."); |
| ConnectedRemoteDevice device = getConnectedDevice(deviceId); |
| if (device == null) { |
| return; |
| } |
| |
| deviceDisconnected(device, STATUS_FORCED_DISCONNECT); |
| } |
| |
| // TODO(b/141312136): Support car central role |
| @Override |
| public AssociationCallback getAssociationCallback() { |
| return null; |
| } |
| |
| @Override |
| public void setAssociationCallback(AssociationCallback callback) {} |
| |
| @Override |
| public void connectToDevice(UUID deviceId) {} |
| |
| @Override |
| public void initiateConnectionToDevice(UUID deviceId) {} |
| |
| @Override |
| public void startAssociation(String nameForAssociation, AssociationCallback callback) {} |
| |
| @Override |
| public void startOutOfBandAssociation( |
| String nameForAssociation, OobChannel oobChannel, AssociationCallback callback) {} |
| |
| private void ignoreDevice(@NonNull ConnectedRemoteDevice device) { |
| ignoredDevices.add(device); |
| } |
| |
| private boolean isDeviceIgnored(@NonNull BluetoothDevice device) { |
| for (ConnectedRemoteDevice connectedDevice : ignoredDevices) { |
| if (device.equals(connectedDevice.device)) { |
| 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(this.serviceUuid)) { |
| return true; |
| } |
| } |
| } |
| if (containsUuidsInOverflow(scanRecord.getBytes(), parsedBgServiceBitMask)) { |
| 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( |
| context, /* autoConnect= */ false, connectionCallback, BluetoothDevice.TRANSPORT_LE); |
| if (gatt == null) { |
| return; |
| } |
| |
| ConnectedRemoteDevice bleDevice = new ConnectedRemoteDevice(device, gatt); |
| bleDevice.state = ConnectedDeviceState.CONNECTING; |
| addConnectedDevice(bleDevice); |
| |
| // Stop scanning if we have reached the maximum number of connections. |
| if (getConnectedDevicesCount() >= MAX_CONNECTIONS) { |
| bleCentralManager.stopScanning(); |
| } |
| } |
| |
| private void deviceConnected(@NonNull ConnectedRemoteDevice device) { |
| if (device.gatt == null) { |
| loge(TAG, "Device connected with null gatt. Disconnecting."); |
| deviceDisconnected(device, BluetoothProfile.STATE_DISCONNECTED); |
| return; |
| } |
| device.state = ConnectedDeviceState.PENDING_VERIFICATION; |
| device.gatt.discoverServices(); |
| logd( |
| TAG, |
| "New device connected: " |
| + device.gatt.getDevice().getAddress() |
| + ". Active connections: " |
| + getConnectedDevicesCount() |
| + "."); |
| } |
| |
| private void deviceDisconnected(@NonNull ConnectedRemoteDevice device, int status) { |
| removeConnectedDevice(device); |
| if (device.gatt != null) { |
| device.gatt.close(); |
| } |
| if (device.deviceId != null) { |
| callbacks.invoke(callback -> callback.onDeviceDisconnected(device.deviceId)); |
| } |
| logd( |
| TAG, |
| "Device with id " |
| + device.deviceId |
| + " disconnected with state " |
| + status |
| + ". Remaining active connections: " |
| + getConnectedDevicesCount() |
| + "."); |
| } |
| |
| private final ScanCallback scanCallback = |
| 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 connectionCallback = |
| 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; |
| } |
| |
| ConnectedRemoteDevice 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; |
| } |
| |
| ConnectedRemoteDevice connectedDevice = getConnectedDevice(gatt); |
| if (connectedDevice == null) { |
| return; |
| } |
| BluetoothGattService service = gatt.getService(serviceUuid); |
| if (service == null) { |
| ignoreDevice(connectedDevice); |
| gatt.disconnect(); |
| return; |
| } |
| |
| connectedDevice.state = ConnectedDeviceState.CONNECTED; |
| BluetoothGattCharacteristic writeCharacteristic = |
| service.getCharacteristic(writeCharacteristicUuid); |
| BluetoothGattCharacteristic readCharacteristic = |
| service.getCharacteristic(readCharacteristicUuid); |
| 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. |
| } |
| }; |
| } |