/** * BleNitroBleManager.kt * React Native BLE Nitro - Android Implementation * Copyright © 2025 Zyke (https://zyke.co) */ package co.zyke.ble import android.bluetooth.* import android.bluetooth.le.* import android.content.Context import android.content.pm.PackageManager import android.os.Build import android.util.Base64 import android.util.Log import androidx.core.content.ContextCompat import com.facebook.react.bridge.ReactApplicationContext import com.margelo.nitro.NitroModules import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel import no.nordicsemi.android.ble.BleManager import no.nordicsemi.android.ble.ktx.suspend import java.util.* import java.util.concurrent.ConcurrentHashMap import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException import kotlin.coroutines.suspendCoroutine // Import generated Nitro types import com.margelo.nitro.co.zyke.ble.* /** * Core BLE Manager implementation using Android BLE APIs * Implements the HybridBleManagerSpec interface generated by Nitro */ class BleNitroBleManager(private val context: ReactApplicationContext) : HybridBleManagerSpec { companion object { private const val TAG = "BleNitroBleManager" private const val DEFAULT_MTU = 23 } // Core BLE components private val bluetoothManager: BluetoothManager by lazy { context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager } private val bluetoothAdapter: BluetoothAdapter? by lazy { bluetoothManager.adapter } private val bleScanner: BluetoothLeScanner? by lazy { bluetoothAdapter?.bluetoothLeScanner } // State management private var logLevel: LogLevel = LogLevel.None private var isScanning = false private var scanCallback: ScanCallback? = null private var scanListener: ((NativeBleError?, NativeDevice?) -> Unit)? = null private var stateChangeListener: ((State) -> Unit)? = null // Device management private val connectedDevices = ConcurrentHashMap() private val discoveredDevices = ConcurrentHashMap() private val deviceCallbacks = ConcurrentHashMap() private val pendingOperations = ConcurrentHashMap() private val deviceDisconnectListeners = ConcurrentHashMap Unit>() private val characteristicMonitors = ConcurrentHashMap Unit>() // Coroutines private val coroutineScope = CoroutineScope(Dispatchers.Default + SupervisorJob()) // MARK: - HybridBleManagerSpec Implementation override fun destroy(): Promise = Promise.async { resolve, reject -> try { stopDeviceScan().get() // Disconnect all devices connectedDevices.values.forEach { gatt -> gatt.disconnect() gatt.close() } // Clear all state connectedDevices.clear() discoveredDevices.clear() deviceCallbacks.clear() pendingOperations.clear() deviceDisconnectListeners.clear() characteristicMonitors.clear() // Cancel coroutine scope coroutineScope.cancel() resolve(Unit) } catch (e: Exception) { reject(e) } } override fun setLogLevel(logLevel: LogLevel): Promise = Promise.resolve { this.logLevel = logLevel logLevel } override fun logLevel(): Promise = Promise.resolve(logLevel) override fun cancelTransaction(transactionId: String): Promise = Promise.resolve { pendingOperations.remove(transactionId) } override fun enable(transactionId: String?): Promise = Promise.async { resolve, reject -> if (bluetoothAdapter?.isEnabled == true) { resolve(Unit) } else { reject(createBleError(BleErrorCode.BluetoothPoweredOff, "Bluetooth is not enabled")) } } override fun disable(transactionId: String?): Promise = Promise.async { resolve, reject -> // Android doesn't allow programmatic disabling of Bluetooth reject(createBleError(BleErrorCode.BluetoothUnsupported, "Cannot programmatically disable Bluetooth")) } override fun state(): Promise = Promise.resolve { when { bluetoothAdapter == null -> State.Unsupported !bluetoothAdapter.isEnabled -> State.PoweredOff bluetoothAdapter.isEnabled -> State.PoweredOn else -> State.Unknown } } override fun onStateChange( listener: (State) -> Unit, emitCurrentState: Boolean? ): Subscription { stateChangeListener = listener if (emitCurrentState == true) { coroutineScope.launch { listener(state().get()) } } return object : Subscription { override fun remove() { stateChangeListener = null } } } override fun startDeviceScan( uuids: List?, options: ScanOptions?, listener: (NativeBleError?, NativeDevice?) -> Unit ): Promise = Promise.async { resolve, reject -> try { // Check permissions if (!hasBluetoothPermissions()) { reject(createBleError(BleErrorCode.BluetoothUnauthorized, "Missing Bluetooth permissions")) return@async } val scanner = bleScanner ?: run { reject(createBleError(BleErrorCode.BluetoothUnsupported, "BLE scanning not supported")) return@async } if (bluetoothAdapter?.isEnabled != true) { reject(createBleError(BleErrorCode.BluetoothPoweredOff, "Bluetooth is not enabled")) return@async } // Stop any existing scan stopCurrentScan() // Setup scan settings val scanSettings = ScanSettings.Builder() .setScanMode(mapScanMode(options?.scanMode)) .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) .build() // Setup scan filters val scanFilters = mutableListOf() uuids?.forEach { uuid -> val filter = ScanFilter.Builder() .setServiceUuid(ParcelUuid.fromString(uuid)) .build() scanFilters.add(filter) } // Create scan callback scanCallback = object : ScanCallback() { override fun onScanResult(callbackType: Int, result: ScanResult) { val device = createNativeDevice(result.device, result.scanRecord, result.rssi) discoveredDevices[device.id] = result.device listener(null, device) } override fun onScanFailed(errorCode: Int) { val error = createBleError(BleErrorCode.ScanStartFailed, "Scan failed with error: $errorCode") listener(error, null) } } scanListener = listener scanner.startScan(scanFilters, scanSettings, scanCallback) isScanning = true resolve(Unit) } catch (e: SecurityException) { reject(createBleError(BleErrorCode.BluetoothUnauthorized, "Permission denied: ${e.message}")) } catch (e: Exception) { reject(createBleError(BleErrorCode.ScanStartFailed, "Failed to start scan: ${e.message}")) } } override fun stopDeviceScan(): Promise = Promise.resolve { stopCurrentScan() } private fun stopCurrentScan() { if (isScanning) { try { scanCallback?.let { bleScanner?.stopScan(it) } } catch (e: SecurityException) { Log.w(TAG, "Permission denied when stopping scan: ${e.message}") } catch (e: Exception) { Log.w(TAG, "Error stopping scan: ${e.message}") } scanCallback = null scanListener = null isScanning = false } } override fun requestConnectionPriorityForDevice( deviceIdentifier: String, connectionPriority: ConnectionPriority, transactionId: String? ): Promise = Promise.async { resolve, reject -> val gatt = connectedDevices[deviceIdentifier] ?: run { reject(createBleError(BleErrorCode.DeviceNotFound, "Device not found: $deviceIdentifier")) return@async } try { val androidPriority = when (connectionPriority) { ConnectionPriority.Balanced -> BluetoothGatt.CONNECTION_PRIORITY_BALANCED ConnectionPriority.High -> BluetoothGatt.CONNECTION_PRIORITY_HIGH ConnectionPriority.LowPower -> BluetoothGatt.CONNECTION_PRIORITY_LOW_POWER } val success = gatt.requestConnectionPriority(androidPriority) if (success) { val device = createNativeDevice(gatt.device) resolve(device) } else { reject(createBleError(BleErrorCode.OperationCancelled, "Failed to request connection priority")) } } catch (e: Exception) { reject(createBleError(BleErrorCode.OperationCancelled, "Error requesting connection priority: ${e.message}")) } } override fun readRSSIForDevice( deviceIdentifier: String, transactionId: String? ): Promise = Promise.async { resolve, reject -> val gatt = connectedDevices[deviceIdentifier] ?: run { reject(createBleError(BleErrorCode.DeviceNotFound, "Device not found: $deviceIdentifier")) return@async } try { val callback = deviceCallbacks[deviceIdentifier] callback?.setRssiPromise(resolve, reject) if (!gatt.readRemoteRssi()) { reject(createBleError(BleErrorCode.OperationCancelled, "Failed to read RSSI")) } } catch (e: Exception) { reject(createBleError(BleErrorCode.OperationCancelled, "Error reading RSSI: ${e.message}")) } } override fun requestMTUForDevice( deviceIdentifier: String, mtu: Double, transactionId: String? ): Promise = Promise.async { resolve, reject -> val gatt = connectedDevices[deviceIdentifier] ?: run { reject(createBleError(BleErrorCode.DeviceNotFound, "Device not found: $deviceIdentifier")) return@async } try { val callback = deviceCallbacks[deviceIdentifier] callback?.setMtuPromise(resolve, reject) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { if (!gatt.requestMtu(mtu.toInt())) { reject(createBleError(BleErrorCode.OperationCancelled, "Failed to request MTU")) } } else { // MTU request not supported on older versions val device = createNativeDevice(gatt.device) resolve(device) } } catch (e: Exception) { reject(createBleError(BleErrorCode.OperationCancelled, "Error requesting MTU: ${e.message}")) } } override fun devices(deviceIdentifiers: List): Promise> = Promise.resolve { deviceIdentifiers.mapNotNull { identifier -> val bluetoothDevice = discoveredDevices[identifier] ?: connectedDevices[identifier]?.device bluetoothDevice?.let { createNativeDevice(it) } } } override fun connectedDevices(serviceUUIDs: List): Promise> = Promise.resolve { val uuids = serviceUUIDs.map { UUID.fromString(it) } bluetoothManager.getConnectedDevices(BluetoothProfile.GATT) .filter { device -> serviceUUIDs.isEmpty() || device.uuids?.any { uuid -> uuids.contains(uuid.uuid) } == true } .map { createNativeDevice(it) } } override fun connectToDevice( deviceIdentifier: String, options: ConnectionOptions? ): Promise = Promise.async { resolve, reject -> try { val device = discoveredDevices[deviceIdentifier] ?: run { // Try to get device by MAC address if (BluetoothAdapter.checkBluetoothAddress(deviceIdentifier)) { bluetoothAdapter?.getRemoteDevice(deviceIdentifier) } else null } ?: run { reject(createBleError(BleErrorCode.DeviceNotFound, "Device not found: $deviceIdentifier")) return@async } // Check if already connected if (connectedDevices.containsKey(deviceIdentifier)) { val nativeDevice = createNativeDevice(device) resolve(nativeDevice) return@async } val callback = BleGattCallback(deviceIdentifier) { error, device -> if (error != null) { reject(error) } else if (device != null) { resolve(device) } } deviceCallbacks[deviceIdentifier] = callback val autoConnect = options?.autoConnect ?: false val gatt = device.connectGatt(context, autoConnect, callback) if (gatt == null) { reject(createBleError(BleErrorCode.DeviceConnectionFailed, "Failed to create GATT connection")) return@async } } catch (e: SecurityException) { reject(createBleError(BleErrorCode.BluetoothUnauthorized, "Permission denied: ${e.message}")) } catch (e: Exception) { reject(createBleError(BleErrorCode.DeviceConnectionFailed, "Connection failed: ${e.message}")) } } override fun cancelDeviceConnection(deviceIdentifier: String): Promise = Promise.async { resolve, reject -> val gatt = connectedDevices[deviceIdentifier] ?: run { reject(createBleError(BleErrorCode.DeviceNotFound, "Device not found: $deviceIdentifier")) return@async } try { val callback = deviceCallbacks[deviceIdentifier] callback?.setDisconnectPromise(resolve, reject) gatt.disconnect() } catch (e: Exception) { reject(createBleError(BleErrorCode.OperationCancelled, "Error cancelling connection: ${e.message}")) } } override fun onDeviceDisconnected( deviceIdentifier: String, listener: (NativeBleError?, NativeDevice?) -> Unit ): Subscription { deviceDisconnectListeners[deviceIdentifier] = listener return object : Subscription { override fun remove() { deviceDisconnectListeners.remove(deviceIdentifier) } } } override fun isDeviceConnected(deviceIdentifier: String): Promise = Promise.resolve { connectedDevices.containsKey(deviceIdentifier) } override fun discoverAllServicesAndCharacteristicsForDevice( deviceIdentifier: String, transactionId: String? ): Promise = Promise.async { resolve, reject -> val gatt = connectedDevices[deviceIdentifier] ?: run { reject(createBleError(BleErrorCode.DeviceNotFound, "Device not found: $deviceIdentifier")) return@async } try { val callback = deviceCallbacks[deviceIdentifier] callback?.setServiceDiscoveryPromise(resolve, reject) if (!gatt.discoverServices()) { reject(createBleError(BleErrorCode.OperationCancelled, "Failed to start service discovery")) } } catch (e: Exception) { reject(createBleError(BleErrorCode.OperationCancelled, "Error discovering services: ${e.message}")) } } override fun servicesForDevice(deviceIdentifier: String): Promise> = Promise.resolve { val gatt = connectedDevices[deviceIdentifier] ?: return@resolve emptyList() gatt.services.mapIndexed { index, service -> NativeService( id = index.toDouble(), uuid = service.uuid.toString(), deviceID = deviceIdentifier, isPrimary = service.type == BluetoothGattService.SERVICE_TYPE_PRIMARY ) } } // Additional methods implementation would continue here... // For brevity, showing the core pattern. The full implementation would include // all remaining methods from the HybridBleManagerSpec interface. // MARK: - Helper Methods private fun hasBluetoothPermissions(): Boolean { val permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { arrayOf( android.Manifest.permission.BLUETOOTH_SCAN, android.Manifest.permission.BLUETOOTH_CONNECT ) } else { arrayOf( android.Manifest.permission.BLUETOOTH, android.Manifest.permission.BLUETOOTH_ADMIN, android.Manifest.permission.ACCESS_FINE_LOCATION ) } return permissions.all { permission -> ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED } } private fun mapScanMode(scanMode: ScanMode?): Int { return when (scanMode) { ScanMode.Opportunistic -> ScanSettings.SCAN_MODE_OPPORTUNISTIC ScanMode.LowPower -> ScanSettings.SCAN_MODE_LOW_POWER ScanMode.Balanced -> ScanSettings.SCAN_MODE_BALANCED ScanMode.LowLatency -> ScanSettings.SCAN_MODE_LOW_LATENCY null -> ScanSettings.SCAN_MODE_LOW_LATENCY } } private fun createNativeDevice( device: BluetoothDevice, scanRecord: ScanRecord? = null, rssi: Int? = null ): NativeDevice { val serviceData = mutableListOf() var serviceUUIDs: List? = null var manufacturerData: String? = null var localName: String? = null var txPowerLevel: Double? = null var isConnectable: Boolean? = null scanRecord?.let { record -> // Parse service UUIDs serviceUUIDs = record.serviceUuids?.map { it.toString() } // Parse manufacturer data record.manufacturerSpecificData?.let { data -> if (data.size() > 0) { val key = data.keyAt(0) val value = data.get(key) manufacturerData = Base64.encodeToString(value, Base64.NO_WRAP) } } // Parse service data record.serviceData?.forEach { (uuid, data) -> serviceData.add(ServiceDataEntry(uuid.toString(), Base64.encodeToString(data, Base64.NO_WRAP))) } localName = record.deviceName txPowerLevel = record.txPowerLevel.toDouble().takeIf { it != Int.MIN_VALUE } } return NativeDevice( id = device.address, deviceName = device.name ?: localName, rssi = rssi?.toDouble(), mtu = DEFAULT_MTU.toDouble(), manufacturerData = manufacturerData, serviceData = serviceData, serviceUUIDs = serviceUUIDs, localName = localName, txPowerLevel = txPowerLevel, solicitedServiceUUIDs = null, // Not available in Android ScanRecord isConnectable = isConnectable, overflowServiceUUIDs = null, // Not available in Android ScanRecord rawScanRecord = scanRecord?.bytes?.let { Base64.encodeToString(it, Base64.NO_WRAP) } ?: "" ) } private fun createBleError( errorCode: BleErrorCode, message: String, deviceId: String? = null ): NativeBleError { return NativeBleError( errorCode = errorCode, attErrorCode = null, iosErrorCode = null, androidErrorCode = null, reason = message, deviceID = deviceId, serviceUUID = null, characteristicUUID = null, descriptorUUID = null, internalMessage = message ) } // MARK: - GATT Callback Implementation private inner class BleGattCallback( private val deviceId: String, private val connectionCallback: (NativeBleError?, NativeDevice?) -> Unit ) : BluetoothGattCallback() { private var disconnectPromise: ((NativeDevice) -> Unit, (Throwable) -> Unit)? = null private var rssiPromise: ((NativeDevice) -> Unit, (Throwable) -> Unit)? = null private var mtuPromise: ((NativeDevice) -> Unit, (Throwable) -> Unit)? = null private var serviceDiscoveryPromise: ((NativeDevice) -> Unit, (Throwable) -> Unit)? = null fun setDisconnectPromise(resolve: (NativeDevice) -> Unit, reject: (Throwable) -> Unit) { disconnectPromise = Pair(resolve, reject) } fun setRssiPromise(resolve: (NativeDevice) -> Unit, reject: (Throwable) -> Unit) { rssiPromise = Pair(resolve, reject) } fun setMtuPromise(resolve: (NativeDevice) -> Unit, reject: (Throwable) -> Unit) { mtuPromise = Pair(resolve, reject) } fun setServiceDiscoveryPromise(resolve: (NativeDevice) -> Unit, reject: (Throwable) -> Unit) { serviceDiscoveryPromise = Pair(resolve, reject) } override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { when (newState) { BluetoothProfile.STATE_CONNECTED -> { connectedDevices[deviceId] = gatt val device = createNativeDevice(gatt.device) connectionCallback(null, device) } BluetoothProfile.STATE_DISCONNECTED -> { connectedDevices.remove(deviceId) deviceCallbacks.remove(deviceId) val device = createNativeDevice(gatt.device) val error = if (status != BluetoothGatt.GATT_SUCCESS) { createBleError(BleErrorCode.DeviceDisconnected, "Device disconnected with status: $status", deviceId) } else null // Notify disconnect listeners deviceDisconnectListeners[deviceId]?.invoke(error, device) // Handle pending disconnect promise disconnectPromise?.let { (resolve, _) -> resolve(device) disconnectPromise = null } gatt.close() } } } override fun onReadRemoteRssi(gatt: BluetoothGatt, rssi: Int, status: Int) { rssiPromise?.let { (resolve, reject) -> if (status == BluetoothGatt.GATT_SUCCESS) { val device = createNativeDevice(gatt.device, rssi = rssi) resolve(device) } else { reject(createBleError(BleErrorCode.OperationCancelled, "Failed to read RSSI: $status")) } rssiPromise = null } } override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) { mtuPromise?.let { (resolve, reject) -> if (status == BluetoothGatt.GATT_SUCCESS) { val device = createNativeDevice(gatt.device) resolve(device) } else { reject(createBleError(BleErrorCode.OperationCancelled, "Failed to change MTU: $status")) } mtuPromise = null } } override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { serviceDiscoveryPromise?.let { (resolve, reject) -> if (status == BluetoothGatt.GATT_SUCCESS) { val device = createNativeDevice(gatt.device) resolve(device) } else { reject(createBleError(BleErrorCode.ServicesNotDiscovered, "Service discovery failed: $status")) } serviceDiscoveryPromise = null } } // Additional callback methods for characteristic and descriptor operations would be here... } }