Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
17 kB · 556 lines
Kotlin
at mistress
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557package com.rtnbleserver
import android.annotation.SuppressLintimport android.bluetooth.BluetoothDeviceimport android.bluetooth.BluetoothGattimport android.bluetooth.BluetoothGattCharacteristicimport android.bluetooth.BluetoothGattDescriptorimport android.bluetooth.BluetoothGattServerimport android.bluetooth.BluetoothGattServerCallbackimport android.bluetooth.BluetoothGattServiceimport android.bluetooth.BluetoothManagerimport android.bluetooth.BluetoothProfileimport android.bluetooth.le.AdvertiseCallbackimport android.bluetooth.le.AdvertiseDataimport android.bluetooth.le.AdvertiseSettingsimport android.bluetooth.le.BluetoothLeAdvertiserimport android.content.Contextimport android.os.Buildimport android.os.ParcelUuidimport android.util.Logimport com.facebook.react.bridge.Argumentsimport com.facebook.react.bridge.Callbackimport com.facebook.react.bridge.ReactApplicationContextimport java.nio.charset.StandardCharsetsimport java.util.UUIDimport 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<String>() private val subscribedDevices = mutableSetOf<String>() private val didBuffers = mutableMapOf<String, ChunkAssembler>()
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)