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