blob: fae6951547784ee878ad9ee6a74e6355853319d3 [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.util.Log;
import androidx.annotation.Nullable;
import com.android.server.nearby.common.bluetooth.BluetoothException;
import com.android.server.nearby.common.bluetooth.BluetoothGattException;
import com.android.server.nearby.common.bluetooth.ReservedUuids;
import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothDevice;
import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor;
import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.Operation;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import com.google.common.io.BaseEncoding;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;
/**
* Connection to a bluetooth LE device over Gatt.
*/
@TargetApi(18)
public class BluetoothGattServerConnection implements Closeable {
@SuppressWarnings("unused")
private static final String TAG = BluetoothGattServerConnection.class.getSimpleName();
/** See {@link BluetoothGattDescriptor#DISABLE_NOTIFICATION_VALUE}. */
private static final short DISABLE_NOTIFICATION_VALUE = 0x0000;
/** See {@link BluetoothGattDescriptor#ENABLE_NOTIFICATION_VALUE}. */
private static final short ENABLE_NOTIFICATION_VALUE = 0x0001;
/** See {@link BluetoothGattDescriptor#ENABLE_INDICATION_VALUE}. */
private static final short ENABLE_INDICATION_VALUE = 0x0002;
/** Default MTU when value is unknown. */
public static final int DEFAULT_MTU = 23;
@VisibleForTesting
static final long OPERATION_TIMEOUT = TimeUnit.SECONDS.toMillis(1);
/** Notification types as defined by the BLE spec vol 4, sec G, part 3.3.3.3 */
public enum NotificationType {
NOTIFICATION,
INDICATION
}
/** BT operation types that can be in flight. */
public enum OperationType {
SEND_NOTIFICATION
}
private final Map<ScopedKey, Object> mContextValues = new HashMap<ScopedKey, Object>();
private final List<Listener> mCloseListeners = new ArrayList<Listener>();
private final BluetoothGattServerHelper mBluetoothGattServerHelper;
private final BluetoothDevice mBluetoothDevice;
@VisibleForTesting
BluetoothOperationExecutor mBluetoothOperationScheduler =
new BluetoothOperationExecutor(1);
/** Stores pending writes. For each UUID, we store an offset and a byte[] of data. */
@VisibleForTesting
final Map<BluetoothGattServlet, SortedMap<Integer, byte[]>> mQueuedCharacteristicWrites =
new HashMap<BluetoothGattServlet, SortedMap<Integer, byte[]>>();
@VisibleForTesting
final Map<BluetoothGattCharacteristic, Notifier> mRegisteredNotifications =
new HashMap<BluetoothGattCharacteristic, Notifier>();
private final Map<BluetoothGattCharacteristic, BluetoothGattServlet> mServlets;
public BluetoothGattServerConnection(
BluetoothGattServerHelper bluetoothGattServerHelper,
BluetoothDevice device,
BluetoothGattServerConfig serverConfig) {
mBluetoothGattServerHelper = bluetoothGattServerHelper;
mBluetoothDevice = device;
mServlets = serverConfig.getServlets();
}
public void setContextValue(Object scope, String key, @Nullable Object value) {
mContextValues.put(new ScopedKey(scope, key), value);
}
@Nullable
public Object getContextValue(Object scope, String key) {
return mContextValues.get(new ScopedKey(scope, key));
}
public BluetoothDevice getDevice() {
return mBluetoothDevice;
}
public int getMtu() {
return DEFAULT_MTU;
}
public int getMaxDataPacketSize() {
// Per BT specs (3.2.9), only MTU - 3 bytes can be used to transmit data
return getMtu() - 3;
}
public void addCloseListener(Listener listener) {
synchronized (mCloseListeners) {
mCloseListeners.add(listener);
}
}
public void removeCloseListener(Listener listener) {
synchronized (mCloseListeners) {
mCloseListeners.remove(listener);
}
}
private BluetoothGattServlet getServlet(BluetoothGattCharacteristic characteristic)
throws BluetoothGattException {
BluetoothGattServlet servlet = mServlets.get(characteristic);
if (servlet == null) {
throw new BluetoothGattException(
String.format("No handler registered for characteristic %s.",
characteristic.getUuid()),
BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED);
}
return servlet;
}
public byte[] readCharacteristic(int offset, BluetoothGattCharacteristic characteristic)
throws BluetoothGattException {
return getServlet(characteristic).read(this, offset);
}
public void writeCharacteristic(BluetoothGattCharacteristic characteristic,
boolean preparedWrite,
int offset, byte[] value) throws BluetoothGattException {
Log.d(TAG, String.format(
"Received %d bytes at offset %d on %s from device %s, prepareWrite=%s.",
value.length,
offset,
BluetoothGattUtils.toString(characteristic),
mBluetoothDevice,
preparedWrite));
BluetoothGattServlet servlet = getServlet(characteristic);
if (preparedWrite) {
SortedMap<Integer, byte[]> bytePackets = mQueuedCharacteristicWrites.get(servlet);
if (bytePackets == null) {
bytePackets = new TreeMap<Integer, byte[]>();
mQueuedCharacteristicWrites.put(servlet, bytePackets);
}
bytePackets.put(offset, value);
return;
}
Log.d(TAG, servlet.toString());
servlet.write(this, offset, value);
}
public byte[] readDescriptor(int offset, BluetoothGattDescriptor descriptor)
throws BluetoothGattException {
BluetoothGattCharacteristic characteristic = descriptor.getCharacteristic();
if (characteristic == null) {
throw new BluetoothGattException(String.format(
"Descriptor %s not associated with a characteristics!",
BluetoothGattUtils.toString(descriptor)), BluetoothGatt.GATT_FAILURE);
}
return getServlet(characteristic).readDescriptor(this, descriptor, offset);
}
public void writeDescriptor(
BluetoothGattDescriptor descriptor,
boolean preparedWrite,
int offset,
byte[] value) throws BluetoothGattException {
Log.d(TAG, String.format(
"Received %d bytes at offset %d on %s from device %s, prepareWrite=%s.",
value.length,
offset,
BluetoothGattUtils.toString(descriptor),
mBluetoothDevice,
preparedWrite));
if (preparedWrite) {
throw new BluetoothGattException(
String.format("Prepare write not supported for descriptor %s.", descriptor),
BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED);
}
BluetoothGattCharacteristic characteristic = descriptor.getCharacteristic();
if (characteristic == null) {
throw new BluetoothGattException(String.format(
"Descriptor %s not associated with a characteristics!",
BluetoothGattUtils.toString(descriptor)), BluetoothGatt.GATT_FAILURE);
}
BluetoothGattServlet servlet = getServlet(characteristic);
if (descriptor.getUuid().equals(
ReservedUuids.Descriptors.CLIENT_CHARACTERISTIC_CONFIGURATION)) {
handleCharacteristicConfigurationChange(characteristic, servlet, offset, value);
return;
}
servlet.writeDescriptor(this, descriptor, offset, value);
}
private void handleCharacteristicConfigurationChange(
final BluetoothGattCharacteristic characteristic, BluetoothGattServlet servlet,
int offset,
byte[] value)
throws BluetoothGattException {
if (offset != 0) {
throw new BluetoothGattException(String.format(
"Offset should be 0 when changing the client characteristic config: %d.",
offset),
BluetoothGatt.GATT_INVALID_OFFSET);
}
if (value.length != 2) {
throw new BluetoothGattException(String.format(
"Value 0x%s is undefined for the client characteristic config",
BaseEncoding.base16().encode(value)),
BluetoothGatt.GATT_INVALID_ATTRIBUTE_LENGTH);
}
boolean notificationRegistered = mRegisteredNotifications.containsKey(characteristic);
Notifier notifier;
switch (toShort(value)) {
case ENABLE_NOTIFICATION_VALUE:
if (!notificationRegistered) {
notifier = new Notifier() {
@Override
public void notify(byte[] data) throws BluetoothException {
sendNotification(characteristic, NotificationType.NOTIFICATION, data);
}
};
mRegisteredNotifications.put(characteristic, notifier);
servlet.enableNotification(this, notifier);
}
break;
case ENABLE_INDICATION_VALUE:
if (!notificationRegistered) {
notifier = new Notifier() {
@Override
public void notify(byte[] data) throws BluetoothException {
sendNotification(characteristic, NotificationType.INDICATION, data);
}
};
mRegisteredNotifications.put(characteristic, notifier);
servlet.enableNotification(this, notifier);
}
break;
case DISABLE_NOTIFICATION_VALUE:
// Note: this disables notifications or indications.
if (notificationRegistered) {
notifier = mRegisteredNotifications.remove(characteristic);
if (notifier == null) {
return; // this is not supposed to happen
}
servlet.disableNotification(this, notifier);
}
break;
default:
throw new BluetoothGattException(String.format(
"Value 0x%s is undefined for the client characteristic config",
BaseEncoding.base16().encode(value)),
BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED);
}
}
private static short toShort(byte[] value) {
Preconditions.checkNotNull(value);
Preconditions.checkArgument(value.length == 2, "Length should be 2 bytes.");
return (short) ((value[0] & 0x00FF) | (value[1] << 8));
}
public void executeWrite(boolean execute) throws BluetoothGattException {
if (!execute) {
mQueuedCharacteristicWrites.clear();
return;
}
try {
for (Entry<BluetoothGattServlet, SortedMap<Integer, byte[]>> queuedWrite :
mQueuedCharacteristicWrites.entrySet()) {
BluetoothGattServlet servlet = queuedWrite.getKey();
SortedMap<Integer, byte[]> chunks = queuedWrite.getValue();
if (servlet == null || chunks == null) {
// This is not supposed to happen
throw new IllegalStateException();
}
assembleByteChunksAndHandle(servlet, chunks);
}
} finally {
mQueuedCharacteristicWrites.clear();
}
}
/**
* Assembles the specified queued writes and calls the provided write handler on the assembled
* chunks. Tries to assemble all the chunks into one write request. For example, if the content
* of byteChunks is:
* <code>
* offset data_size
* 0 10
* 10 1
* 11 5
* </code>
*
* then this method would call <code>writeHandler.onWrite(0, byte[16])</code>
*
* However, if all the chunks cannot be assembled into a continuous byte[], then onWrite() will
* be called multiple times with the largest continuous chunks. For example, if the content of
* byteChunks is:
* <code>
* offset data_size
* 10 12
* 30 5
* 35 9
* </code>
*
* then this method would call <code>writeHandler.onWrite(10, byte[12)</code> and
* <code>writeHandler.onWrite(30, byte[14]).
*/
private void assembleByteChunksAndHandle(BluetoothGattServlet servlet,
SortedMap<Integer, byte[]> byteChunks) throws BluetoothGattException {
ByteArrayOutputStream assembledRequest = new ByteArrayOutputStream();
Integer startWritingAtOffset = 0;
while (!byteChunks.isEmpty()) {
Integer offset = byteChunks.firstKey();
if (offset.intValue() < startWritingAtOffset + assembledRequest.size()) {
throw new BluetoothGattException(
"Expected offset of at least " + assembledRequest.size()
+ ", but got offset " + offset, BluetoothGatt.GATT_INVALID_OFFSET);
}
// If we have a hole, then write what we've already assembled and start assembling a new
// long write
if (offset.intValue() > startWritingAtOffset + assembledRequest.size()) {
servlet.write(this, startWritingAtOffset.intValue(),
assembledRequest.toByteArray());
startWritingAtOffset = offset;
assembledRequest.reset();
}
try {
byte[] dataChunk = byteChunks.remove(offset);
if (dataChunk == null) {
// This is not supposed to happen
throw new IllegalStateException();
}
assembledRequest.write(dataChunk);
} catch (IOException e) {
throw new BluetoothGattException("Error assembling request",
BluetoothGatt.GATT_FAILURE);
}
}
// If there is anything to write, write it
if (assembledRequest.size() > 0) {
Preconditions.checkNotNull(startWritingAtOffset); // should never be null at this point
servlet.write(this, startWritingAtOffset.intValue(), assembledRequest.toByteArray());
}
}
private void sendNotification(final BluetoothGattCharacteristic characteristic,
final NotificationType notificationType, final byte[] data)
throws BluetoothException {
mBluetoothOperationScheduler.execute(
new Operation<Void>(OperationType.SEND_NOTIFICATION) {
@Override
public void run() throws BluetoothException {
mBluetoothGattServerHelper.sendNotification(mBluetoothDevice,
characteristic,
data,
notificationType == NotificationType.INDICATION ? true : false);
}
},
OPERATION_TIMEOUT);
}
@Override
public void close() throws IOException {
try {
mBluetoothGattServerHelper.closeConnection(mBluetoothDevice);
} catch (BluetoothException e) {
throw new IOException("Failed to close connection", e);
}
}
public void notifyNotificationSent(int status) {
mBluetoothOperationScheduler.notifyCompletion(
new Operation<Void>(OperationType.SEND_NOTIFICATION), status);
}
public void onClose() {
synchronized (mCloseListeners) {
for (Listener listener : mCloseListeners) {
listener.onClose();
}
}
}
/** Scope/key pair to use to reference contextual values. */
private static class ScopedKey {
private final Object mScope;
private final String mKey;
ScopedKey(Object scope, String key) {
mScope = scope;
mKey = key;
}
@Override
public boolean equals(@Nullable Object o) {
if (!(o instanceof ScopedKey)) {
return false;
}
ScopedKey other = (ScopedKey) o;
return other.mScope.equals(mScope) && other.mKey.equals(mKey);
}
@Override
public int hashCode() {
return Objects.hashCode(mScope, mKey);
}
}
/** Listener to be notified when the connection closes. */
public interface Listener {
void onClose();
}
/** Notifier to notify data over notification or indication. */
public interface Notifier {
void notify(byte[] data) throws BluetoothException;
}
}