blob: 9339e1400a5bb089adaa310136b16179733cb58e [file] [log] [blame]
/*
* Copyright (C) 2022 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 android.nearby.fastpair.provider.bluetooth;
import android.annotation.TargetApi;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.os.Build.VERSION_CODES;
import android.util.Log;
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.nearby.common.bluetooth.BluetoothException;
import com.android.server.nearby.common.bluetooth.BluetoothGattException;
import com.android.server.nearby.common.bluetooth.testability.VersionProvider;
import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothDevice;
import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothGattServer;
import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothGattServerCallback;
import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor;
import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.Operation;
import com.google.common.base.Preconditions;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
/**
* Helper for simplifying operations on {@link BluetoothGattServer}.
*/
@TargetApi(18)
public class BluetoothGattServerHelper {
private static final String TAG = BluetoothGattServerHelper.class.getSimpleName();
@VisibleForTesting
static final long OPERATION_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(1);
private static final int MAX_PARALLEL_OPERATIONS = 5;
/** BT operation types that can be in flight. */
public enum OperationType {
ADD_SERVICE,
CLOSE_CONNECTION,
START_ADVERTISING
}
private final Object mOperationLock = new Object();
@VisibleForTesting
final BluetoothGattServerCallback mGattServerCallback =
new GattServerCallback();
@VisibleForTesting
BluetoothOperationExecutor mBluetoothOperationScheduler =
new BluetoothOperationExecutor(MAX_PARALLEL_OPERATIONS);
private final Context mContext;
private final BluetoothManager mBluetoothManager;
private final VersionProvider mVersionProvider;
@Nullable
@VisibleForTesting
volatile BluetoothGattServerConfig mServerConfig = null;
@Nullable
@VisibleForTesting
volatile BluetoothGattServer mBluetoothGattServer = null;
@VisibleForTesting
final ConcurrentMap<BluetoothDevice, BluetoothGattServerConnection>
mConnections = new ConcurrentHashMap<BluetoothDevice, BluetoothGattServerConnection>();
public BluetoothGattServerHelper(Context context, BluetoothManager bluetoothManager) {
this(
Preconditions.checkNotNull(context),
Preconditions.checkNotNull(bluetoothManager),
new VersionProvider()
);
}
@VisibleForTesting
BluetoothGattServerHelper(
Context context, BluetoothManager bluetoothManager, VersionProvider versionProvider) {
mContext = context;
mBluetoothManager = bluetoothManager;
mVersionProvider = versionProvider;
}
@Nullable
public BluetoothGattServerConfig getConfig() {
return mServerConfig;
}
public void open(final BluetoothGattServerConfig gattServerConfig) throws BluetoothException {
synchronized (mOperationLock) {
Preconditions.checkState(mBluetoothGattServer == null, "Gatt server is already open.");
final BluetoothGattServer server =
mBluetoothManager.openGattServer(mContext, mGattServerCallback);
if (server == null) {
throw new BluetoothException(
"Failed to open the GATT server, openGattServer returned null.");
}
try {
for (final BluetoothGattService service :
gattServerConfig.getBluetoothGattServices()) {
if (service == null) {
continue;
}
mBluetoothOperationScheduler.execute(
new Operation<Void>(OperationType.ADD_SERVICE, service) {
@Override
public void run() throws BluetoothException {
boolean success = server.addService(service);
if (!success) {
throw new BluetoothException("Fails on adding service");
}
}
}, OPERATION_TIMEOUT_MILLIS);
}
mBluetoothGattServer = server;
mServerConfig = gattServerConfig;
} catch (BluetoothException e) {
server.close();
throw e;
}
}
}
public boolean isOpen() {
synchronized (mOperationLock) {
return mBluetoothGattServer != null;
}
}
public void close() {
synchronized (mOperationLock) {
BluetoothGattServer bluetoothGattServer = mBluetoothGattServer;
if (bluetoothGattServer == null) {
return;
}
bluetoothGattServer.close();
mBluetoothGattServer = null;
}
}
private BluetoothGattServerConnection getConnectionByDevice(BluetoothDevice device)
throws BluetoothGattException {
BluetoothGattServerConnection bluetoothLeConnection = mConnections.get(device);
if (bluetoothLeConnection == null) {
throw new BluetoothGattException(
String.format("Received operation on an unknown device: %s", device),
BluetoothGatt.GATT_FAILURE);
}
return bluetoothLeConnection;
}
public void sendNotification(
BluetoothDevice device,
BluetoothGattCharacteristic characteristic,
byte[] data,
boolean confirm)
throws BluetoothException {
Log.d(TAG, String.format("Sending a %s of %d bytes on characteristics %s on device %s.",
confirm ? "indication" : "notification",
data.length,
characteristic.getUuid(),
device));
synchronized (mOperationLock) {
BluetoothGattServer bluetoothGattServer = mBluetoothGattServer;
if (bluetoothGattServer == null) {
throw new BluetoothException("Server is not open.");
}
BluetoothGattCharacteristic clonedCharacteristic =
BluetoothGattUtils.clone(characteristic);
clonedCharacteristic.setValue(data);
bluetoothGattServer.notifyCharacteristicChanged(device, clonedCharacteristic, confirm);
}
}
public void closeConnection(final BluetoothDevice bluetoothDevice) throws BluetoothException {
final BluetoothGattServer bluetoothGattServer = mBluetoothGattServer;
if (bluetoothGattServer == null) {
throw new BluetoothException("Server is not open.");
}
int connectionSate =
mBluetoothManager.getConnectionState(bluetoothDevice, BluetoothProfile.GATT);
if (connectionSate != BluetoothGatt.STATE_CONNECTED) {
return;
}
mBluetoothOperationScheduler.execute(
new Operation<Void>(OperationType.CLOSE_CONNECTION) {
@Override
public void run() throws BluetoothException {
bluetoothGattServer.cancelConnection(bluetoothDevice);
}
},
OPERATION_TIMEOUT_MILLIS);
}
private class GattServerCallback extends BluetoothGattServerCallback {
@Override
public void onServiceAdded(int status, BluetoothGattService service) {
mBluetoothOperationScheduler.notifyCompletion(
new Operation<Void>(OperationType.ADD_SERVICE, service), status);
}
@Override
public void onConnectionStateChange(BluetoothDevice device, int status, int newState) {
BluetoothGattServerConfig serverConfig = mServerConfig;
BluetoothGattServer bluetoothGattServer = mBluetoothGattServer;
BluetoothGattServerConnection bluetoothLeConnection;
if (serverConfig == null || bluetoothGattServer == null) {
return;
}
switch (newState) {
case BluetoothGattServer.STATE_CONNECTED:
if (status != BluetoothGatt.GATT_SUCCESS) {
Log.e(TAG, String.format("Connection to %s failed: %s", device,
BluetoothGattUtils.getMessageForStatusCode(status)));
return;
}
Log.i(TAG, String.format("Connected to device %s.", device));
if (mConnections.containsKey(device)) {
Log.w(TAG, String.format("A connection is already open with device %s. "
+ "Keeping existing one.", device));
return;
}
BluetoothGattServerConnection connection = new BluetoothGattServerConnection(
BluetoothGattServerHelper.this,
device,
serverConfig);
if (serverConfig.getServerListener() != null) {
serverConfig.getServerListener().onConnection(connection);
}
mConnections.put(device, connection);
// By default, Android disconnects active GATT server connection if the
// advertisement is
// stop (or sometime stopScanning also disconnect, see b/62667394). Asking
// the server to
// reverse connect will tell Android to keep the connection open.
// Code handling connect() on Android OS is: btif_gatt_server.c
// Note: for Android < P, unknown type devices don't connect. See b/62827460.
// for Android P+, unknown type devices always use LE to connect (see
// code)
// Note: for Android < N, dual mode devices always connect using BT classic,
// so connect()
// should *NOT* be called for those devices. See b/29819614.
if (mVersionProvider.getSdkInt() >= VERSION_CODES.N
|| device.getType() != BluetoothDevice.DEVICE_TYPE_DUAL) {
boolean success = bluetoothGattServer.connect(device, /* autoConnect */
false);
if (!success) {
Log.w(TAG, String.format(
"Keeping connection open on stop advertising failed for "
+ "device %s.",
device));
}
}
break;
case BluetoothGattServer.STATE_DISCONNECTED:
if (status != BluetoothGatt.GATT_SUCCESS) {
Log.w(TAG, String.format(
"Disconnection from %s error: %s. Proceeding anyway.",
device, BluetoothGattUtils.getMessageForStatusCode(status)));
}
bluetoothLeConnection = mConnections.remove(device);
if (bluetoothLeConnection != null) {
// Disconnect the server, required after connecting to it.
bluetoothGattServer.cancelConnection(device);
bluetoothLeConnection.onClose();
}
mBluetoothOperationScheduler.notifyCompletion(
new Operation<Void>(OperationType.CLOSE_CONNECTION), status);
break;
default:
Log.e(TAG, String.format("Unexpected connection state: %d", newState));
}
}
@Override
public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset,
BluetoothGattCharacteristic characteristic) {
BluetoothGattServer bluetoothGattServer = mBluetoothGattServer;
if (bluetoothGattServer == null) {
return;
}
try {
byte[] value =
getConnectionByDevice(device).readCharacteristic(offset, characteristic);
bluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS,
offset,
value);
} catch (BluetoothGattException e) {
Log.e(TAG,
String.format(
"Could not read %s on device %s at offset %d",
BluetoothGattUtils.toString(characteristic),
device,
offset),
e);
bluetoothGattServer.sendResponse(
device, requestId, e.getGattErrorCode(), offset, null);
}
}
@Override
public void onCharacteristicWriteRequest(BluetoothDevice device,
int requestId,
BluetoothGattCharacteristic characteristic,
boolean preparedWrite,
boolean responseNeeded,
int offset,
byte[] value) {
BluetoothGattServer bluetoothGattServer = mBluetoothGattServer;
if (bluetoothGattServer == null) {
return;
}
try {
getConnectionByDevice(device).writeCharacteristic(characteristic,
preparedWrite,
offset,
value);
if (responseNeeded) {
bluetoothGattServer.sendResponse(
device, requestId, BluetoothGatt.GATT_SUCCESS, offset, null);
}
} catch (BluetoothGattException e) {
Log.e(TAG,
String.format(
"Could not write %s on device %s at offset %d",
BluetoothGattUtils.toString(characteristic),
device,
offset),
e);
bluetoothGattServer.sendResponse(
device, requestId, e.getGattErrorCode(), offset, null);
}
}
@Override
public void onDescriptorReadRequest(BluetoothDevice device, int requestId, int offset,
BluetoothGattDescriptor descriptor) {
BluetoothGattServer bluetoothGattServer = mBluetoothGattServer;
if (bluetoothGattServer == null) {
return;
}
try {
byte[] value = getConnectionByDevice(device).readDescriptor(offset, descriptor);
bluetoothGattServer.sendResponse(
device, requestId, BluetoothGatt.GATT_SUCCESS, offset, value);
} catch (BluetoothGattException e) {
Log.e(TAG, String.format(
"Could not read %s on device %s at %d",
BluetoothGattUtils.toString(descriptor),
device,
offset),
e);
bluetoothGattServer.sendResponse(
device, requestId, e.getGattErrorCode(), offset, null);
}
}
@Override
public void onDescriptorWriteRequest(BluetoothDevice device,
int requestId,
BluetoothGattDescriptor descriptor,
boolean preparedWrite,
boolean responseNeeded,
int offset,
byte[] value) {
BluetoothGattServer bluetoothGattServer = mBluetoothGattServer;
if (bluetoothGattServer == null) {
return;
}
try {
getConnectionByDevice(device)
.writeDescriptor(descriptor, preparedWrite, offset, value);
if (responseNeeded) {
bluetoothGattServer.sendResponse(
device, requestId, BluetoothGatt.GATT_SUCCESS, offset, null);
}
Log.d(TAG, "Operation onDescriptorWriteRequest successful.");
} catch (BluetoothGattException e) {
Log.e(TAG,
String.format(
"Could not write %s on device %s at %d",
BluetoothGattUtils.toString(descriptor),
device,
offset),
e);
bluetoothGattServer.sendResponse(
device, requestId, e.getGattErrorCode(), offset, null);
}
}
@Override
public void onExecuteWrite(BluetoothDevice device, int requestId, boolean execute) {
BluetoothGattServer bluetoothGattServer = mBluetoothGattServer;
if (bluetoothGattServer == null) {
return;
}
try {
getConnectionByDevice(device).executeWrite(execute);
bluetoothGattServer.sendResponse(
device, requestId, BluetoothGatt.GATT_SUCCESS, 0, null);
} catch (BluetoothGattException e) {
Log.e(TAG, "Could not execute write.", e);
bluetoothGattServer.sendResponse(device, requestId, e.getGattErrorCode(), 0, null);
}
}
@Override
public void onNotificationSent(BluetoothDevice device, int status) {
Log.d(TAG,
String.format("Received onNotificationSent for device %s with status %s",
device, status));
try {
getConnectionByDevice(device).notifyNotificationSent(status);
} catch (BluetoothGattException e) {
Log.e(TAG, "An error occurred when receiving onNotificationSent: " + e);
}
}
}
/** Listener for {@link BluetoothGattServerHelper}'s events. */
public interface Listener {
/** Called when a new connection to the server is established. */
void onConnection(BluetoothGattServerConnection connection);
}
}