blob: 46ecbe5c9efe1fb4ede75a1253ccd5fda9e4d71f [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.pandora
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothGattDescriptor
import android.bluetooth.BluetoothGattService
import android.bluetooth.BluetoothGattService.SERVICE_TYPE_PRIMARY
import android.bluetooth.BluetoothManager
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.util.Log
import io.grpc.stub.StreamObserver
import java.io.Closeable
import java.util.UUID
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.take
import pandora.GATTGrpc.GATTImplBase
import pandora.GattProto.*
@kotlinx.coroutines.ExperimentalCoroutinesApi
class Gatt(private val context: Context) : GATTImplBase(), Closeable {
private val TAG = "PandoraGatt"
private val mScope: CoroutineScope = CoroutineScope(Dispatchers.Default)
private val mBluetoothManager = context.getSystemService(BluetoothManager::class.java)!!
private val mBluetoothAdapter = mBluetoothManager.adapter
private val serverManager by lazy { GattServerManager(mBluetoothManager, context, mScope) }
private val flow: Flow<Intent>
init {
val intentFilter = IntentFilter()
intentFilter.addAction(BluetoothDevice.ACTION_UUID)
flow = intentFlow(context, intentFilter).shareIn(mScope, SharingStarted.Eagerly)
}
override fun close() {
serverManager.server.close()
mScope.cancel()
// Clear existing Gatt instances to fix flakiness: b/279599889
GattInstance.clearAllInstances()
}
override fun exchangeMTU(
request: ExchangeMTURequest,
responseObserver: StreamObserver<ExchangeMTUResponse>
) {
grpcUnary<ExchangeMTUResponse>(mScope, responseObserver) {
val mtu = request.mtu
Log.i(TAG, "exchangeMTU MTU=$mtu")
if (!GattInstance.get(request.connection.address).mGatt.requestMtu(mtu)) {
throw RuntimeException("Error on requesting MTU $mtu")
}
ExchangeMTUResponse.newBuilder().build()
}
}
override fun writeAttFromHandle(
request: WriteRequest,
responseObserver: StreamObserver<WriteResponse>
) {
grpcUnary<WriteResponse>(mScope, responseObserver) {
Log.i(TAG, "writeAttFromHandle handle=${request.handle}")
val gattInstance = GattInstance.get(request.connection.address)
var characteristic: BluetoothGattCharacteristic? =
getCharacteristicWithHandle(request.handle, gattInstance)
if (characteristic == null) {
val descriptor: BluetoothGattDescriptor? =
getDescriptorWithHandle(request.handle, gattInstance)
checkNotNull(descriptor) {
"Found no characteristic or descriptor with handle ${request.handle}"
}
val valueWrote =
gattInstance.writeDescriptorBlocking(descriptor, request.value.toByteArray())
WriteResponse.newBuilder()
.setHandle(valueWrote.handle)
.setStatus(valueWrote.status)
.build()
} else {
val valueWrote =
gattInstance.writeCharacteristicBlocking(
characteristic,
request.value.toByteArray()
)
WriteResponse.newBuilder()
.setHandle(valueWrote.handle)
.setStatus(valueWrote.status)
.build()
}
}
}
override fun discoverServiceByUuid(
request: DiscoverServiceByUuidRequest,
responseObserver: StreamObserver<DiscoverServicesResponse>
) {
grpcUnary<DiscoverServicesResponse>(mScope, responseObserver) {
val gattInstance = GattInstance.get(request.connection.address)
Log.i(TAG, "discoverServiceByUuid uuid=${request.uuid}")
// In some cases, GATT starts a discovery immediately after being connected, so
// we need to wait until the service discovery is finished to be able to discover again.
// This takes between 20s and 28s, and there is no way to know if the service is busy or
// not.
// Delay was originally 30s, but due to flakyness increased to 32s.
delay(32000L)
check(gattInstance.mGatt.discoverServiceByUuid(UUID.fromString(request.uuid)))
// BluetoothGatt#discoverServiceByUuid does not trigger any callback and does not return
// any service, the API was made for PTS testing only.
DiscoverServicesResponse.newBuilder().build()
}
}
override fun discoverServices(
request: DiscoverServicesRequest,
responseObserver: StreamObserver<DiscoverServicesResponse>
) {
grpcUnary<DiscoverServicesResponse>(mScope, responseObserver) {
Log.i(TAG, "discoverServices")
val gattInstance = GattInstance.get(request.connection.address)
check(gattInstance.mGatt.discoverServices())
gattInstance.waitForDiscoveryEnd()
DiscoverServicesResponse.newBuilder()
.addAllServices(generateServicesList(gattInstance.mGatt.services, 1))
.build()
}
}
override fun discoverServicesSdp(
request: DiscoverServicesSdpRequest,
responseObserver: StreamObserver<DiscoverServicesSdpResponse>
) {
grpcUnary<DiscoverServicesSdpResponse>(mScope, responseObserver) {
Log.i(TAG, "discoverServicesSdp")
val bluetoothDevice = request.address.toBluetoothDevice(mBluetoothAdapter)
check(bluetoothDevice.fetchUuidsWithSdp())
// Several ACTION_UUID could be sent and some of them are empty (null)
flow
.filter { it.getAction() == BluetoothDevice.ACTION_UUID }
.filter { it.getBluetoothDeviceExtra() == bluetoothDevice }
.take(2)
.filter { bluetoothDevice.getUuids() != null }
.first()
val uuidsList = arrayListOf<String>()
for (parcelUuid in bluetoothDevice.getUuids()) {
uuidsList.add(parcelUuid.toString().uppercase())
}
DiscoverServicesSdpResponse.newBuilder().addAllServiceUuids(uuidsList).build()
}
}
override fun clearCache(
request: ClearCacheRequest,
responseObserver: StreamObserver<ClearCacheResponse>
) {
grpcUnary<ClearCacheResponse>(mScope, responseObserver) {
Log.i(TAG, "clearCache")
val gattInstance = GattInstance.get(request.connection.address)
check(gattInstance.mGatt.refresh())
ClearCacheResponse.newBuilder().build()
}
}
override fun readCharacteristicFromHandle(
request: ReadCharacteristicRequest,
responseObserver: StreamObserver<ReadCharacteristicResponse>
) {
grpcUnary<ReadCharacteristicResponse>(mScope, responseObserver) {
Log.i(TAG, "readCharacteristicFromHandle handle=${request.handle}")
val gattInstance = GattInstance.get(request.connection.address)
val characteristic: BluetoothGattCharacteristic? =
getCharacteristicWithHandle(request.handle, gattInstance)
checkNotNull(characteristic) { "Characteristic handle ${request.handle} not found." }
val readValue = gattInstance.readCharacteristicBlocking(characteristic)
ReadCharacteristicResponse.newBuilder()
.setValue(
AttValue.newBuilder().setHandle(readValue.handle).setValue(readValue.value)
)
.setStatus(readValue.status)
.build()
}
}
override fun readCharacteristicsFromUuid(
request: ReadCharacteristicsFromUuidRequest,
responseObserver: StreamObserver<ReadCharacteristicsFromUuidResponse>
) {
grpcUnary<ReadCharacteristicsFromUuidResponse>(mScope, responseObserver) {
Log.i(TAG, "readCharacteristicsFromUuid uuid=${request.uuid}")
val gattInstance = GattInstance.get(request.connection.address)
tryDiscoverServices(gattInstance)
val readValues =
gattInstance.readCharacteristicUuidBlocking(
UUID.fromString(request.uuid),
request.startHandle,
request.endHandle
)
ReadCharacteristicsFromUuidResponse.newBuilder()
.addAllCharacteristicsRead(generateReadValuesList(readValues))
.build()
}
}
override fun readCharacteristicDescriptorFromHandle(
request: ReadCharacteristicDescriptorRequest,
responseObserver: StreamObserver<ReadCharacteristicDescriptorResponse>
) {
grpcUnary<ReadCharacteristicDescriptorResponse>(mScope, responseObserver) {
Log.i(TAG, "readCharacteristicDescriptorFromHandle handle=${request.handle}")
val gattInstance = GattInstance.get(request.connection.address)
val descriptor: BluetoothGattDescriptor? =
getDescriptorWithHandle(request.handle, gattInstance)
checkNotNull(descriptor) { "Descriptor handle ${request.handle} not found." }
val readValue = gattInstance.readDescriptorBlocking(descriptor)
ReadCharacteristicDescriptorResponse.newBuilder()
.setValue(
AttValue.newBuilder().setHandle(readValue.handle).setValue(readValue.value)
)
.setStatus(readValue.status)
.build()
}
}
override fun registerService(
request: RegisterServiceRequest,
responseObserver: StreamObserver<RegisterServiceResponse>
) {
grpcUnary(mScope, responseObserver) {
Log.i(TAG, "registerService")
val service =
BluetoothGattService(UUID.fromString(request.service.uuid), SERVICE_TYPE_PRIMARY)
for (characteristic_params in request.service.characteristicsList) {
val characteristic =
BluetoothGattCharacteristic(
UUID.fromString(characteristic_params.uuid),
characteristic_params.properties,
characteristic_params.permissions
)
for (descriptor_params in characteristic_params.descriptorsList) {
characteristic.addDescriptor(
BluetoothGattDescriptor(
UUID.fromString(descriptor_params.uuid),
descriptor_params.properties,
descriptor_params.permissions
)
)
}
service.addCharacteristic(characteristic)
}
val fullService = coroutineScope {
val firstService = mScope.async { serverManager.newServiceFlow.first() }
serverManager.server.addService(service)
firstService.await()
}
RegisterServiceResponse.newBuilder()
.setService(
GattService.newBuilder()
.setHandle(fullService.instanceId)
.setType(fullService.type)
.setUuid(fullService.uuid.toString().uppercase())
.addAllIncludedServices(generateServicesList(service.includedServices, 1))
.addAllCharacteristics(generateCharacteristicsList(service.characteristics))
.build()
)
.build()
}
}
override fun setCharacteristicNotificationFromHandle(
request: SetCharacteristicNotificationFromHandleRequest,
responseObserver: StreamObserver<SetCharacteristicNotificationFromHandleResponse>
) {
grpcUnary<SetCharacteristicNotificationFromHandleResponse>(mScope, responseObserver) {
Log.i(TAG, "SetCharcteristicNotificationFromHandle")
val gattInstance = GattInstance.get(request.connection.address)
val descriptor: BluetoothGattDescriptor? =
getDescriptorWithHandle(request.handle, gattInstance)
checkNotNull(descriptor) { "Found no descriptor with handle ${request.handle}" }
var characteristic = descriptor.getCharacteristic()
gattInstance.mGatt.setCharacteristicNotification(characteristic, true)
if (request.enableValue == EnableValue.ENABLE_INDICATION_VALUE) {
val valueWrote =
gattInstance.writeDescriptorBlocking(
descriptor,
BluetoothGattDescriptor.ENABLE_INDICATION_VALUE
)
SetCharacteristicNotificationFromHandleResponse.newBuilder()
.setHandle(valueWrote.handle)
.setStatus(valueWrote.status)
.build()
} else {
val valueWrote =
gattInstance.writeDescriptorBlocking(
descriptor,
BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
)
SetCharacteristicNotificationFromHandleResponse.newBuilder()
.setHandle(valueWrote.handle)
.setStatus(valueWrote.status)
.build()
}
}
}
override fun waitCharacteristicNotification(
request: WaitCharacteristicNotificationRequest,
responseObserver: StreamObserver<WaitCharacteristicNotificationResponse>
) {
grpcUnary<WaitCharacteristicNotificationResponse>(mScope, responseObserver) {
val gattInstance = GattInstance.get(request.connection.address)
val descriptor: BluetoothGattDescriptor? =
getDescriptorWithHandle(request.handle, gattInstance)
checkNotNull(descriptor) { "Found no descriptor with handle ${request.handle}" }
var characteristic = descriptor.getCharacteristic()
val characteristicNotificationReceived =
gattInstance.waitForOnCharacteristicChanged(characteristic)
WaitCharacteristicNotificationResponse.newBuilder()
.setCharacteristicNotificationReceived(characteristicNotificationReceived)
.build()
}
}
/**
* Discovers services, then returns characteristic with given handle. BluetoothGatt API is
* package-private so we have to redefine it here.
*/
private suspend fun getCharacteristicWithHandle(
handle: Int,
gattInstance: GattInstance
): BluetoothGattCharacteristic? {
tryDiscoverServices(gattInstance)
for (service: BluetoothGattService in gattInstance.mGatt.services.orEmpty()) {
for (characteristic: BluetoothGattCharacteristic in service.characteristics) {
if (characteristic.instanceId == handle) {
return characteristic
}
}
}
return null
}
/**
* Discovers services, then returns descriptor with given handle. BluetoothGatt API is
* package-private so we have to redefine it here.
*/
private suspend fun getDescriptorWithHandle(
handle: Int,
gattInstance: GattInstance
): BluetoothGattDescriptor? {
tryDiscoverServices(gattInstance)
for (service: BluetoothGattService in gattInstance.mGatt.services.orEmpty()) {
for (characteristic: BluetoothGattCharacteristic in service.characteristics) {
for (descriptor: BluetoothGattDescriptor in characteristic.descriptors) {
if (descriptor.getInstanceId() == handle) {
return descriptor
}
}
}
}
return null
}
/** Generates a list of GattService from a list of BluetoothGattService. */
private fun generateServicesList(
servicesList: List<BluetoothGattService>,
dpth: Int
): ArrayList<GattService> {
val newServicesList = arrayListOf<GattService>()
for (service in servicesList) {
val serviceBuilder =
GattService.newBuilder()
.setHandle(service.getInstanceId())
.setType(service.getType())
.setUuid(service.getUuid().toString().uppercase())
.addAllIncludedServices(
generateServicesList(service.getIncludedServices(), dpth + 1)
)
.addAllCharacteristics(generateCharacteristicsList(service.characteristics))
newServicesList.add(serviceBuilder.build())
}
return newServicesList
}
/** Generates a list of GattCharacteristic from a list of BluetoothGattCharacteristic. */
private fun generateCharacteristicsList(
characteristicsList: List<BluetoothGattCharacteristic>
): ArrayList<GattCharacteristic> {
val newCharacteristicsList = arrayListOf<GattCharacteristic>()
for (characteristic in characteristicsList) {
val characteristicBuilder =
GattCharacteristic.newBuilder()
.setProperties(characteristic.getProperties())
.setPermissions(characteristic.getPermissions())
.setUuid(characteristic.getUuid().toString().uppercase())
.addAllDescriptors(generateDescriptorsList(characteristic.getDescriptors()))
.setHandle(characteristic.getInstanceId())
newCharacteristicsList.add(characteristicBuilder.build())
}
return newCharacteristicsList
}
/** Generates a list of GattCharacteristicDescriptor from a list of BluetoothGattDescriptor. */
private fun generateDescriptorsList(
descriptorsList: List<BluetoothGattDescriptor>
): ArrayList<GattCharacteristicDescriptor> {
val newDescriptorsList = arrayListOf<GattCharacteristicDescriptor>()
for (descriptor in descriptorsList) {
val descriptorBuilder =
GattCharacteristicDescriptor.newBuilder()
.setHandle(descriptor.getInstanceId())
.setPermissions(descriptor.getPermissions())
.setUuid(descriptor.getUuid().toString().uppercase())
newDescriptorsList.add(descriptorBuilder.build())
}
return newDescriptorsList
}
/** Generates a list of ReadCharacteristicResponse from a list of GattInstanceValueRead. */
private fun generateReadValuesList(
readValuesList: ArrayList<GattInstance.GattInstanceValueRead>
): ArrayList<ReadCharacteristicResponse> {
val newReadValuesList = arrayListOf<ReadCharacteristicResponse>()
for (readValue in readValuesList) {
val readValueBuilder =
ReadCharacteristicResponse.newBuilder()
.setValue(
AttValue.newBuilder().setHandle(readValue.handle).setValue(readValue.value)
)
.setStatus(readValue.status)
newReadValuesList.add(readValueBuilder.build())
}
return newReadValuesList
}
private suspend fun tryDiscoverServices(gattInstance: GattInstance) {
if (!gattInstance.servicesDiscovered() && !gattInstance.mGatt.discoverServices()) {
throw RuntimeException("Error on discovering services for $gattInstance")
} else {
gattInstance.waitForDiscoveryEnd()
}
}
}