package com.munimbluetooth import android.bluetooth.* import android.bluetooth.le.* 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.WritableArray import com.facebook.react.bridge.WritableMap import com.facebook.react.modules.core.DeviceEventManagerModule import com.margelo.nitro.NitroModules import com.margelo.nitro.munimbluetooth.HybridMunimBluetoothSpec import kotlinx.coroutines.* import java.util.* class HybridMunimBluetooth : HybridMunimBluetoothSpec() { private val TAG = "HybridMunimBluetooth" // Peripheral Manager private var advertiser: BluetoothLeAdvertiser? = null private var gattServer: BluetoothGattServer? = null private var gattServerReady = false private var advertiseJob: Job? = null private var currentAdvertisingData: Map? = null private var bluetoothManager: BluetoothManager? = null private var bluetoothAdapter: BluetoothAdapter? = null // Central Manager private var bluetoothLeScanner: BluetoothLeScanner? = null private var scanCallback: ScanCallback? = null private var isScanning = false private val discoveredDevices = mutableMapOf() private val connectedDevices = mutableMapOf() private val deviceCharacteristics = mutableMapOf>() private val eventEmitter = NitroEventEmitter(TAG) init { // Initialize Bluetooth managers - this would need ReactApplicationContext in real implementation // For now, we'll initialize them when needed } private fun getBluetoothManager(): BluetoothManager? { val context = NitroModules.applicationContext ?: return null return context.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager } private fun ensureBluetoothManager() { if (bluetoothManager == null) { bluetoothManager = getBluetoothManager() bluetoothAdapter = bluetoothManager?.adapter } } // MARK: - Peripheral Features override fun startAdvertising(options: Map) { ensureBluetoothManager() val adapter = bluetoothAdapter if (adapter == null || !adapter.isEnabled) { Log.e(TAG, "Bluetooth is not enabled or not available") return } val serviceUUIDs = options["serviceUUIDs"] as? List if (serviceUUIDs == null || serviceUUIDs.isEmpty()) { Log.e(TAG, "No service UUIDs provided for advertising") return } // Ensure GATT server is set up before advertising if (!gattServerReady) { setServicesFromOptions(serviceUUIDs) } // Cancel any previous advertising job advertiseJob?.cancel() advertiseJob = CoroutineScope(Dispatchers.Main).launch { delay(300) // Wait for GATT server to be ready advertiser = adapter.bluetoothLeAdvertiser val settings = AdvertiseSettings.Builder() .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY) .setConnectable(true) .setTimeout(0) .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH) .build() val dataBuilder = AdvertiseData.Builder() // Process comprehensive advertising data val advertisingDataMap = options["advertisingData"] as? Map if (advertisingDataMap != null) { processAdvertisingData(advertisingDataMap, dataBuilder) } // Legacy support - add service UUIDs for (uuid in serviceUUIDs) { dataBuilder.addServiceUuid(ParcelUuid.fromString(uuid)) } // Legacy support - local name val localName = options["localName"] as? String if (localName != null) { dataBuilder.setIncludeDeviceName(true) } // Legacy support - manufacturer data val manufacturerData = options["manufacturerData"] as? String if (manufacturerData != null) { val data = hexStringToByteArray(manufacturerData) if (data != null) { dataBuilder.addManufacturerData(0x0000, data) // Default manufacturer code } } currentAdvertisingData = mapOf( "advertisingData" to (advertisingDataMap ?: emptyMap()), "localName" to (localName ?: ""), "manufacturerData" to (manufacturerData ?: "") ) advertiser?.startAdvertising(settings, dataBuilder.build(), object : AdvertiseCallback() { override fun onStartSuccess(settingsInEffect: AdvertiseSettings) { Log.i(TAG, "Advertising started successfully") } override fun onStartFailure(errorCode: Int) { Log.e(TAG, "Advertising failed: $errorCode") } }) } } override fun updateAdvertisingData(advertisingData: Map) { ensureBluetoothManager() val adapter = bluetoothAdapter if (adapter == null || !adapter.isEnabled) { Log.e(TAG, "Bluetooth is not enabled or not available") return } advertiser?.stopAdvertising(object : AdvertiseCallback() {}) advertiseJob?.cancel() advertiseJob = CoroutineScope(Dispatchers.Main).launch { delay(100) // Brief delay before restarting advertiser = adapter.bluetoothLeAdvertiser val settings = AdvertiseSettings.Builder() .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY) .setConnectable(true) .setTimeout(0) .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH) .build() val dataBuilder = AdvertiseData.Builder() processAdvertisingData(advertisingData, dataBuilder) currentAdvertisingData = mapOf("advertisingData" to advertisingData) advertiser?.startAdvertising(settings, dataBuilder.build(), object : AdvertiseCallback() { override fun onStartSuccess(settingsInEffect: AdvertiseSettings) { Log.i(TAG, "Advertising updated successfully") } override fun onStartFailure(errorCode: Int) { Log.e(TAG, "Advertising update failed: $errorCode") } }) } } override fun getAdvertisingData(): Map { return currentAdvertisingData ?: emptyMap() } override fun stopAdvertising() { advertiser?.stopAdvertising(object : AdvertiseCallback() {}) advertiser = null advertiseJob?.cancel() currentAdvertisingData = null } override fun setServices(services: List>) { ensureBluetoothManager() gattServerReady = false val manager = bluetoothManager ?: return gattServer = manager.openGattServer(null, object : BluetoothGattServerCallback() { override fun onConnectionStateChange(device: BluetoothDevice, status: Int, newState: Int) { // Handle connection state changes } override fun onCharacteristicReadRequest( device: BluetoothDevice, requestId: Int, offset: Int, characteristic: BluetoothGattCharacteristic ) { gattServer?.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, characteristic.value) } override fun onCharacteristicWriteRequest( device: BluetoothDevice, requestId: Int, characteristic: BluetoothGattCharacteristic, preparedWrite: Boolean, responseNeeded: Boolean, offset: Int, value: ByteArray? ) { if (responseNeeded) { gattServer?.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, null) } } }) gattServer?.clearServices() for (serviceMap in services) { val serviceUuid = serviceMap["uuid"] as? String ?: continue val service = BluetoothGattService( UUID.fromString(serviceUuid), BluetoothGattService.SERVICE_TYPE_PRIMARY ) val characteristics = serviceMap["characteristics"] as? List> if (characteristics != null) { for (charMap in characteristics) { val charUuid = charMap["uuid"] as? String ?: continue val propertiesArray = charMap["properties"] as? List var properties = 0 if (propertiesArray != null) { for (prop in propertiesArray) { when (prop) { "read" -> properties = properties or BluetoothGattCharacteristic.PROPERTY_READ "write" -> properties = properties or BluetoothGattCharacteristic.PROPERTY_WRITE "notify" -> properties = properties or BluetoothGattCharacteristic.PROPERTY_NOTIFY "indicate" -> properties = properties or BluetoothGattCharacteristic.PROPERTY_INDICATE } } } val permissions = BluetoothGattCharacteristic.PERMISSION_READ or BluetoothGattCharacteristic.PERMISSION_WRITE val characteristic = BluetoothGattCharacteristic( UUID.fromString(charUuid), properties, permissions ) val value = charMap["value"] as? String if (value != null) { characteristic.value = value.toByteArray() } service.addCharacteristic(characteristic) } } gattServer?.addService(service) } gattServerReady = true } // MARK: - Central Features override fun isBluetoothEnabled(): Boolean { ensureBluetoothManager() return bluetoothAdapter?.isEnabled == true } override fun requestBluetoothPermission(): Boolean { // On Android, permissions are requested at runtime // This would need Activity context in real implementation return true } override fun startScan(options: Map?) { ensureBluetoothManager() val adapter = bluetoothAdapter if (adapter == null || !adapter.isEnabled) { Log.e(TAG, "Bluetooth is not enabled or not available") return } if (isScanning) return isScanning = true discoveredDevices.clear() val scanner = adapter.bluetoothLeScanner bluetoothLeScanner = scanner val serviceUUIDs = options?.get("serviceUUIDs") as? List val scanFilters = if (serviceUUIDs != null && serviceUUIDs.isNotEmpty()) { serviceUUIDs.map { uuid -> ScanFilter.Builder() .setServiceUuid(ParcelUuid.fromString(uuid)) .build() } } else { emptyList() } val scanMode = when (options?.get("scanMode") as? String) { "lowPower" -> ScanSettings.SCAN_MODE_LOW_POWER "balanced" -> ScanSettings.SCAN_MODE_BALANCED "lowLatency" -> ScanSettings.SCAN_MODE_LOW_LATENCY else -> ScanSettings.SCAN_MODE_BALANCED } val scanSettings = ScanSettings.Builder() .setScanMode(scanMode) .build() scanCallback = object : ScanCallback() { override fun onScanResult(callbackType: Int, result: ScanResult) { val device = result.device val deviceId = device.address discoveredDevices[deviceId] = device eventEmitter.emit( "deviceFound", buildScanPayload(result) ) } override fun onBatchScanResults(results: MutableList) { for (result in results) { onScanResult(ScanCallback.SCAN_RESULT_TYPE_BATCH, result) } } override fun onScanFailed(errorCode: Int) { Log.e(TAG, "Scan failed: $errorCode") isScanning = false } } scanner.startScan(scanFilters, scanSettings, scanCallback) } override fun stopScan() { if (!isScanning) return bluetoothLeScanner?.stopScan(scanCallback) bluetoothLeScanner = null scanCallback = null isScanning = false } override fun connect(deviceId: String) { val device = discoveredDevices[deviceId] if (device == null) { Log.e(TAG, "Device not found: $deviceId") return } val gatt = device.connectGatt(null, false, object : BluetoothGattCallback() { override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { if (newState == BluetoothProfile.STATE_CONNECTED) { connectedDevices[deviceId] = gatt gatt.discoverServices() // Emit deviceConnected event } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { connectedDevices.remove(deviceId) deviceCharacteristics.remove(deviceId) // Emit deviceDisconnected event } } override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { if (status == BluetoothGatt.GATT_SUCCESS) { val characteristics = mutableListOf() for (service in gatt.services) { characteristics.addAll(service.characteristics) } deviceCharacteristics[deviceId] = characteristics // Emit servicesDiscovered event } } override fun onCharacteristicRead( gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int ) { if (status == BluetoothGatt.GATT_SUCCESS) { val hexValue = characteristic.value?.joinToString("") { "%02x".format(it) } ?: "" // Emit characteristicValueChanged event } } override fun onCharacteristicWrite( gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int ) { if (status == BluetoothGatt.GATT_SUCCESS) { // Emit writeSuccess event } else { // Emit writeError event } } override fun onCharacteristicChanged( gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic ) { val hexValue = characteristic.value?.joinToString("") { "%02x".format(it) } ?: "" // Emit characteristicValueChanged event } override fun onReadRemoteRssi(gatt: BluetoothGatt, rssi: Int, status: Int) { if (status == BluetoothGatt.GATT_SUCCESS) { // Emit rssiUpdated event } } }) } override fun disconnect(deviceId: String) { val gatt = connectedDevices[deviceId] gatt?.disconnect() gatt?.close() connectedDevices.remove(deviceId) deviceCharacteristics.remove(deviceId) } override fun discoverServices(deviceId: String): List> { val gatt = connectedDevices[deviceId] if (gatt == null) { Log.e(TAG, "Device not connected: $deviceId") return emptyList() } gatt.discoverServices() // Services will be discovered via callback // Return empty list for now return emptyList() } override fun readCharacteristic( deviceId: String, serviceUUID: String, characteristicUUID: String ): Map { val gatt = connectedDevices[deviceId] if (gatt == null) { Log.e(TAG, "Device not connected: $deviceId") return emptyMap() } val characteristics = deviceCharacteristics[deviceId] ?: return emptyMap() val characteristic = characteristics.firstOrNull { it.service?.uuid.toString() == serviceUUID && it.uuid.toString() == characteristicUUID } if (characteristic == null) { Log.e(TAG, "Characteristic not found") return emptyMap() } gatt.readCharacteristic(characteristic) return mapOf( "value" to "", "serviceUUID" to serviceUUID, "characteristicUUID" to characteristicUUID ) } override fun writeCharacteristic( deviceId: String, serviceUUID: String, characteristicUUID: String, value: String, writeType: String? ) { val gatt = connectedDevices[deviceId] if (gatt == null) { Log.e(TAG, "Device not connected: $deviceId") return } val characteristics = deviceCharacteristics[deviceId] ?: return val characteristic = characteristics.firstOrNull { it.service?.uuid.toString() == serviceUUID && it.uuid.toString() == characteristicUUID } if (characteristic == null) { Log.e(TAG, "Characteristic not found") return } val data = hexStringToByteArray(value) ?: return characteristic.value = data val writeTypeValue = if (writeType == "writeWithoutResponse") { BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE } else { BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT } characteristic.writeType = writeTypeValue gatt.writeCharacteristic(characteristic) } override fun subscribeToCharacteristic( deviceId: String, serviceUUID: String, characteristicUUID: String ) { val gatt = connectedDevices[deviceId] if (gatt == null) { Log.e(TAG, "Device not connected: $deviceId") return } val characteristics = deviceCharacteristics[deviceId] ?: return val characteristic = characteristics.firstOrNull { it.service?.uuid.toString() == serviceUUID && it.uuid.toString() == characteristicUUID } if (characteristic == null) { Log.e(TAG, "Characteristic not found") return } gatt.setCharacteristicNotification(characteristic, true) // Enable notification descriptor val descriptor = characteristic.getDescriptor( UUID.fromString("00002902-0000-1000-8000-00805f9b34fb") ) descriptor?.let { it.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE gatt.writeDescriptor(it) } } override fun unsubscribeFromCharacteristic( deviceId: String, serviceUUID: String, characteristicUUID: String ) { val gatt = connectedDevices[deviceId] if (gatt == null) { Log.e(TAG, "Device not connected: $deviceId") return } val characteristics = deviceCharacteristics[deviceId] ?: return val characteristic = characteristics.firstOrNull { it.service?.uuid.toString() == serviceUUID && it.uuid.toString() == characteristicUUID } if (characteristic == null) { Log.e(TAG, "Characteristic not found") return } gatt.setCharacteristicNotification(characteristic, false) } override fun getConnectedDevices(): List { return connectedDevices.keys.toList() } override fun readRSSI(deviceId: String): Double { val gatt = connectedDevices[deviceId] if (gatt == null) { Log.e(TAG, "Device not connected: $deviceId") return 0.0 } gatt.readRemoteRssi() // RSSI will come via callback return 0.0 } // MARK: - Event Management override fun addListener(eventName: String) { // Event listeners are handled by the event emitter // This is a no-op as Nitro modules handle events differently } override fun removeListeners(count: Double) { // Event listeners are handled by the event emitter // This is a no-op as Nitro modules handle events differently } private fun buildScanPayload(result: ScanResult): Map { val record = result.scanRecord val manufacturerData = extractManufacturerData(record) val serviceUUIDs = record?.serviceUuids?.map { it.uuid.toString() } val serviceData = extractServiceData(record) val txPower = record?.txPowerLevel?.takeIf { it != Int.MIN_VALUE } val advertisementData = mutableMapOf() record?.deviceName?.let { advertisementData["completeLocalName"] = it } txPower?.let { advertisementData["txPowerLevel"] = it } manufacturerData?.let { advertisementData["manufacturerData"] = it } serviceUUIDs?.let { advertisementData["serviceUUIDs"] = it } serviceData?.takeIf { it.isNotEmpty() }?.let { advertisementData["serviceData"] = it } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { advertisementData["isConnectable"] = result.isConnectable } advertisementData["rssi"] = result.rssi return mapOf( "id" to result.device.address, "name" to result.device.name, "localName" to record?.deviceName, "manufacturerData" to manufacturerData, "serviceUUIDs" to serviceUUIDs, "serviceData" to serviceData, "rssi" to result.rssi, "txPowerLevel" to txPower, "isConnectable" to if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) result.isConnectable else null, "advertisementData" to advertisementData ) } private fun extractManufacturerData(record: ScanRecord?): String? { val data = record?.manufacturerSpecificData ?: return null if (data.size() == 0) return null val bytes = data.valueAt(0) ?: return null return bytes.toHexString() } private fun extractServiceData(record: ScanRecord?): List>? { val map = record?.serviceData ?: return null val result = map.entries.mapNotNull { entry -> val bytes = entry.value ?: return@mapNotNull null mapOf( "uuid" to entry.key.uuid.toString(), "data" to bytes.toHexString() ) } return result.takeIf { it.isNotEmpty() } } private fun ByteArray.toHexString(): String { return joinToString("") { "%02x".format(it) } } // MARK: - Helper Methods private fun processAdvertisingData( dataMap: Map, dataBuilder: AdvertiseData.Builder ) { // Service UUIDs addServiceUUIDs(dataMap["incompleteServiceUUIDs16"] as? List, dataBuilder) addServiceUUIDs(dataMap["completeServiceUUIDs16"] as? List, dataBuilder) addServiceUUIDs(dataMap["incompleteServiceUUIDs32"] as? List, dataBuilder) addServiceUUIDs(dataMap["completeServiceUUIDs32"] as? List, dataBuilder) addServiceUUIDs(dataMap["incompleteServiceUUIDs128"] as? List, dataBuilder) addServiceUUIDs(dataMap["completeServiceUUIDs128"] as? List, dataBuilder) // Local Name if (dataMap.containsKey("shortenedLocalName") || dataMap.containsKey("completeLocalName")) { dataBuilder.setIncludeDeviceName(true) } // Tx Power Level if (dataMap.containsKey("txPowerLevel")) { dataBuilder.setIncludeTxPowerLevel(true) } // Service Solicitation addServiceUUIDs(dataMap["serviceSolicitationUUIDs16"] as? List, dataBuilder) addServiceUUIDs(dataMap["serviceSolicitationUUIDs128"] as? List, dataBuilder) addServiceUUIDs(dataMap["serviceSolicitationUUIDs32"] as? List, dataBuilder) // Service Data addServiceData(dataMap["serviceData16"] as? List>, dataBuilder) addServiceData(dataMap["serviceData32"] as? List>, dataBuilder) addServiceData(dataMap["serviceData128"] as? List>, dataBuilder) // Appearance if (dataMap.containsKey("appearance")) { val appearance = (dataMap["appearance"] as? Number)?.toInt() ?: 0 val appearanceData = byteArrayOf( (appearance and 0xFF).toByte(), ((appearance shr 8) and 0xFF).toByte() ) dataBuilder.addServiceData( ParcelUuid.fromString("00001800-0000-1000-8000-00805F9B34FB"), appearanceData ) } // Manufacturer Data val manufacturerData = dataMap["manufacturerData"] as? String if (manufacturerData != null) { val data = hexStringToByteArray(manufacturerData) if (data != null) { dataBuilder.addManufacturerData(0x0000, data) } } } private fun addServiceUUIDs(uuids: List?, dataBuilder: AdvertiseData.Builder) { if (uuids != null) { for (uuid in uuids) { dataBuilder.addServiceUuid(ParcelUuid.fromString(uuid)) } } } private fun addServiceData( serviceDataArray: List>?, dataBuilder: AdvertiseData.Builder ) { if (serviceDataArray != null) { for (serviceData in serviceDataArray) { val uuid = serviceData["uuid"] as? String val data = serviceData["data"] as? String if (uuid != null && data != null) { val dataBytes = hexStringToByteArray(data) if (dataBytes != null) { dataBuilder.addServiceData(ParcelUuid.fromString(uuid), dataBytes) } } } } } private fun hexStringToByteArray(hexString: String?): ByteArray? { if (hexString == null) return null val cleanHex = hexString.replace(" ", "") if (cleanHex.length % 2 != 0) return null val bytes = ByteArray(cleanHex.length / 2) for (i in bytes.indices) { val index = i * 2 bytes[i] = cleanHex.substring(index, index + 2).toInt(16).toByte() } return bytes } private fun setServicesFromOptions(serviceUUIDs: List) { ensureBluetoothManager() gattServerReady = false val manager = bluetoothManager ?: return gattServer = manager.openGattServer(null, object : BluetoothGattServerCallback() {}) gattServer?.clearServices() for (uuid in serviceUUIDs) { val service = BluetoothGattService( UUID.fromString(uuid), BluetoothGattService.SERVICE_TYPE_PRIMARY ) gattServer?.addService(service) } gattServerReady = true } } private class NitroEventEmitter(private val tag: String) { fun emit(eventName: String, payload: Map) { val context = NitroModules.applicationContext if (context == null) { Log.w(tag, "Unable to emit $eventName: React context unavailable") return } val writable = Arguments.createMap() payload.forEach { (key, value) -> writeValue(writable, key, value) } context .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) .emit(eventName, writable) } private fun writeValue(map: WritableMap, key: String, value: Any?) { when (value) { null -> map.putNull(key) is String -> map.putString(key, value) is Boolean -> map.putBoolean(key, value) is Int -> map.putInt(key, value) is Double -> map.putDouble(key, value) is Float -> map.putDouble(key, value.toDouble()) is Long -> map.putDouble(key, value.toDouble()) is Map<*, *> -> map.putMap(key, convertMap(value)) is List<*> -> map.putArray(key, convertArray(value)) else -> map.putString(key, value.toString()) } } private fun convertMap(map: Map<*, *>): WritableMap { val writable = Arguments.createMap() map.forEach { (key, value) -> if (key is String) { writeValue(writable, key, value) } } return writable } private fun convertArray(list: List<*>): WritableArray { val writable = Arguments.createArray() list.forEach { value -> when (value) { null -> writable.pushNull() is String -> writable.pushString(value) is Boolean -> writable.pushBoolean(value) is Int -> writable.pushInt(value) is Double -> writable.pushDouble(value) is Float -> writable.pushDouble(value.toDouble()) is Long -> writable.pushDouble(value.toDouble()) is Map<*, *> -> writable.pushMap(convertMap(value)) is List<*> -> writable.pushArray(convertArray(value)) else -> writable.pushString(value.toString()) } } return writable } }