[go: nahoru, domu]

blob: 785983d4c25134352e7a79f0582028dded3b274e [file] [log] [blame]
/*
* Copyright 2023 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 androidx.bluetooth
import android.Manifest.permission.BLUETOOTH_CONNECT
import android.annotation.SuppressLint
import android.bluetooth.BluetoothDevice as FwkBluetoothDevice
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCharacteristic as FwkCharacteristic
import android.bluetooth.BluetoothGattServer
import android.bluetooth.BluetoothGattServerCallback
import android.bluetooth.BluetoothGattService
import android.bluetooth.BluetoothManager
import android.bluetooth.BluetoothProfile
import android.content.Context
import android.os.Build
import androidx.annotation.RequiresPermission
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.receiveAsFlow
/**
* Class for handling operations as a GATT server role
*/
internal class GattServer(private val context: Context) {
private interface GattServerImpl {
fun openGattServer(context: Context, callback: BluetoothGattServerCallback)
fun closeGattServer()
fun clearServices()
fun addService(service: BluetoothGattService)
fun notifyCharacteristicChanged(
device: FwkBluetoothDevice,
characteristic: FwkCharacteristic,
confirm: Boolean,
value: ByteArray
)
fun sendResponse(
device: FwkBluetoothDevice,
requestId: Int,
status: Int,
offset: Int,
value: ByteArray?
)
}
private companion object {
private const val TAG = "GattServer"
}
internal class Session {
enum class State {
DISCONNECTED,
CONNECTING,
CONNECTED,
}
var state: AtomicReference<State> = AtomicReference(State.CONNECTING)
val requestChannel = Channel<GattServerRequest>(Channel.UNLIMITED)
}
// Should be accessed only from the callback thread
private val sessions: MutableMap<FwkBluetoothDevice, Session> = mutableMapOf()
private val attributeMap = AttributeMap()
@SuppressLint("ObsoleteSdkInt")
private val impl: GattServerImpl =
if (Build.VERSION.SDK_INT >= 33) GattServerImplApi33()
else BaseGattServerImpl()
fun open(services: List<GattService>):
Flow<BluetoothLe.GattServerConnectionRequest> = callbackFlow {
attributeMap.updateWithServices(services)
val callback = object : BluetoothGattServerCallback() {
override fun onConnectionStateChange(
device: FwkBluetoothDevice,
status: Int,
newState: Int
) {
when (newState) {
BluetoothProfile.STATE_CONNECTED -> {
trySend(
BluetoothLe.GattServerConnectionRequest(
BluetoothDevice(device),
this@GattServer,
addSession(device)
)
)
}
BluetoothProfile.STATE_DISCONNECTED -> removeSession(device)
}
}
override fun onCharacteristicReadRequest(
device: FwkBluetoothDevice,
requestId: Int,
offset: Int,
characteristic: FwkCharacteristic
) {
attributeMap.fromFwkCharacteristic(characteristic)?.let {
findActiveSessionWithDevice(device)?.requestChannel?.trySend(
GattServerRequest.ReadCharacteristicRequest(
this@GattServer, device, requestId, offset, it
)
)
} ?: run {
sendResponse(device, requestId, BluetoothGatt.GATT_READ_NOT_PERMITTED, offset,
/*value=*/null)
}
}
override fun onCharacteristicWriteRequest(
device: FwkBluetoothDevice,
requestId: Int,
characteristic: FwkCharacteristic,
preparedWrite: Boolean,
responseNeeded: Boolean,
offset: Int,
value: ByteArray?
) {
attributeMap.fromFwkCharacteristic(characteristic)?.let {
findActiveSessionWithDevice(device)?.requestChannel?.trySend(
GattServerRequest.WriteCharacteristicRequest(
this@GattServer,
device,
requestId,
it,
preparedWrite,
responseNeeded,
offset,
value
)
)
} ?: run {
sendResponse(device, requestId, BluetoothGatt.GATT_WRITE_NOT_PERMITTED,
offset, /*value=*/null)
}
}
}
impl.openGattServer(context, callback)
services.forEach { impl.addService(it.fwkService) }
awaitClose {
impl.closeGattServer()
}
}
fun updateServices(services: List<GattService>) {
impl.clearServices()
services.forEach { impl.addService(it.fwkService) }
}
suspend fun<R> acceptConnection(
request: BluetoothLe.GattServerConnectionRequest,
block: BluetoothLe.GattServerScope.() -> R
) = coroutineScope {
val session = request.session
if (!session.state.compareAndSet(Session.State.CONNECTING, Session.State.CONNECTED)) {
throw IllegalStateException("the request is already handled")
}
val scope = object : BluetoothLe.GattServerScope {
override val device: BluetoothDevice
get() = request.device
override val requests = session.requestChannel.receiveAsFlow()
override fun notify(
characteristic: GattCharacteristic,
value: ByteArray
) {
impl.notifyCharacteristicChanged(
request.device.fwkDevice, characteristic.fwkCharacteristic, false, value)
}
}
scope.block()
}
@SuppressLint("MissingPermission")
fun rejectConnection(request: BluetoothLe.GattServerConnectionRequest) {
if (!request.session.state.compareAndSet(
Session.State.CONNECTING, Session.State.DISCONNECTED)) {
throw IllegalStateException("the request is already handled")
}
}
internal fun findActiveSessionWithDevice(device: FwkBluetoothDevice): Session? {
return sessions[device]?.takeIf {
it.state.get() != Session.State.DISCONNECTED
}
}
internal fun addSession(device: FwkBluetoothDevice): Session {
return Session().apply {
sessions[device] = this
}
}
internal fun removeSession(device: FwkBluetoothDevice) {
sessions.remove(device)
}
internal fun sendResponse(
device: FwkBluetoothDevice,
requestId: Int,
status: Int,
offset: Int,
value: ByteArray?
) {
impl.sendResponse(device, requestId, status, offset, value)
}
private open class BaseGattServerImpl : GattServerImpl {
internal var gattServer: BluetoothGattServer? = null
private val isOpen = AtomicBoolean(false)
@RequiresPermission(BLUETOOTH_CONNECT)
override fun openGattServer(context: Context, callback: BluetoothGattServerCallback) {
if (!isOpen.compareAndSet(false, true))
throw IllegalStateException("GATT server is already opened")
val bluetoothManager = context.getSystemService(BluetoothManager::class.java)
gattServer = bluetoothManager?.openGattServer(context, callback)
}
@RequiresPermission(BLUETOOTH_CONNECT)
override fun closeGattServer() {
if (!isOpen.compareAndSet(true, false))
throw IllegalStateException("GATT server is already closed")
gattServer?.close()
}
@RequiresPermission(BLUETOOTH_CONNECT)
override fun clearServices() {
gattServer?.clearServices()
}
@RequiresPermission(BLUETOOTH_CONNECT)
override fun addService(service: BluetoothGattService) {
gattServer?.addService(service)
}
@Suppress("DEPRECATION")
@RequiresPermission(BLUETOOTH_CONNECT)
override fun notifyCharacteristicChanged(
device: FwkBluetoothDevice,
characteristic: FwkCharacteristic,
confirm: Boolean,
value: ByteArray
) {
characteristic.value = value
gattServer?.notifyCharacteristicChanged(device, characteristic, confirm)
}
@RequiresPermission(BLUETOOTH_CONNECT)
override fun sendResponse(
device: android.bluetooth.BluetoothDevice,
requestId: Int,
status: Int,
offset: Int,
value: ByteArray?
) {
gattServer?.sendResponse(device, requestId, status, offset, value)
}
}
private open class GattServerImplApi33 : BaseGattServerImpl() {
@RequiresPermission(BLUETOOTH_CONNECT)
override fun notifyCharacteristicChanged(
device: FwkBluetoothDevice,
characteristic: FwkCharacteristic,
confirm: Boolean,
value: ByteArray
) {
gattServer?.notifyCharacteristicChanged(device, characteristic, confirm, value)
}
}
}