blob: 63b9f11f1b9dac3e346a414d181fdf37a4758225 [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.trust;
import static android.bluetooth.BluetoothProfile.GATT_SERVER;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
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 android.util.Log;
import androidx.annotation.Nullable;
import com.android.car.Utils;
import java.util.UUID;
/**
* A generic class that manages BLE 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 abstract class BleManager {
private static final String TAG = BleManager.class.getSimpleName();
private static final int BLE_RETRY_LIMIT = 5;
private static final int BLE_RETRY_INTERVAL_MS = 1000;
// 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 = new Handler();
private final Context mContext;
private BluetoothManager mBluetoothManager;
private BluetoothLeAdvertiser mAdvertiser;
private BluetoothGattServer mGattServer;
private BluetoothGatt mBluetoothGatt;
private int mAdvertiserStartCount;
BleManager(Context context) {
mContext = context;
}
/**
* 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 advertiseCallback {@link AdvertiseCallback} callback for advertiser
*/
protected void startAdvertising(BluetoothGattService service, AdvertiseData data,
AdvertiseCallback advertiseCallback) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "startAdvertising: " + service.getUuid().toString());
}
if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
Log.e(TAG, "System does not support BLE");
return;
}
// Only open one Gatt server.
if (mGattServer == null) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Opening a new GATT Server");
}
mBluetoothManager = (BluetoothManager) mContext.getSystemService(
Context.BLUETOOTH_SERVICE);
mGattServer = mBluetoothManager.openGattServer(mContext, mGattServerCallback);
if (mGattServer == null) {
Log.e(TAG, "Gatt Server not created");
return;
}
}
mGattServer.clearServices();
mGattServer.addService(service);
AdvertiseSettings settings = new AdvertiseSettings.Builder()
.setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY)
.setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH)
.setConnectable(true)
.build();
mAdvertiserStartCount = 0;
startAdvertisingInternally(settings, data, advertiseCallback);
}
private void startAdvertisingInternally(AdvertiseSettings settings, AdvertiseData data,
AdvertiseCallback advertiseCallback) {
if (BluetoothAdapter.getDefaultAdapter() != null) {
mAdvertiser = BluetoothAdapter.getDefaultAdapter().getBluetoothLeAdvertiser();
}
if (mAdvertiser != null) {
mAdvertiser.startAdvertising(settings, data, advertiseCallback);
mAdvertiserStartCount = 0;
} else if (mAdvertiserStartCount < BLE_RETRY_LIMIT) {
mHandler.postDelayed(
() -> startAdvertisingInternally(settings, data, advertiseCallback),
BLE_RETRY_INTERVAL_MS);
mAdvertiserStartCount += 1;
} else {
Log.e(TAG, "Cannot start BLE Advertisement. BT Adapter: "
+ BluetoothAdapter.getDefaultAdapter() + " Advertise Retry count: "
+ mAdvertiserStartCount);
}
}
protected void stopAdvertising(AdvertiseCallback advertiseCallback) {
if (mAdvertiser != null) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "stopAdvertising: ");
}
mAdvertiser.stopAdvertising(advertiseCallback);
}
}
/**
* Notifies the characteristic change via {@link BluetoothGattServer}
*/
protected void notifyCharacteristicChanged(BluetoothDevice device,
BluetoothGattCharacteristic characteristic, boolean confirm) {
if (mGattServer != null) {
mGattServer.notifyCharacteristicChanged(device, characteristic, confirm);
}
}
/**
* Connect the Gatt server of the remote device to retrieve device name.
*/
protected final void retrieveDeviceName(BluetoothDevice device) {
mBluetoothGatt = device.connectGatt(getContext(), false, mGattCallback);
}
protected Context getContext() {
return mContext;
}
/**
* Cleans up the BLE GATT server state.
*/
void cleanup() {
// Stops the advertiser and GATT server. This needs to be done to avoid leaks
if (mAdvertiser != null) {
mAdvertiser.cleanup();
}
if (mGattServer != null) {
mGattServer.clearServices();
try {
for (BluetoothDevice d : mBluetoothManager.getConnectedDevices(GATT_SERVER)) {
mGattServer.cancelConnection(d);
}
} catch (UnsupportedOperationException e) {
Log.e(TAG, "Error getting connected devices", e);
} finally {
stopGattServer();
}
}
}
/**
* Close the GATT Server
*/
void stopGattServer() {
if (mGattServer == null) {
return;
}
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "stopGattServer");
}
if (mBluetoothGatt != null) {
mBluetoothGatt.disconnect();
}
mGattServer.close();
mGattServer = null;
}
/**
* Triggered when the name of the remote device is retrieved.
*
* @param deviceName Name of the remote device.
*/
protected 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.
*/
protected void onMtuSizeChanged(int size) {
}
/**
* Triggered when a device (GATT client) connected.
*
* @param device Remote device that connected on BLE.
*/
protected void onRemoteDeviceConnected(BluetoothDevice device) {
}
/**
* Triggered when a device (GATT client) disconnected.
*
* @param device Remote device that disconnected on BLE.
*/
protected void onRemoteDeviceDisconnected(BluetoothDevice device) {
}
/**
* Triggered when this BleManager receives a write request from a remote
* device. Sub-classes should implement how to handle requests.
* <p>
*
* @see BluetoothGattServerCallback#onCharacteristicWriteRequest(BluetoothDevice, int,
* BluetoothGattCharacteristic, boolean, boolean, int, byte[])
*/
protected abstract void onCharacteristicWrite(BluetoothDevice device, int requestId,
BluetoothGattCharacteristic characteristic, boolean preparedWrite, boolean
responseNeeded, int offset, byte[] value);
/**
* Triggered when this BleManager receives a read request from a remote device.
* <p>
*
* @see BluetoothGattServerCallback#onCharacteristicReadRequest(BluetoothDevice, int, int,
* BluetoothGattCharacteristic)
*/
protected abstract void onCharacteristicRead(BluetoothDevice device,
int requestId, int offset, BluetoothGattCharacteristic characteristic);
private final BluetoothGattServerCallback mGattServerCallback =
new BluetoothGattServerCallback() {
@Override
public void onConnectionStateChange(BluetoothDevice device, int status,
int newState) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "BLE Connection State Change: " + newState);
}
switch (newState) {
case BluetoothProfile.STATE_CONNECTED:
onRemoteDeviceConnected(device);
break;
case BluetoothProfile.STATE_DISCONNECTED:
onRemoteDeviceDisconnected(device);
break;
default:
Log.w(TAG,
"Connection state not connecting or disconnecting; ignoring: "
+ newState);
}
}
@Override
public void onServiceAdded(int status, BluetoothGattService service) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG,
"Service added status: " + status + " uuid: " + service.getUuid());
}
}
@Override
public void onCharacteristicReadRequest(BluetoothDevice device, int requestId,
int offset, BluetoothGattCharacteristic characteristic) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Read request for characteristic: " + characteristic.getUuid());
}
mGattServer.sendResponse(device, requestId,
BluetoothGatt.GATT_SUCCESS, offset, characteristic.getValue());
onCharacteristicRead(device, requestId, offset, characteristic);
}
@Override
public void onCharacteristicWriteRequest(BluetoothDevice device, int requestId,
BluetoothGattCharacteristic characteristic, boolean preparedWrite,
boolean responseNeeded, int offset, byte[] value) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Write request for characteristic: " + characteristic.getUuid()
+ "value: " + Utils.byteArrayToHexString(value));
}
mGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS,
offset, value);
onCharacteristicWrite(device, requestId, characteristic,
preparedWrite, responseNeeded, offset, value);
}
@Override
public void onMtuChanged(BluetoothDevice device, int mtu) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onMtuChanged: " + mtu + " for device " + device.getAddress());
}
onMtuSizeChanged(mtu);
}
};
private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Gatt Connection State Change: " + newState);
}
switch (newState) {
case BluetoothProfile.STATE_CONNECTED:
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Gatt connected");
}
mBluetoothGatt.discoverServices();
break;
case BluetoothProfile.STATE_DISCONNECTED:
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Gatt Disconnected");
}
break;
default:
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG,
"Connection state not connecting or disconnecting; ignoring: "
+ newState);
}
}
}
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Gatt Services Discovered");
}
BluetoothGattService gapService = mBluetoothGatt.getService(
GENERIC_ACCESS_PROFILE_UUID);
if (gapService == null) {
Log.e(TAG, "Generic Access Service is Null");
return;
}
BluetoothGattCharacteristic deviceNameCharacteristic = gapService.getCharacteristic(
DEVICE_NAME_UUID);
if (deviceNameCharacteristic == null) {
Log.e(TAG, "Device Name Characteristic is Null");
return;
}
mBluetoothGatt.readCharacteristic(deviceNameCharacteristic);
}
@Override
public void onCharacteristicRead(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic, int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
String deviceName = characteristic.getStringValue(0);
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "BLE Device Name: " + deviceName);
}
onDeviceNameRetrieved(deviceName);
} else {
Log.e(TAG, "Reading GAP Failed: " + status);
}
}
};
}