blob: 645cecbf9c62aee6d5b35c5c8c674e76023f03fa [file] [log] [blame]
/*
* Copyright (C) 2019 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.connection.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 android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattServer;
import android.bluetooth.BluetoothGattServerCallback;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.le.AdvertiseCallback;
import android.bluetooth.le.AdvertiseData;
import android.bluetooth.le.AdvertiseSettings;
import android.bluetooth.le.BluetoothLeAdvertiser;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Handler;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.car.connecteddevice.util.ByteUtils;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicReference;
/**
* A generic class that manages BLE peripheral operations like start/stop advertising, notifying
* connects/disconnects and reading/writing values to GATT characteristics.
*/
// TODO(b/123248433) This could move to a separate comms library.
public class BlePeripheralManager {
private static final String TAG = "BlePeripheralManager";
private static final int BLE_RETRY_LIMIT = 5;
private static final int BLE_RETRY_INTERVAL_MS = 1000;
private static final int GATT_SERVER_RETRY_LIMIT = 20;
private static final int GATT_SERVER_RETRY_DELAY_MS = 200;
// https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth
// .service.generic_access.xml
private static final UUID GENERIC_ACCESS_PROFILE_UUID =
UUID.fromString("00001800-0000-1000-8000-00805f9b34fb");
// https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth
// .characteristic.gap.device_name.xml
private static final UUID DEVICE_NAME_UUID =
UUID.fromString("00002a00-0000-1000-8000-00805f9b34fb");
private final Handler mHandler;
private final Context mContext;
private final Set<Callback> mCallbacks = new CopyOnWriteArraySet<>();
private final Set<OnCharacteristicWriteListener> mWriteListeners = new HashSet<>();
private final Set<OnCharacteristicReadListener> mReadListeners = new HashSet<>();
private final AtomicReference<BluetoothGattServer> mGattServer = new AtomicReference<>();
private final AtomicReference<BluetoothGatt> mBluetoothGatt = new AtomicReference<>();
private int mMtuSize = 20;
private BluetoothManager mBluetoothManager;
private AtomicReference<BluetoothLeAdvertiser> mAdvertiser = new AtomicReference<>();
private int mAdvertiserStartCount;
private int mGattServerRetryStartCount;
private BluetoothGattService mBluetoothGattService;
private AdvertiseCallback mAdvertiseCallback;
private AdvertiseData mAdvertiseData;
private AdvertiseData mScanResponse;
public BlePeripheralManager(Context context) {
mContext = context;
mHandler = new Handler(mContext.getMainLooper());
}
/**
* Registers the given callback to be notified of various events within the {@link
* BlePeripheralManager}.
*
* @param callback The callback to be notified.
*/
void registerCallback(@NonNull Callback callback) {
mCallbacks.add(callback);
}
/**
* Unregisters a previously registered callback.
*
* @param callback The callback to unregister.
*/
void unregisterCallback(@NonNull Callback callback) {
mCallbacks.remove(callback);
}
/**
* Adds a listener to be notified of a write to characteristics.
*
* @param listener The listener to invoke.
*/
void addOnCharacteristicWriteListener(@NonNull OnCharacteristicWriteListener listener) {
mWriteListeners.add(listener);
}
/**
* Removes the given listener from being notified of characteristic writes.
*
* @param listener The listener to remove.
*/
void removeOnCharacteristicWriteListener(@NonNull OnCharacteristicWriteListener listener) {
mWriteListeners.remove(listener);
}
/**
* Adds a listener to be notified of reads to characteristics.
*
* @param listener The listener to invoke.
*/
void addOnCharacteristicReadListener(@NonNull OnCharacteristicReadListener listener) {
mReadListeners.add(listener);
}
/**
* Removes the given listener from being notified of characteristic reads.
*
* @param listener The listener to remove.
*/
void removeOnCharacteristicReadistener(@NonNull OnCharacteristicReadListener listener) {
mReadListeners.remove(listener);
}
/**
* Returns the current MTU size.
*
* @return The size of the MTU in bytes.
*/
int getMtuSize() {
return mMtuSize;
}
/**
* Starts the GATT server with the given {@link BluetoothGattService} and begins advertising.
*
* <p>It is possible that BLE service is still in TURNING_ON state when this method is invoked.
* Therefore, several retries will be made to ensure advertising is started.
*
* @param service {@link BluetoothGattService} that will be discovered by clients
* @param data {@link AdvertiseData} data to advertise
* @param scanResponse {@link AdvertiseData} scan response
* @param advertiseCallback {@link AdvertiseCallback} callback for advertiser
*/
void startAdvertising(
BluetoothGattService service, AdvertiseData data,
AdvertiseData scanResponse, AdvertiseCallback advertiseCallback) {
logd(TAG, "Request to start advertising with service " + service.getUuid() + ".");
if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
loge(TAG, "Attempted start advertising, but system does not support BLE. Ignoring.");
return;
}
// Clears previous session before starting advertising.
cleanup();
mBluetoothGattService = service;
mAdvertiseCallback = advertiseCallback;
mAdvertiseData = data;
mScanResponse = scanResponse;
mGattServerRetryStartCount = 0;
mBluetoothManager = (BluetoothManager) mContext.getSystemService(Context.BLUETOOTH_SERVICE);
mGattServer.set(mBluetoothManager.openGattServer(mContext, mGattServerCallback));
openGattServer();
}
/**
* Stops the GATT server from advertising.
*
* @param advertiseCallback The callback that is associated with the advertisement.
*/
void stopAdvertising(AdvertiseCallback advertiseCallback) {
BluetoothLeAdvertiser advertiser = mAdvertiser.getAndSet(null);
if (advertiser != null) {
advertiser.stopAdvertising(advertiseCallback);
logd(TAG, "Advertising stopped.");
}
}
/**
* Notifies the characteristic change via {@link BluetoothGattServer}
*/
void notifyCharacteristicChanged(
@NonNull BluetoothDevice device,
@NonNull BluetoothGattCharacteristic characteristic,
boolean confirm) {
BluetoothGattServer gattServer = mGattServer.get();
if (gattServer == null) {
return;
}
if (!gattServer.notifyCharacteristicChanged(device, characteristic, confirm)) {
loge(TAG, "notifyCharacteristicChanged failed");
}
}
/**
* Connect the Gatt server of the remote device to retrieve device name.
*/
final void retrieveDeviceName(BluetoothDevice device) {
mBluetoothGatt.compareAndSet(null, device.connectGatt(mContext, false, mGattCallback));
}
/**
* Cleans up the BLE GATT server state.
*/
void cleanup() {
logd(TAG, "Cleaning up manager.");
// Stops the advertiser, scanner and GATT server. This needs to be done to avoid leaks.
stopAdvertising(mAdvertiseCallback);
// Clears all registered listeners. IHU only supports single connection in peripheral role.
mReadListeners.clear();
mWriteListeners.clear();
BluetoothGattServer gattServer = mGattServer.getAndSet(null);
if (gattServer == null) {
return;
}
logd(TAG, "Stopping gatt server.");
BluetoothGatt bluetoothGatt = mBluetoothGatt.getAndSet(null);
if (bluetoothGatt != null) {
gattServer.cancelConnection(bluetoothGatt.getDevice());
logd(TAG, "Disconnecting gatt.");
bluetoothGatt.disconnect();
bluetoothGatt.close();
}
gattServer.clearServices();
gattServer.close();
}
private void openGattServer() {
// Only open one Gatt server.
BluetoothGattServer gattServer = mGattServer.get();
if (gattServer != null) {
logd(TAG, "Gatt Server created, retry count: " + mGattServerRetryStartCount);
gattServer.clearServices();
gattServer.addService(mBluetoothGattService);
AdvertiseSettings settings =
new AdvertiseSettings.Builder()
.setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY)
.setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH)
.setConnectable(true)
.build();
mAdvertiserStartCount = 0;
startAdvertisingInternally(settings, mAdvertiseData, mScanResponse, mAdvertiseCallback);
mGattServerRetryStartCount = 0;
} else if (mGattServerRetryStartCount < GATT_SERVER_RETRY_LIMIT) {
mGattServer.set(mBluetoothManager.openGattServer(mContext, mGattServerCallback));
mGattServerRetryStartCount++;
mHandler.postDelayed(() -> openGattServer(), GATT_SERVER_RETRY_DELAY_MS);
} else {
loge(TAG, "Gatt server not created - exceeded retry limit.");
}
}
private void startAdvertisingInternally(
AdvertiseSettings settings, AdvertiseData advertisement,
AdvertiseData scanResponse, AdvertiseCallback advertiseCallback) {
if (BluetoothAdapter.getDefaultAdapter() != null) {
mAdvertiser.compareAndSet(null,
BluetoothAdapter.getDefaultAdapter().getBluetoothLeAdvertiser());
}
BluetoothLeAdvertiser advertiser = mAdvertiser.get();
if (advertiser != null) {
logd(TAG, "Advertiser created, retry count: " + mAdvertiserStartCount);
advertiser.startAdvertising(settings, advertisement, scanResponse, advertiseCallback);
mAdvertiserStartCount = 0;
} else if (mAdvertiserStartCount < BLE_RETRY_LIMIT) {
mHandler.postDelayed(
() -> startAdvertisingInternally(settings, advertisement, scanResponse,
advertiseCallback), BLE_RETRY_INTERVAL_MS);
mAdvertiserStartCount += 1;
} else {
loge(TAG, "Cannot start BLE Advertisement. Advertise Retry count: "
+ mAdvertiserStartCount);
}
}
private final BluetoothGattServerCallback mGattServerCallback =
new BluetoothGattServerCallback() {
@Override
public void onConnectionStateChange(BluetoothDevice device, int status,
int newState) {
switch (newState) {
case BluetoothProfile.STATE_CONNECTED:
logd(TAG, "BLE Connection State Change: CONNECTED");
BluetoothGattServer gattServer = mGattServer.get();
if (gattServer == null) {
return;
}
gattServer.connect(device, /* autoConnect= */ false);
for (Callback callback : mCallbacks) {
callback.onRemoteDeviceConnected(device);
}
break;
case BluetoothProfile.STATE_DISCONNECTED:
logd(TAG, "BLE Connection State Change: DISCONNECTED");
for (Callback callback : mCallbacks) {
callback.onRemoteDeviceDisconnected(device);
}
break;
default:
logw(TAG, "Connection state not connecting or disconnecting; ignoring: "
+ newState);
}
}
@Override
public void onServiceAdded(int status, BluetoothGattService service) {
logd(TAG, "Service added status: " + status + " uuid: " + service.getUuid());
}
@Override
public void onCharacteristicWriteRequest(
BluetoothDevice device,
int requestId,
BluetoothGattCharacteristic characteristic,
boolean preparedWrite,
boolean responseNeeded,
int offset,
byte[] value) {
BluetoothGattServer gattServer = mGattServer.get();
if (gattServer == null) {
return;
}
gattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset,
value);
for (OnCharacteristicWriteListener listener : mWriteListeners) {
listener.onCharacteristicWrite(device, characteristic, value);
}
}
@Override
public void onDescriptorWriteRequest(
BluetoothDevice device,
int requestId,
BluetoothGattDescriptor descriptor,
boolean preparedWrite,
boolean responseNeeded,
int offset,
byte[] value) {
logd(TAG, "Write request for descriptor: "
+ descriptor.getUuid()
+ "; value: "
+ ByteUtils.byteArrayToHexString(value));
BluetoothGattServer gattServer = mGattServer.get();
if (gattServer == null) {
return;
}
gattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset,
value);
}
@Override
public void onMtuChanged(BluetoothDevice device, int mtu) {
logd(TAG, "onMtuChanged: " + mtu + " for device " + device.getAddress());
mMtuSize = mtu;
for (Callback callback : mCallbacks) {
callback.onMtuSizeChanged(mtu);
}
}
@Override
public void onNotificationSent(BluetoothDevice device, int status) {
super.onNotificationSent(device, status);
if (status == BluetoothGatt.GATT_SUCCESS) {
logd(TAG, "Notification sent successfully. Device: " + device.getAddress()
+ ", Status: " + status + ". Notifying all listeners.");
for (OnCharacteristicReadListener listener : mReadListeners) {
listener.onCharacteristicRead(device);
}
} else {
loge(TAG, "Notification failed. Device: " + device + ", Status: "
+ status);
}
}
};
private final BluetoothGattCallback mGattCallback =
new BluetoothGattCallback() {
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
logd(TAG, "Gatt Connection State Change: " + newState);
switch (newState) {
case BluetoothProfile.STATE_CONNECTED:
logd(TAG, "Gatt connected");
BluetoothGatt bluetoothGatt = mBluetoothGatt.get();
if (bluetoothGatt == null) {
break;
}
bluetoothGatt.discoverServices();
break;
case BluetoothProfile.STATE_DISCONNECTED:
logd(TAG, "Gatt Disconnected");
break;
default:
logd(TAG, "Connection state not connecting or disconnecting; ignoring: "
+ newState);
}
}
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
logd(TAG, "Gatt Services Discovered");
BluetoothGatt bluetoothGatt = mBluetoothGatt.get();
if (bluetoothGatt == null) {
return;
}
BluetoothGattService gapService = bluetoothGatt.getService(
GENERIC_ACCESS_PROFILE_UUID);
if (gapService == null) {
loge(TAG, "Generic Access Service is null.");
return;
}
BluetoothGattCharacteristic deviceNameCharacteristic =
gapService.getCharacteristic(DEVICE_NAME_UUID);
if (deviceNameCharacteristic == null) {
loge(TAG, "Device Name Characteristic is null.");
return;
}
bluetoothGatt.readCharacteristic(deviceNameCharacteristic);
}
@Override
public void onCharacteristicRead(
BluetoothGatt gatt, BluetoothGattCharacteristic characteristic,
int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
String deviceName = characteristic.getStringValue(0);
logd(TAG, "BLE Device Name: " + deviceName);
for (Callback callback : mCallbacks) {
callback.onDeviceNameRetrieved(deviceName);
}
} else {
loge(TAG, "Reading GAP Failed: " + status);
}
}
};
/**
* Interface to be notified of various events within the {@link BlePeripheralManager}.
*/
interface Callback {
/**
* Triggered when the name of the remote device is retrieved.
*
* @param deviceName Name of the remote device.
*/
void onDeviceNameRetrieved(@Nullable String deviceName);
/**
* Triggered if a remote client has requested to change the MTU for a given connection.
*
* @param size The new MTU size.
*/
void onMtuSizeChanged(int size);
/**
* Triggered when a device (GATT client) connected.
*
* @param device Remote device that connected on BLE.
*/
void onRemoteDeviceConnected(@NonNull BluetoothDevice device);
/**
* Triggered when a device (GATT client) disconnected.
*
* @param device Remote device that disconnected on BLE.
*/
void onRemoteDeviceDisconnected(@NonNull BluetoothDevice device);
}
/**
* An interface for classes that wish to be notified of writes to a characteristic.
*/
interface OnCharacteristicWriteListener {
/**
* Triggered when this BlePeripheralManager receives a write request from a remote device.
*
* @param device The bluetooth device that holds the characteristic.
* @param characteristic The characteristic that was written to.
* @param value The value that was written.
*/
void onCharacteristicWrite(
@NonNull BluetoothDevice device,
@NonNull BluetoothGattCharacteristic characteristic,
@NonNull byte[] value);
}
/**
* An interface for classes that wish to be notified of reads on a characteristic.
*/
interface OnCharacteristicReadListener {
/**
* Triggered when this BlePeripheralManager receives a read request from a remote device.
*
* @param device The bluetooth device that holds the characteristic.
*/
void onCharacteristicRead(@NonNull BluetoothDevice device);
}
}