1 /*
2  * Copyright (C) 2019 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 
23 import android.annotation.NonNull;
24 import android.annotation.Nullable;
25 import android.bluetooth.BluetoothAdapter;
26 import android.bluetooth.BluetoothDevice;
27 import android.bluetooth.BluetoothGatt;
28 import android.bluetooth.BluetoothGattCallback;
29 import android.bluetooth.BluetoothGattCharacteristic;
30 import android.bluetooth.BluetoothGattDescriptor;
31 import android.bluetooth.BluetoothGattServer;
32 import android.bluetooth.BluetoothGattServerCallback;
33 import android.bluetooth.BluetoothGattService;
34 import android.bluetooth.BluetoothManager;
35 import android.bluetooth.BluetoothProfile;
36 import android.bluetooth.le.AdvertiseCallback;
37 import android.bluetooth.le.AdvertiseData;
38 import android.bluetooth.le.AdvertiseSettings;
39 import android.bluetooth.le.BluetoothLeAdvertiser;
40 import android.content.Context;
41 import android.content.pm.PackageManager;
42 import android.os.Handler;
43 
44 import com.android.car.connecteddevice.util.ByteUtils;
45 
46 import java.util.HashSet;
47 import java.util.Set;
48 import java.util.UUID;
49 import java.util.concurrent.CopyOnWriteArraySet;
50 import java.util.concurrent.atomic.AtomicReference;
51 
52 /**
53  * A generic class that manages BLE peripheral operations like start/stop advertising, notifying
54  * connects/disconnects and reading/writing values to GATT characteristics.
55  */
56 // TODO(b/123248433) This could move to a separate comms library.
57 public class BlePeripheralManager {
58     private static final String TAG = "BlePeripheralManager";
59 
60     private static final int BLE_RETRY_LIMIT = 5;
61     private static final int BLE_RETRY_INTERVAL_MS = 1000;
62 
63     private static final int GATT_SERVER_RETRY_LIMIT = 20;
64     private static final int GATT_SERVER_RETRY_DELAY_MS = 200;
65 
66     // https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth
67     // .service.generic_access.xml
68     private static final UUID GENERIC_ACCESS_PROFILE_UUID =
69             UUID.fromString("00001800-0000-1000-8000-00805f9b34fb");
70     // https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth
71     // .characteristic.gap.device_name.xml
72     private static final UUID DEVICE_NAME_UUID =
73             UUID.fromString("00002a00-0000-1000-8000-00805f9b34fb");
74 
75     private final Handler mHandler;
76 
77     private final Context mContext;
78     private final Set<Callback> mCallbacks = new CopyOnWriteArraySet<>();
79     private final Set<OnCharacteristicWriteListener> mWriteListeners = new HashSet<>();
80     private final Set<OnCharacteristicReadListener> mReadListeners = new HashSet<>();
81     private final AtomicReference<BluetoothGattServer> mGattServer = new AtomicReference<>();
82     private final AtomicReference<BluetoothGatt> mBluetoothGatt = new AtomicReference<>();
83 
84     private int mMtuSize = 20;
85 
86     private BluetoothManager mBluetoothManager;
87     private BluetoothLeAdvertiser mAdvertiser;
88     private int mAdvertiserStartCount;
89     private int mGattServerRetryStartCount;
90     private BluetoothGattService mBluetoothGattService;
91     private AdvertiseCallback mAdvertiseCallback;
92     private AdvertiseData mAdvertiseData;
93 
BlePeripheralManager(Context context)94     public BlePeripheralManager(Context context) {
95         mContext = context;
96         mHandler = new Handler(mContext.getMainLooper());
97     }
98 
99     /**
100      * Registers the given callback to be notified of various events within the {@link
101      * BlePeripheralManager}.
102      *
103      * @param callback The callback to be notified.
104      */
registerCallback(@onNull Callback callback)105     void registerCallback(@NonNull Callback callback) {
106         mCallbacks.add(callback);
107     }
108 
109     /**
110      * Unregisters a previously registered callback.
111      *
112      * @param callback The callback to unregister.
113      */
unregisterCallback(@onNull Callback callback)114     void unregisterCallback(@NonNull Callback callback) {
115         mCallbacks.remove(callback);
116     }
117 
118     /**
119      * Adds a listener to be notified of a write to characteristics.
120      *
121      * @param listener The listener to invoke.
122      */
addOnCharacteristicWriteListener(@onNull OnCharacteristicWriteListener listener)123     void addOnCharacteristicWriteListener(@NonNull OnCharacteristicWriteListener listener) {
124         mWriteListeners.add(listener);
125     }
126 
127     /**
128      * Removes the given listener from being notified of characteristic writes.
129      *
130      * @param listener The listener to remove.
131      */
removeOnCharacteristicWriteListener(@onNull OnCharacteristicWriteListener listener)132     void removeOnCharacteristicWriteListener(@NonNull OnCharacteristicWriteListener listener) {
133         mWriteListeners.remove(listener);
134     }
135 
136     /**
137      * Adds a listener to be notified of reads to characteristics.
138      *
139      * @param listener The listener to invoke.
140      */
addOnCharacteristicReadListener(@onNull OnCharacteristicReadListener listener)141     void addOnCharacteristicReadListener(@NonNull OnCharacteristicReadListener listener) {
142         mReadListeners.add(listener);
143     }
144 
145     /**
146      * Removes the given listener from being notified of characteristic reads.
147      *
148      * @param listener The listener to remove.
149      */
removeOnCharacteristicReadistener(@onNull OnCharacteristicReadListener listener)150     void removeOnCharacteristicReadistener(@NonNull OnCharacteristicReadListener listener) {
151         mReadListeners.remove(listener);
152     }
153 
154     /**
155      * Returns the current MTU size.
156      *
157      * @return The size of the MTU in bytes.
158      */
getMtuSize()159     int getMtuSize() {
160         return mMtuSize;
161     }
162 
163     /**
164      * Starts the GATT server with the given {@link BluetoothGattService} and begins advertising.
165      *
166      * <p>It is possible that BLE service is still in TURNING_ON state when this method is invoked.
167      * Therefore, several retries will be made to ensure advertising is started.
168      *
169      * @param service           {@link BluetoothGattService} that will be discovered by clients
170      * @param data              {@link AdvertiseData} data to advertise
171      * @param advertiseCallback {@link AdvertiseCallback} callback for advertiser
172      */
startAdvertising( BluetoothGattService service, AdvertiseData data, AdvertiseCallback advertiseCallback)173     void startAdvertising(
174             BluetoothGattService service, AdvertiseData data, AdvertiseCallback advertiseCallback) {
175         logd(TAG, "startAdvertising: " + service.getUuid());
176         if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
177             loge(TAG, "Attempted start advertising, but system does not support BLE. Ignoring.");
178             return;
179         }
180         // Clears previous session before starting advertising.
181         cleanup();
182         mBluetoothGattService = service;
183         mAdvertiseCallback = advertiseCallback;
184         mAdvertiseData = data;
185         mGattServerRetryStartCount = 0;
186         mBluetoothManager = (BluetoothManager) mContext.getSystemService(Context.BLUETOOTH_SERVICE);
187         mGattServer.set(mBluetoothManager.openGattServer(mContext, mGattServerCallback));
188         openGattServer();
189     }
190 
191     /**
192      * Stops the GATT server from advertising.
193      *
194      * @param advertiseCallback The callback that is associated with the advertisement.
195      */
stopAdvertising(AdvertiseCallback advertiseCallback)196     void stopAdvertising(AdvertiseCallback advertiseCallback) {
197         if (mAdvertiser != null) {
198             logd(TAG, "Stop advertising.");
199             mAdvertiser.stopAdvertising(advertiseCallback);
200         }
201     }
202 
203     /**
204      * Notifies the characteristic change via {@link BluetoothGattServer}
205      */
notifyCharacteristicChanged( @onNull BluetoothDevice device, @NonNull BluetoothGattCharacteristic characteristic, boolean confirm)206     void notifyCharacteristicChanged(
207             @NonNull BluetoothDevice device,
208             @NonNull BluetoothGattCharacteristic characteristic,
209             boolean confirm) {
210         BluetoothGattServer gattServer = mGattServer.get();
211         if (gattServer == null) {
212             return;
213         }
214 
215         if (!gattServer.notifyCharacteristicChanged(device, characteristic, confirm)) {
216             loge(TAG, "notifyCharacteristicChanged failed");
217         }
218     }
219 
220     /**
221      * Connect the Gatt server of the remote device to retrieve device name.
222      */
retrieveDeviceName(BluetoothDevice device)223     final void retrieveDeviceName(BluetoothDevice device) {
224         mBluetoothGatt.compareAndSet(null, device.connectGatt(mContext, false, mGattCallback));
225     }
226 
227     /**
228      * Cleans up the BLE GATT server state.
229      */
cleanup()230     void cleanup() {
231         // Stops the advertiser, scanner and GATT server. This needs to be done to avoid leaks.
232         if (mAdvertiser != null) {
233             mAdvertiser.stopAdvertising(mAdvertiseCallback);
234         }
235         // Clears all registered listeners. IHU only supports single connection in peripheral role.
236         mReadListeners.clear();
237         mWriteListeners.clear();
238         mAdvertiser = null;
239 
240         BluetoothGattServer gattServer = mGattServer.getAndSet(null);
241         if (gattServer == null) {
242             return;
243         }
244 
245         logd(TAG, "stopGattServer");
246         BluetoothGatt bluetoothGatt = mBluetoothGatt.getAndSet(null);
247         if (bluetoothGatt != null) {
248             gattServer.cancelConnection(bluetoothGatt.getDevice());
249             bluetoothGatt.disconnect();
250         }
251         gattServer.clearServices();
252         gattServer.close();
253     }
254 
openGattServer()255     private void openGattServer() {
256         // Only open one Gatt server.
257         BluetoothGattServer gattServer = mGattServer.get();
258         if (gattServer != null) {
259             logd(TAG, "Gatt Server created, retry count: " + mGattServerRetryStartCount);
260             gattServer.clearServices();
261             gattServer.addService(mBluetoothGattService);
262             AdvertiseSettings settings =
263                     new AdvertiseSettings.Builder()
264                             .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY)
265                             .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH)
266                             .setConnectable(true)
267                             .build();
268             mAdvertiserStartCount = 0;
269             startAdvertisingInternally(settings, mAdvertiseData, mAdvertiseCallback);
270             mGattServerRetryStartCount = 0;
271         } else if (mGattServerRetryStartCount < GATT_SERVER_RETRY_LIMIT) {
272             mGattServer.set(mBluetoothManager.openGattServer(mContext, mGattServerCallback));
273             mGattServerRetryStartCount++;
274             mHandler.postDelayed(() -> openGattServer(), GATT_SERVER_RETRY_DELAY_MS);
275         } else {
276             loge(TAG, "Gatt server not created - exceeded retry limit.");
277         }
278     }
279 
startAdvertisingInternally( AdvertiseSettings settings, AdvertiseData data, AdvertiseCallback advertiseCallback)280     private void startAdvertisingInternally(
281             AdvertiseSettings settings, AdvertiseData data, AdvertiseCallback advertiseCallback) {
282         if (BluetoothAdapter.getDefaultAdapter() != null) {
283             mAdvertiser = BluetoothAdapter.getDefaultAdapter().getBluetoothLeAdvertiser();
284         }
285 
286         if (mAdvertiser != null) {
287             logd(TAG, "Advertiser created, retry count: " + mAdvertiserStartCount);
288             mAdvertiser.startAdvertising(settings, data, advertiseCallback);
289             mAdvertiserStartCount = 0;
290         } else if (mAdvertiserStartCount < BLE_RETRY_LIMIT) {
291             mHandler.postDelayed(
292                     () -> startAdvertisingInternally(settings, data, advertiseCallback),
293                     BLE_RETRY_INTERVAL_MS);
294             mAdvertiserStartCount += 1;
295         } else {
296             loge(TAG, "Cannot start BLE Advertisement. Advertise Retry count: "
297                             + mAdvertiserStartCount);
298         }
299     }
300 
301     private final BluetoothGattServerCallback mGattServerCallback =
302             new BluetoothGattServerCallback() {
303                 @Override
304                 public void onConnectionStateChange(BluetoothDevice device, int status,
305                                                     int newState) {
306                     logd(TAG, "BLE Connection State Change: " + newState);
307                     switch (newState) {
308                         case BluetoothProfile.STATE_CONNECTED:
309                             for (Callback callback : mCallbacks) {
310                                 callback.onRemoteDeviceConnected(device);
311                             }
312                             break;
313                         case BluetoothProfile.STATE_DISCONNECTED:
314                             for (Callback callback : mCallbacks) {
315                                 callback.onRemoteDeviceDisconnected(device);
316                             }
317                             break;
318                         default:
319                             logw(TAG, "Connection state not connecting or disconnecting; ignoring: "
320                                     + newState);
321                     }
322                 }
323 
324                 @Override
325                 public void onServiceAdded(int status, BluetoothGattService service) {
326                     logd(TAG, "Service added status: " + status + " uuid: " + service.getUuid());
327                 }
328 
329                 @Override
330                 public void onCharacteristicWriteRequest(
331                         BluetoothDevice device,
332                         int requestId,
333                         BluetoothGattCharacteristic characteristic,
334                         boolean preparedWrite,
335                         boolean responseNeeded,
336                         int offset,
337                         byte[] value) {
338                     BluetoothGattServer gattServer = mGattServer.get();
339                     if (gattServer == null) {
340                         return;
341                     }
342                     gattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset,
343                             value);
344                     for (OnCharacteristicWriteListener listener : mWriteListeners) {
345                         listener.onCharacteristicWrite(device, characteristic, value);
346                     }
347                 }
348 
349                 @Override
350                 public void onDescriptorWriteRequest(
351                         BluetoothDevice device,
352                         int requestId,
353                         BluetoothGattDescriptor descriptor,
354                         boolean preparedWrite,
355                         boolean responseNeeded,
356                         int offset,
357                         byte[] value) {
358                     logd(TAG, "Write request for descriptor: "
359                             + descriptor.getUuid()
360                             + "; value: "
361                             + ByteUtils.byteArrayToHexString(value));
362                     BluetoothGattServer gattServer = mGattServer.get();
363                     if (gattServer == null) {
364                         return;
365                     }
366                     gattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset,
367                             value);
368                 }
369 
370                 @Override
371                 public void onMtuChanged(BluetoothDevice device, int mtu) {
372                     logd(TAG, "onMtuChanged: " + mtu + " for device " + device.getAddress());
373 
374                     mMtuSize = mtu;
375 
376                     for (Callback callback : mCallbacks) {
377                         callback.onMtuSizeChanged(mtu);
378                     }
379                 }
380 
381                 @Override
382                 public void onNotificationSent(BluetoothDevice device, int status) {
383                     super.onNotificationSent(device, status);
384                     if (status == BluetoothGatt.GATT_SUCCESS) {
385                         logd(TAG, "Notification sent successfully. Device: " + device.getAddress()
386                                 + ", Status: " + status + ". Notifying all listeners.");
387                         for (OnCharacteristicReadListener listener : mReadListeners) {
388                             listener.onCharacteristicRead(device);
389                         }
390                     } else {
391                         loge(TAG, "Notification failed. Device: " + device + ", Status: "
392                                 + status);
393                     }
394                 }
395             };
396 
397     private final BluetoothGattCallback mGattCallback =
398             new BluetoothGattCallback() {
399                 @Override
400                 public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
401                     logd(TAG, "Gatt Connection State Change: " + newState);
402                     switch (newState) {
403                         case BluetoothProfile.STATE_CONNECTED:
404                             logd(TAG, "Gatt connected");
405                             BluetoothGatt bluetoothGatt = mBluetoothGatt.get();
406                             if (bluetoothGatt == null) {
407                                 break;
408                             }
409                             bluetoothGatt.discoverServices();
410                             break;
411                         case BluetoothProfile.STATE_DISCONNECTED:
412                             logd(TAG, "Gatt Disconnected");
413                             break;
414                         default:
415                             logd(TAG, "Connection state not connecting or disconnecting; ignoring: "
416                                     + newState);
417                     }
418                 }
419 
420                 @Override
421                 public void onServicesDiscovered(BluetoothGatt gatt, int status) {
422                     logd(TAG, "Gatt Services Discovered");
423                     BluetoothGatt bluetoothGatt = mBluetoothGatt.get();
424                     if (bluetoothGatt == null) {
425                         return;
426                     }
427                     BluetoothGattService gapService = bluetoothGatt.getService(
428                             GENERIC_ACCESS_PROFILE_UUID);
429                     if (gapService == null) {
430                         loge(TAG, "Generic Access Service is null.");
431                         return;
432                     }
433                     BluetoothGattCharacteristic deviceNameCharacteristic =
434                             gapService.getCharacteristic(DEVICE_NAME_UUID);
435                     if (deviceNameCharacteristic == null) {
436                         loge(TAG, "Device Name Characteristic is null.");
437                         return;
438                     }
439                     bluetoothGatt.readCharacteristic(deviceNameCharacteristic);
440                 }
441 
442                 @Override
443                 public void onCharacteristicRead(
444                         BluetoothGatt gatt, BluetoothGattCharacteristic characteristic,
445                         int status) {
446                     if (status == BluetoothGatt.GATT_SUCCESS) {
447                         String deviceName = characteristic.getStringValue(0);
448                         logd(TAG, "BLE Device Name: " + deviceName);
449 
450                         for (Callback callback : mCallbacks) {
451                             callback.onDeviceNameRetrieved(deviceName);
452                         }
453                     } else {
454                         loge(TAG, "Reading GAP Failed: " + status);
455                     }
456                 }
457             };
458 
459     /**
460      * Interface to be notified of various events within the {@link BlePeripheralManager}.
461      */
462     interface Callback {
463         /**
464          * Triggered when the name of the remote device is retrieved.
465          *
466          * @param deviceName Name of the remote device.
467          */
onDeviceNameRetrieved(@ullable String deviceName)468         void onDeviceNameRetrieved(@Nullable String deviceName);
469 
470         /**
471          * Triggered if a remote client has requested to change the MTU for a given connection.
472          *
473          * @param size The new MTU size.
474          */
onMtuSizeChanged(int size)475         void onMtuSizeChanged(int size);
476 
477         /**
478          * Triggered when a device (GATT client) connected.
479          *
480          * @param device Remote device that connected on BLE.
481          */
onRemoteDeviceConnected(@onNull BluetoothDevice device)482         void onRemoteDeviceConnected(@NonNull BluetoothDevice device);
483 
484         /**
485          * Triggered when a device (GATT client) disconnected.
486          *
487          * @param device Remote device that disconnected on BLE.
488          */
onRemoteDeviceDisconnected(@onNull BluetoothDevice device)489         void onRemoteDeviceDisconnected(@NonNull BluetoothDevice device);
490     }
491 
492     /**
493      * An interface for classes that wish to be notified of writes to a characteristic.
494      */
495     interface OnCharacteristicWriteListener {
496         /**
497          * Triggered when this BlePeripheralManager receives a write request from a remote device.
498          *
499          * @param device         The bluetooth device that holds the characteristic.
500          * @param characteristic The characteristic that was written to.
501          * @param value          The value that was written.
502          */
onCharacteristicWrite( @onNull BluetoothDevice device, @NonNull BluetoothGattCharacteristic characteristic, @NonNull byte[] value)503         void onCharacteristicWrite(
504                 @NonNull BluetoothDevice device,
505                 @NonNull BluetoothGattCharacteristic characteristic,
506                 @NonNull byte[] value);
507     }
508 
509     /**
510      * An interface for classes that wish to be notified of reads on a characteristic.
511      */
512     interface OnCharacteristicReadListener {
513         /**
514          * Triggered when this BlePeripheralManager receives a read request from a remote device.
515          *
516          * @param device The bluetooth device that holds the characteristic.
517          */
onCharacteristicRead(@onNull BluetoothDevice device)518         void onCharacteristicRead(@NonNull BluetoothDevice device);
519     }
520 }
521