package com.rtnbleserver import android.annotation.SuppressLint 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.bluetooth.le.AdvertiseCallback import android.bluetooth.le.AdvertiseData import android.bluetooth.le.AdvertiseSettings import android.bluetooth.le.BluetoothLeAdvertiser import android.content.Context import android.os.Build import android.os.ParcelUuid import android.util.Log import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.Callback import com.facebook.react.bridge.ReactApplicationContext import java.nio.charset.StandardCharsets import java.util.UUID import org.json.JSONObject @SuppressLint("MissingPermission") class RTNbleServerModule(reactContext: ReactApplicationContext) : NativeRTNbleServerSpec(reactContext) { private val lock = Any() private val bluetoothManager = reactApplicationContext.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager private var gattServer: BluetoothGattServer? = null private var advertiser: BluetoothLeAdvertiser? = null private var running = false private var otp: String? = null private var did: String = "" private var record: Record? = null private var didHandler: Callback? = null private var clientErrorHandler: Callback? = null private var client: RTNbleGattClient? = null private val authorizedDevices = mutableSetOf() private val subscribedDevices = mutableSetOf() private val didBuffers = mutableMapOf() override fun setOTP(otp: String) { synchronized(lock) { this.otp = otp } } override fun setDID(did: String) { synchronized(lock) { this.did = did } } override fun getRecord(): String { val snapshot = synchronized(lock) { record } ?: return "" return JSONObject() .put("did", snapshot.did) .put("createdAt", snapshot.createdAt) .toString() } override fun setDIDHandler(handler: Callback) { synchronized(lock) { didHandler = handler } } override fun startGatt() { synchronized(lock) { if (running) { return } val manager = bluetoothManager ?: throw IllegalStateException("Bluetooth is not supported on this device") val adapter = manager.adapter ?: throw IllegalStateException("Bluetooth is not supported on this device") if (!hasServerPermissions()) { throw SecurityException( "BLUETOOTH_CONNECT and BLUETOOTH_ADVERTISE permissions are required" ) } if (!adapter.isEnabled) { throw IllegalStateException("Bluetooth is turned off") } val bleAdvertiser = adapter.bluetoothLeAdvertiser ?: throw IllegalStateException("Bluetooth LE advertising is not supported") val server = manager.openGattServer(reactApplicationContext, serverCallback) ?: throw IllegalStateException("Unable to open the GATT server") gattServer = server advertiser = bleAdvertiser running = true if (!server.addService(buildService())) { server.close() gattServer = null advertiser = null running = false throw IllegalStateException("Unable to register the GATT service") } } } override fun stopGatt() { synchronized(lock) { running = false try { advertiser?.stopAdvertising(advertiseCallback) } catch (_: SecurityException) { // The adapter may already be off. } advertiser = null try { gattServer?.close() } catch (_: SecurityException) { // The adapter may already be off. } gattServer = null authorizedDevices.clear() subscribedDevices.clear() didBuffers.clear() } } override fun startClient(otp: String) { val clientDid = synchronized(lock) { did } if (clientDid.isEmpty()) { throw IllegalStateException("setDID must be called before startClient") } val gattClient = RTNbleGattClient( reactApplicationContext, clientDid, otp, onRecord = { serverDid, createdAt -> synchronized(lock) { record = Record(serverDid, createdAt) } invokeDidHandler(serverDid, createdAt) }, onError = { message -> val handler = synchronized(lock) { clientErrorHandler } handler?.invoke(message) } ) synchronized(lock) { val current = client if (current != null && current.isRunning) { throw IllegalStateException("The GATT client is already running") } client = gattClient } try { gattClient.start() } catch (error: Exception) { synchronized(lock) { client = null } throw error } } override fun stopClient() { val current = synchronized(lock) { val running = client client = null running } current?.stop() } override fun setClientErrorHandler(handler: Callback) { synchronized(lock) { clientErrorHandler = handler } } override fun invalidate() { stopClient() stopGatt() super.invalidate() } override fun hasServerPermissions(): Boolean { return BluetoothPermissions.serverGranted(reactApplicationContext) } override fun hasClientPermissions(): Boolean { return BluetoothPermissions.clientGranted(reactApplicationContext) } private fun buildService(): BluetoothGattService { val service = BluetoothGattService(GattProfile.SERVICE_UUID, BluetoothGattService.SERVICE_TYPE_PRIMARY) service.addCharacteristic( BluetoothGattCharacteristic( GattProfile.OTP_UUID, BluetoothGattCharacteristic.PROPERTY_WRITE, BluetoothGattCharacteristic.PERMISSION_WRITE ) ) service.addCharacteristic( notifyingCharacteristic( GattProfile.DID_UUID, BluetoothGattCharacteristic.PROPERTY_READ or BluetoothGattCharacteristic.PROPERTY_WRITE or BluetoothGattCharacteristic.PROPERTY_NOTIFY, BluetoothGattCharacteristic.PERMISSION_READ or BluetoothGattCharacteristic.PERMISSION_WRITE ) ) service.addCharacteristic( notifyingCharacteristic( GattProfile.RECORD_UUID, BluetoothGattCharacteristic.PROPERTY_READ or BluetoothGattCharacteristic.PROPERTY_NOTIFY, BluetoothGattCharacteristic.PERMISSION_READ ) ) return service } private fun notifyingCharacteristic( uuid: UUID, properties: Int, permissions: Int ): BluetoothGattCharacteristic { val characteristic = BluetoothGattCharacteristic(uuid, properties, permissions) characteristic.addDescriptor( BluetoothGattDescriptor( GattProfile.CCCD_UUID, BluetoothGattDescriptor.PERMISSION_READ or BluetoothGattDescriptor.PERMISSION_WRITE ) ) return characteristic } private fun startAdvertising() { val currentAdvertiser = synchronized(lock) { if (running) advertiser else null } ?: return try { currentAdvertiser.startAdvertising( AdvertiseSettings.Builder() .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_BALANCED) .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM) .setConnectable(true) .setTimeout(0) .build(), AdvertiseData.Builder() .addServiceUuid(ParcelUuid(GattProfile.SERVICE_UUID)) .setIncludeDeviceName(false) .build(), advertiseCallback ) } catch (error: SecurityException) { Log.e(TAG, "Unable to start BLE advertising", error) } } private fun respond( device: BluetoothDevice, requestId: Int, status: Int, offset: Int = 0, value: ByteArray? = null ) { try { gattServer?.sendResponse(device, requestId, status, offset, value) } catch (error: SecurityException) { Log.e(TAG, "Unable to send the GATT response", error) } } private fun disconnect(device: BluetoothDevice) { try { gattServer?.cancelConnection(device) } catch (error: SecurityException) { Log.e(TAG, "Unable to cancel the connection", error) } } private fun isAuthorized(device: BluetoothDevice): Boolean { synchronized(lock) { return device.address in authorizedDevices } } private fun isSubscribed(device: BluetoothDevice): Boolean { synchronized(lock) { return device.address in subscribedDevices } } private fun checkOTP(value: ByteArray): Boolean { val expected = synchronized(lock) { otp } ?: return false val offered = String(value, StandardCharsets.UTF_8).trim() return offered.isNotEmpty() && offered == expected.toString() } private fun appendDIDChunk(device: BluetoothDevice, value: ByteArray): String? { synchronized(lock) { val assembler = didBuffers.getOrPut(device.address) { ChunkAssembler() } val complete = assembler.append(value) ?: return null didBuffers.remove(device.address) return String(complete, StandardCharsets.UTF_8) } } private fun recordData(record: Record): ByteArray { return JSONObject() .put("did", record.did) .put("createdAt", record.createdAt) .toString() .toByteArray(StandardCharsets.UTF_8) } private fun sendDID(device: BluetoothDevice) { val payload = synchronized(lock) { did }.toByteArray(StandardCharsets.UTF_8) notifyFramed(device, GattProfile.DID_UUID, payload) } private fun sendRecord(device: BluetoothDevice, record: Record) { notifyFramed(device, GattProfile.RECORD_UUID, recordData(record)) } private fun notifyFramed(device: BluetoothDevice, characteristicUuid: UUID, payload: ByteArray) { if (!isSubscribed(device)) { return } val server = synchronized(lock) { gattServer } ?: return val characteristic = server.getService(GattProfile.SERVICE_UUID)?.getCharacteristic(characteristicUuid) ?: return for (frame in buildChunkFrames(payload)) { try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { server.notifyCharacteristicChanged(device, characteristic, false, frame) } else { @Suppress("DEPRECATION") characteristic.value = frame @Suppress("DEPRECATION") server.notifyCharacteristicChanged(device, characteristic, false) } } catch (error: SecurityException) { Log.e(TAG, "Unable to notify the central", error) return } } } private fun completeHandshake(device: BluetoothDevice, peerDid: String) { // The record time comes from this device's clock, never from the peer. val snapshot = Record(peerDid, IsoDatetime.format(System.currentTimeMillis())) synchronized(lock) { record = snapshot } sendRecord(device, snapshot) invokeDidHandler(snapshot.did, snapshot.createdAt) } private fun invokeDidHandler(didValue: String, createdAt: String) { val handler = synchronized(lock) { didHandler } handler?.invoke( Arguments.createMap().apply { putString("did", didValue) putString("createdAt", createdAt) } ) } private val advertiseCallback = object : AdvertiseCallback() { override fun onStartSuccess(settingsInEffect: AdvertiseSettings?) { Log.i(TAG, "GATT server is advertising") } override fun onStartFailure(errorCode: Int) { Log.e(TAG, "BLE advertising failed with error $errorCode") synchronized(lock) { running = false try { gattServer?.close() } catch (_: SecurityException) { // The adapter may already be off. } gattServer = null advertiser = null } } } private val serverCallback = object : BluetoothGattServerCallback() { override fun onServiceAdded(status: Int, service: BluetoothGattService) { if (status != BluetoothGatt.GATT_SUCCESS) { Log.e(TAG, "Unable to add the GATT service: $status") return } if (service.uuid == GattProfile.SERVICE_UUID) { startAdvertising() } } override fun onConnectionStateChange(device: BluetoothDevice, status: Int, newState: Int) { if (newState == BluetoothProfile.STATE_DISCONNECTED) { synchronized(lock) { authorizedDevices.remove(device.address) subscribedDevices.remove(device.address) didBuffers.remove(device.address) } } } override fun onDescriptorWriteRequest( device: BluetoothDevice, requestId: Int, descriptor: BluetoothGattDescriptor, preparedWrite: Boolean, responseNeeded: Boolean, offset: Int, value: ByteArray ) { if (descriptor.uuid == GattProfile.CCCD_UUID) { synchronized(lock) { if (value.contentEquals(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE)) { subscribedDevices.add(device.address) } else { subscribedDevices.remove(device.address) } } if (isSubscribed(device)) { when (descriptor.characteristic.uuid) { GattProfile.DID_UUID -> if (isAuthorized(device)) { sendDID(device) } GattProfile.RECORD_UUID -> synchronized(lock) { record }?.let { sendRecord(device, it) } } } } if (responseNeeded) { respond(device, requestId, BluetoothGatt.GATT_SUCCESS, offset) } } override fun onCharacteristicReadRequest( device: BluetoothDevice, requestId: Int, offset: Int, characteristic: BluetoothGattCharacteristic ) { val value = when (characteristic.uuid) { GattProfile.DID_UUID -> synchronized(lock) { did }.toByteArray(StandardCharsets.UTF_8) GattProfile.RECORD_UUID -> synchronized(lock) { record }?.let { recordData(it) } else -> null } if (value == null) { respond(device, requestId, BluetoothGatt.GATT_READ_NOT_PERMITTED, offset) return } if (offset > value.size) { respond(device, requestId, BluetoothGatt.GATT_INVALID_OFFSET, offset) return } respond( device, requestId, BluetoothGatt.GATT_SUCCESS, offset, value.copyOfRange(offset, value.size) ) } override fun onCharacteristicWriteRequest( device: BluetoothDevice, requestId: Int, characteristic: BluetoothGattCharacteristic, preparedWrite: Boolean, responseNeeded: Boolean, offset: Int, value: ByteArray ) { when (characteristic.uuid) { GattProfile.OTP_UUID -> { if (checkOTP(value)) { synchronized(lock) { authorizedDevices.add(device.address) } if (responseNeeded) { respond(device, requestId, BluetoothGatt.GATT_SUCCESS, offset) } sendDID(device) } else { synchronized(lock) { authorizedDevices.remove(device.address) didBuffers.remove(device.address) } if (responseNeeded) { respond(device, requestId, BluetoothGatt.GATT_FAILURE, offset) } disconnect(device) } } GattProfile.DID_UUID -> { if (!isAuthorized(device)) { if (responseNeeded) { respond(device, requestId, BluetoothGatt.GATT_INSUFFICIENT_AUTHENTICATION, offset) } disconnect(device) return } val peerDid = appendDIDChunk(device, value) if (responseNeeded) { respond(device, requestId, BluetoothGatt.GATT_SUCCESS, offset) } if (peerDid != null) { completeHandshake(device, peerDid) } } else -> { if (responseNeeded) { respond(device, requestId, BluetoothGatt.GATT_WRITE_NOT_PERMITTED, offset) } } } } } companion object { const val NAME = NativeRTNbleServerSpec.NAME private const val TAG = "RTNbleServer" } } private data class Record(val did: String, val createdAt: String)