blob: 17412a678d98f0e2b5039c6365ab50c720924934 [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 com.android.server.uwb.discovery.ble;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
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.content.AttributionSource;
import android.content.Context;
import android.content.ContextParams;
import android.util.Log;
import androidx.annotation.WorkerThread;
import com.android.server.uwb.discovery.TransportProvider.MessagePacket;
import com.android.server.uwb.discovery.TransportProvider.TerminationReason;
import com.android.server.uwb.discovery.TransportServerProvider;
import com.android.server.uwb.discovery.TransportServerProvider.TransportServerCallback;
import com.android.server.uwb.discovery.info.FiraConnectorCapabilities;
import com.android.server.uwb.discovery.info.FiraConnectorDataPacket;
import com.android.server.uwb.discovery.info.FiraConnectorMessage;
import java.io.ByteArrayOutputStream;
import java.nio.ByteBuffer;
import java.util.ArrayDeque;
import java.util.Arrays;
/**
* Class for FiRa CP UWB transport server provider using Bluetooth GATT.
*
* <p>The GATT server simply waits for the discovery from client side. It shall also wait for at
* least one valid update of FiRa Connector Capabilities characteristic value from the client side.
* Until this happens and until the client enables the Handle Value Notification method on the "OUT"
* Control Point characteristic (through Client Characteristic Configuration Descriptor), the server
* shall ignore all commands sent by Write methods through the "IN" Control Point characteristic.
*/
@WorkerThread
public class GattTransportServerProvider extends TransportServerProvider {
private static final String TAG = GattTransportServerProvider.class.getSimpleName();
private TransportServerCallback mTransportServerCallback;
private BluetoothManager mBluetoothManager;
private BluetoothGattServer mBluetoothGattServer;
private BluetoothDevice mRemoteGattDevice;
private FiraConnectorCapabilities mRemoteCapabilities;
private boolean mConnected;
private boolean mNotificationEnabled;
private BluetoothGattService mFiraCPService =
new BluetoothGattService(
UuidConstants.FIRA_CP_PARCEL_UUID.getUuid(),
BluetoothGattService.SERVICE_TYPE_PRIMARY);
private BluetoothGattCharacteristic mInControlPointCharacteristic;
private BluetoothGattCharacteristic mOutControlPointCharacteristic;
private BluetoothGattCharacteristic mCapabilitiesCharacteristic;
private BluetoothGattDescriptor mOutControlPointCccdDescriptor;
/* Queue of Fira Connector Data Packets from the mInControlPointCharacteristic that are
* incomplete to be constructed as FiRa Connector Message.
*/
private ArrayDeque<FiraConnectorDataPacket> mIncompleteInDataPacketQueue;
/* Queue of Fira Connector Message wrapped as MessagePacket to be sent via the
* mOutControlPointCharacteristic.
*/
private ArrayDeque<MessagePacket> mOutMessageQueue;
/**
* GATT server callbacks responsible for servicing read and write calls from the remote device
*/
private BluetoothGattServerCallback mBluetoothGattServerCallback =
new BluetoothGattServerCallback() {
@Override
public void onConnectionStateChange(
BluetoothDevice device, int status, int newState) {
Log.i(TAG, "onConnectionStateChange state:" + newState + " Device:" + device);
if (newState == BluetoothProfile.STATE_DISCONNECTED) {
mConnected = false;
startProcessing(device);
} else if (newState == BluetoothProfile.STATE_CONNECTED) {
mConnected = true;
startProcessing(device);
}
}
@Override
public void onCharacteristicReadRequest(
BluetoothDevice device,
int requestId,
int offset,
BluetoothGattCharacteristic characteristic) {
Log.d(TAG, "onCharacteristicReadRequest");
if (characteristic.getUuid().equals(mOutControlPointCharacteristic.getUuid())) {
Log.d(TAG, "onRead OutControlPointCharacteristic");
mBluetoothGattServer.sendResponse(
device,
requestId,
BluetoothGatt.GATT_SUCCESS,
offset,
mOutControlPointCharacteristic.getValue());
processOutDataPacket();
} else {
Log.w(TAG, "onRead unknown " + characteristic.getUuid());
}
}
@Override
public void onCharacteristicWriteRequest(
BluetoothDevice device,
int requestId,
BluetoothGattCharacteristic characteristic,
boolean preparedWrite,
boolean responseNeeded,
int offset,
byte[] value) {
Log.d(
TAG,
"onCharacteristicWriteRequest uuid:"
+ characteristic.getUuid()
+ ", Length: "
+ value.length);
if (characteristic.getUuid().equals(mCapabilitiesCharacteristic.getUuid())) {
Log.i(TAG, "onWrite CapabilitiesCharacteristic");
mRemoteCapabilities = FiraConnectorCapabilities.fromBytes(value);
if (mRemoteCapabilities != null) {
mTransportServerCallback.onCapabilitesUpdated(mRemoteCapabilities);
startProcessing(device);
if (responseNeeded) {
mBluetoothGattServer.sendResponse(
device,
requestId,
BluetoothGatt.GATT_SUCCESS,
offset,
value);
}
return;
}
if (responseNeeded) {
mBluetoothGattServer.sendResponse(
device,
requestId,
BluetoothGatt.GATT_FAILURE,
offset,
/*value=*/ null);
}
} else if (characteristic
.getUuid()
.equals(mInControlPointCharacteristic.getUuid())) {
Log.d(TAG, "onWrite InControlPointCharacteristic");
boolean success = processInDataPacket(value);
if (responseNeeded) {
if (success) {
mBluetoothGattServer.sendResponse(
device,
requestId,
BluetoothGatt.GATT_SUCCESS,
offset,
value);
} else {
mBluetoothGattServer.sendResponse(
device,
requestId,
BluetoothGatt.GATT_FAILURE,
offset,
/*value=*/ null);
}
}
} else {
Log.w(TAG, "onWrite unknown " + characteristic.getUuid());
}
}
@Override
public void onDescriptorWriteRequest(
BluetoothDevice device,
int requestId,
BluetoothGattDescriptor descriptor,
boolean preparedWrite,
boolean responseNeeded,
int offset,
byte[] value) {
Log.d(
TAG,
"onDescriptorWriteRequest uuid:"
+ descriptor.getUuid()
+ ", Length: "
+ value.length);
if (descriptor.getUuid().equals(mOutControlPointCccdDescriptor.getUuid())) {
if (Arrays.equals(
BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE, value)) {
Log.d(TAG, "Enable OutControlPoint value notifications: " + device);
mNotificationEnabled = true;
startProcessing(device);
} else if (Arrays.equals(
BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE, value)) {
Log.d(TAG, "Disable OutControlPoint value notifications: " + device);
mNotificationEnabled = false;
startProcessing(device);
}
if (responseNeeded) {
mBluetoothGattServer.sendResponse(
device, requestId, BluetoothGatt.GATT_SUCCESS, offset, value);
}
} else {
Log.w(TAG, "onDescriptorWrite unknown " + descriptor.getUuid());
if (responseNeeded) {
mBluetoothGattServer.sendResponse(
device,
requestId,
BluetoothGatt.GATT_FAILURE,
offset,
/*value=*/ null);
}
}
}
};
public GattTransportServerProvider(
AttributionSource attributionSource,
Context context,
int secid,
TransportServerCallback transportServerCallback) {
super(secid);
Context attributedContext =
context.createContext(
new ContextParams.Builder()
.setNextAttributionSource(attributionSource)
.build());
mTransportServerCallback = transportServerCallback;
mBluetoothManager = attributedContext.getSystemService(BluetoothManager.class);
mBluetoothGattServer =
mBluetoothManager.openGattServer(attributedContext, mBluetoothGattServerCallback);
mIncompleteInDataPacketQueue = new ArrayDeque();
mOutMessageQueue = new ArrayDeque();
setupGattCharacteristic();
}
@Override
public boolean start() {
if (!super.start()) {
return false;
}
if (mBluetoothGattServer == null) {
Log.w(TAG, "start failed due to mBluetoothGattServer is null.");
return false;
}
boolean succeed = mBluetoothGattServer.addService(mFiraCPService);
mStarted = succeed;
return succeed;
}
@Override
public boolean stop() {
if (!super.stop()) {
return false;
}
if (mBluetoothGattServer == null) {
Log.w(TAG, "stop failed due to mBluetoothGattServer is null.");
return false;
}
boolean succeed = mBluetoothGattServer.removeService(mFiraCPService);
// Clear in/out message queue.
mIncompleteInDataPacketQueue.clear();
mOutMessageQueue.clear();
mStarted = !succeed;
return succeed;
}
@Override
public boolean sendMessage(int secid, FiraConnectorMessage message) {
if (!isProcessing()) {
Log.w(TAG, "Sent request failed due to server not ready for processing.");
return false;
}
byte[] messageBytes = message.toBytes();
if (messageBytes.length > mRemoteCapabilities.maxMessageBufferSize) {
Log.w(
TAG,
"Sent request failed due to message size exceeded remote device capabilities.");
return false;
}
mOutMessageQueue.add(new MessagePacket(secid, ByteBuffer.wrap(messageBytes)));
// No existing meesage in progress, send this message immediately.
if (mOutMessageQueue.size() == 1) {
return processOutDataPacket();
}
return true;
}
/**
* Process the next out control data packet from the queue. Notify remote device if new data
* packet is set in the {@link mOutControlPointCharacteristic}.
*
* @return indicate if next out data packet was process successfully.
*/
private boolean processOutDataPacket() {
if (!isProcessing()) {
Log.w(TAG, "processOutDataPacket failed due to server not ready for processing.");
return false;
}
if (mOutMessageQueue.isEmpty()) {
Log.d(TAG, "processOutDataPacket skipped due to empty queue.");
return false;
}
MessagePacket messagePacket = mOutMessageQueue.peek();
ByteBuffer byteBuffer = messagePacket.messageBytes;
byte[] nextPayload =
new byte
[Math.min(
byteBuffer.remaining(),
mRemoteCapabilities.optimizedDataPacketSize
- FiraConnectorDataPacket.HEADER_SIZE)];
byteBuffer.get(nextPayload);
FiraConnectorDataPacket dataPacket =
new FiraConnectorDataPacket(
/*lastChainingPacket=*/ !byteBuffer.hasRemaining(),
messagePacket.secid,
nextPayload);
if (!byteBuffer.hasRemaining()) {
mOutMessageQueue.pop();
}
if (!mOutControlPointCharacteristic.setValue(dataPacket.toBytes())) {
Log.w(
TAG,
"processOutDataPacket failed due to fail to set"
+ " mOutControlPointCharacteristic.");
return false;
}
if (!mBluetoothGattServer.notifyCharacteristicChanged(
mRemoteGattDevice, mOutControlPointCharacteristic, /*confirm=*/ false)) {
Log.w(TAG, "processOutDataPacket failed due to fail to notifyCharacteristicChanged.");
return false;
}
return true;
}
/**
* Process the next in control data packet. Construct the FiraConnectorMEssage if data is
* complete, and notify callback with the constructed message.
*
* @return indicate if next in data packet was process successfully.
*/
private boolean processInDataPacket(byte[] bytes) {
if (!isProcessing()) {
Log.w(TAG, "processInDataPacket failed due to server not ready for processing.");
return false;
}
FiraConnectorDataPacket latestDataPacket = FiraConnectorDataPacket.fromBytes(bytes);
if (latestDataPacket == null) {
Log.w(
TAG,
"processInDataPacket failed due to latest FiraConnectorDataPacket cannot be"
+ " constructed from bytes.");
return false;
}
if (!mIncompleteInDataPacketQueue.isEmpty()
&& latestDataPacket.secid != mIncompleteInDataPacketQueue.peek().secid) {
Log.w(
TAG,
"processInDataPacket failed due to latest FiraConnectorDataPacket's SECID"
+ " doesn't match previous data packet.");
return false;
}
mIncompleteInDataPacketQueue.add(latestDataPacket);
if (!latestDataPacket.lastChainingPacket) {
return true;
}
// All data packets of the message has been received. Constructing the message.
ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
for (FiraConnectorDataPacket dataPacket : mIncompleteInDataPacketQueue) {
byteStream.write(dataPacket.payload, /*off=*/ 0, dataPacket.payload.length);
}
mIncompleteInDataPacketQueue.clear();
FiraConnectorMessage message = FiraConnectorMessage.fromBytes(byteStream.toByteArray());
if (message == null) {
Log.w(
TAG,
"processInDataPacket failed due to FiraConnectorMessage cannot be constructed"
+ " from bytes.");
return false;
}
super.onMessageReceived(latestDataPacket.secid, message);
return true;
}
/**
* Start processing of the FiRa Connector Data Packets and the FiRa Connector Messages through
* the In/Out control point characterstic when all conditions are meet to start the FiRa GATT
* server.
*
* @param device Remote Bluetooth device.
*/
private void startProcessing(BluetoothDevice device) {
if (!mConnected || !mNotificationEnabled || mRemoteCapabilities == null) {
Log.d(
TAG,
"Gatt server not fully ready: connected="
+ mConnected
+ ", notification enabled="
+ mNotificationEnabled
+ ", valid"
+ " capabilities="
+ (mRemoteCapabilities = null));
boolean stopping = isProcessing();
mRemoteGattDevice = null;
if (stopping) {
mTransportServerCallback.onProcessingStopped();
}
return;
}
mRemoteGattDevice = device;
mTransportServerCallback.onProcessingStarted();
}
/**
* Check if processing has started.
*
* @return indicate if server has started processing.
*/
private boolean isProcessing() {
return mRemoteGattDevice != null;
}
/**
* Initialize all of the GATT characteristics with appropriate default values and the required
* configurations.
*/
private void setupGattCharacteristic() {
mInControlPointCharacteristic =
new BluetoothGattCharacteristic(
UuidConstants.CP_IN_CONTROL_POINT_UUID.getUuid(),
BluetoothGattCharacteristic.PROPERTY_WRITE,
BluetoothGattCharacteristic.PERMISSION_WRITE);
mFiraCPService.addCharacteristic(mInControlPointCharacteristic);
mOutControlPointCharacteristic =
new BluetoothGattCharacteristic(
UuidConstants.CP_OUT_CONTROL_POINT_UUID.getUuid(),
BluetoothGattCharacteristic.PROPERTY_READ
| BluetoothGattCharacteristic.PROPERTY_NOTIFY,
BluetoothGattCharacteristic.PERMISSION_READ);
mOutControlPointCccdDescriptor =
new BluetoothGattDescriptor(
UuidConstants.CCCD_UUID.getUuid(), BluetoothGattDescriptor.PERMISSION_READ);
mOutControlPointCharacteristic.addDescriptor(mOutControlPointCccdDescriptor);
mFiraCPService.addCharacteristic(mOutControlPointCharacteristic);
mCapabilitiesCharacteristic =
new BluetoothGattCharacteristic(
UuidConstants.CP_FIRA_CONNECTOR_CAPABILITIES_UUID.getUuid(),
BluetoothGattCharacteristic.PROPERTY_WRITE,
BluetoothGattCharacteristic.PERMISSION_WRITE);
mFiraCPService.addCharacteristic(mCapabilitiesCharacteristic);
}
@Override
protected void terminateOnError(TerminationReason reason) {
Log.e(TAG, "GattTransportServerProvider terminated with reason:" + reason);
stop();
mTransportServerCallback.onTerminated(reason);
}
}