Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
12 kB · 400 lines
Kotlin
at mistress
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401package com.rtnbleserver
import android.annotation.SuppressLintimport android.bluetooth.BluetoothDeviceimport android.bluetooth.BluetoothGattimport android.bluetooth.BluetoothGattCallbackimport android.bluetooth.BluetoothGattCharacteristicimport android.bluetooth.BluetoothGattDescriptorimport android.bluetooth.BluetoothManagerimport android.bluetooth.BluetoothProfileimport android.bluetooth.le.BluetoothLeScannerimport android.bluetooth.le.ScanCallbackimport android.bluetooth.le.ScanFilterimport android.bluetooth.le.ScanResultimport android.bluetooth.le.ScanSettingsimport android.content.Contextimport android.os.Handlerimport android.os.Looperimport android.os.ParcelUuidimport android.util.Logimport java.nio.charset.StandardCharsetsimport java.util.UUIDimport org.json.JSONObject
/** * One-shot GATT client: scans for the service, authenticates with the OTP, sends * this device's DID, and reports the DID and timestamp sent back by the server. * It never opens a GATT server. */@SuppressLint("MissingPermission")internal class RTNbleGattClient( private val context: Context, private val did: String, private val otp: String, private val onRecord: (serverDid: String, createdAt: String) -> Unit, private val onError: (message: String) -> Unit) {
private enum class State { IDLE, SCANNING, CONNECTING, DISCOVERING, ENABLING_DID, ENABLING_RECORD, WRITING_OTP, WRITING_DID, WAITING_RECORD }
private val mainHandler = Handler(Looper.getMainLooper()) private val lock = Any()
private val manager = context.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager
private var state = State.IDLE private var running = true
private var scanner: BluetoothLeScanner? = null private var gatt: BluetoothGatt? = null
private var otpCharacteristic: BluetoothGattCharacteristic? = null private var didCharacteristic: BluetoothGattCharacteristic? = null private var recordCharacteristic: BluetoothGattCharacteristic? = null
private val didAssembler = ChunkAssembler() private val recordAssembler = ChunkAssembler() private var serverDid: String? = null private var recordCreatedAt: String? = null
private var didFrames: List<ByteArray> = emptyList() private var didFrameIndex = 0
val isRunning: Boolean get() = synchronized(lock) { running }
fun start() { val adapter = manager?.adapter ?: throw IllegalStateException("Bluetooth is not supported on this device")
if (!BluetoothPermissions.clientGranted(context)) { throw SecurityException("BLUETOOTH_SCAN and BLUETOOTH_CONNECT permissions are required") } if (!adapter.isEnabled) { throw IllegalStateException("Bluetooth is turned off") }
val bleScanner = adapter.bluetoothLeScanner ?: throw IllegalStateException("Bluetooth LE scanning is not supported")
scanner = bleScanner synchronized(lock) { state = State.SCANNING }
bleScanner.startScan( listOf(ScanFilter.Builder().setServiceUuid(ParcelUuid(GattProfile.SERVICE_UUID)).build()), ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build(), scanCallback )
mainHandler.postDelayed(timeoutRunnable, TIMEOUT_MS) }
fun stop() { synchronized(lock) { if (!running) { return } running = false state = State.IDLE }
mainHandler.removeCallbacks(timeoutRunnable) stopTransport() }
private fun stopTransport() { try { scanner?.stopScan(scanCallback) } catch (_: SecurityException) { // The adapter may already be off. } scanner = null
val current = gatt gatt = null try { current?.disconnect() } catch (_: SecurityException) { // The adapter may already be off. } try { current?.close() } catch (_: SecurityException) { // The adapter may already be off. } }
private fun fail(message: String) { synchronized(lock) { if (!running) { return } running = false state = State.IDLE }
mainHandler.removeCallbacks(timeoutRunnable) stopTransport() Log.e(TAG, message) onError(message) }
private fun complete() { val result = synchronized(lock) { if (!running) { return } val serverDID = serverDid ?: return val createdAt = recordCreatedAt ?: return running = false state = State.IDLE serverDID to createdAt }
mainHandler.removeCallbacks(timeoutRunnable) stopTransport() onRecord(result.first, result.second) }
private fun maybeComplete() { synchronized(lock) { if (!running || serverDid == null || recordCreatedAt == null) { return } } complete() }
private val timeoutRunnable = Runnable { fail("Timed out waiting for the GATT server") }
private val scanCallback = object : ScanCallback() { override fun onScanResult(callbackType: Int, result: ScanResult) { synchronized(lock) { if (!running || state != State.SCANNING) { return } state = State.CONNECTING }
try { scanner?.stopScan(this) } catch (_: SecurityException) { // Ignore; the connection below reports the real failure. }
val connection = try { result.device.connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_LE) } catch (error: SecurityException) { null }
if (connection == null) { fail("Unable to connect to the GATT server") return }
gatt = connection }
override fun onScanFailed(errorCode: Int) { fail("BLE scan failed with error $errorCode") } }
private val gattCallback = object : BluetoothGattCallback() { override fun onConnectionStateChange( gatt: BluetoothGatt, status: Int, newState: Int ) { if (newState == BluetoothProfile.STATE_CONNECTED) { mainHandler.post { gatt.discoverServices() } } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { if (isRunning) { fail("Disconnected from the GATT server") } } }
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { if (status != BluetoothGatt.GATT_SUCCESS) { fail("Service discovery failed with status $status") return }
val service = gatt.getService(GattProfile.SERVICE_UUID) ?: return fail("GATT service not found") val otp = service.getCharacteristic(GattProfile.OTP_UUID) ?: return fail("OTP characteristic not found") val didCharacteristic = service.getCharacteristic(GattProfile.DID_UUID) ?: return fail("DID characteristic not found") val recordCharacteristic = service.getCharacteristic(GattProfile.RECORD_UUID) ?: return fail("Record characteristic not found")
otpCharacteristic = otp this@RTNbleGattClient.didCharacteristic = didCharacteristic this@RTNbleGattClient.recordCharacteristic = recordCharacteristic
synchronized(lock) { state = State.DISCOVERING } enableNotifications(didCharacteristic, State.ENABLING_DID) }
override fun onDescriptorWrite( gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int ) { if (status != BluetoothGatt.GATT_SUCCESS) { fail("Unable to enable notifications: status $status") return }
when (synchronized(lock) { state }) { State.ENABLING_DID -> recordCharacteristic?.let { enableNotifications(it, State.ENABLING_RECORD) } ?: fail("Record characteristic not found") State.ENABLING_RECORD -> writeOTP() else -> Unit } }
override fun onCharacteristicWrite( gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int ) { if (status != BluetoothGatt.GATT_SUCCESS) { fail("GATT write failed with status $status") return }
when (characteristic.uuid) { GattProfile.OTP_UUID, GattProfile.DID_UUID -> writeNextDIDFrame() else -> Unit } }
@Deprecated("Deprecated in API 33") override fun onCharacteristicChanged( gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic ) { handleNotification(characteristic.uuid, characteristic.value ?: ByteArray(0)) }
override fun onCharacteristicChanged( gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, value: ByteArray ) { handleNotification(characteristic.uuid, value) } }
private fun enableNotifications( characteristic: BluetoothGattCharacteristic, nextState: State ) { val current = gatt ?: return fail("Connection lost")
current.setCharacteristicNotification(characteristic, true) val descriptor = characteristic.getDescriptor(GattProfile.CCCD_UUID) ?: return fail("Notification descriptor not found") descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
synchronized(lock) { state = nextState } if (!current.writeDescriptor(descriptor)) { fail("Unable to enable notifications") } }
private fun writeOTP() { val current = gatt ?: return fail("Connection lost") val characteristic = otpCharacteristic ?: return fail("OTP characteristic not found")
characteristic.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT characteristic.value = otp.toByteArray(StandardCharsets.UTF_8)
synchronized(lock) { state = State.WRITING_OTP } if (!current.writeCharacteristic(characteristic)) { fail("Unable to write the OTP") } }
private fun writeNextDIDFrame() { val current = gatt ?: return fail("Connection lost") val characteristic = didCharacteristic ?: return fail("DID characteristic not found")
if (didFrames.isEmpty()) { didFrames = buildChunkFrames(did.toByteArray(StandardCharsets.UTF_8)) }
val index = synchronized(lock) { if (didFrameIndex >= didFrames.size) { state = State.WAITING_RECORD return } val currentIndex = didFrameIndex didFrameIndex += 1 currentIndex }
characteristic.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT characteristic.value = didFrames[index]
synchronized(lock) { state = State.WRITING_DID } if (!current.writeCharacteristic(characteristic)) { fail("Unable to write the DID") } }
private fun handleNotification(uuid: UUID, value: ByteArray) { when (uuid) { GattProfile.DID_UUID -> { val complete = didAssembler.append(value) if (complete != null) { synchronized(lock) { serverDid = String(complete, StandardCharsets.UTF_8) } } maybeComplete() }
GattProfile.RECORD_UUID -> { val complete = recordAssembler.append(value) if (complete != null) { parseCreatedAt(String(complete, StandardCharsets.UTF_8))?.let { createdAt -> synchronized(lock) { recordCreatedAt = createdAt } } } maybeComplete() } } }
private fun parseCreatedAt(raw: String): String? { return try { val value = JSONObject(raw).optString("createdAt") if (value.isEmpty()) null else IsoDatetime.parse(value) } catch (_: Exception) { null } }
companion object { private const val TAG = "RTNbleGattClient" private const val TIMEOUT_MS = 15_000L }}