package com.rnthermalprinter.discovery import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothManager import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter import android.os.Build import com.facebook.react.bridge.* import com.facebook.react.modules.core.DeviceEventManagerModule import org.json.JSONArray import org.json.JSONObject /** * Bluetooth printer discovery (Classic & BLE) Based on react-native-bluetooth-escpos-printer logic */ class BluetoothDiscovery(private val reactContext: ReactApplicationContext) { companion object { const val EVENT_DEVICE_ALREADY_PAIRED = "EVENT_DEVICE_ALREADY_PAIRED" const val EVENT_DEVICE_FOUND = "EVENT_DEVICE_FOUND" const val EVENT_DEVICE_DISCOVER_DONE = "EVENT_DEVICE_DISCOVER_DONE" const val EVENT_BLUETOOTH_NOT_SUPPORT = "EVENT_BLUETOOTH_NOT_SUPPORT" } val bluetoothAdapter: BluetoothAdapter? by lazy { val manager = reactContext.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager manager?.adapter } private var pairedDevices = JSONArray() private var foundDevices = JSONArray() private var isScanning = false private var discoverReceiver: BroadcastReceiver? = null /** Scan devices - compatible with react-native-bluetooth-escpos-printer */ fun scanDevices(): WritableMap { val result = Arguments.createMap() val adapter = bluetoothAdapter if (adapter == null) { result.putBoolean("success", false) result.putString("error", "Bluetooth not supported") emitRNEvent(EVENT_BLUETOOTH_NOT_SUPPORT, null) return result } // Cancel any existing discovery cancelDiscovery() // Reset arrays pairedDevices = JSONArray() foundDevices = JSONArray() // Get paired devices try { val bondedDevices = try { adapter.bondedDevices } catch (e: SecurityException) { android.util.Log.e( "BluetoothDiscovery", "Cannot get bonded devices: Missing BLUETOOTH_CONNECT permission" ) emptySet() } for (device in bondedDevices) { try { val deviceName = try { device.name ?: "" } catch (e: SecurityException) { "" } val obj = JSONObject() obj.put("name", if (deviceName.isEmpty()) "Unknown Device" else deviceName) obj.put("address", device.address) // Detect device type val deviceType = when { device.type == BluetoothDevice.DEVICE_TYPE_LE -> "ble" device.type == BluetoothDevice.DEVICE_TYPE_CLASSIC -> "bt" device.type == BluetoothDevice.DEVICE_TYPE_DUAL -> "dual" else -> "unknown" } obj.put("deviceType", deviceType) obj.put("isSupported", true) // Android supports all paired devices pairedDevices.put(obj) } catch (e: Exception) { // Ignore } } // Emit paired devices event (compatible with old lib) val params = Arguments.createMap() params.putString("devices", pairedDevices.toString()) emitRNEvent(EVENT_DEVICE_ALREADY_PAIRED, params) // Register broadcast receiver for discovery registerDiscoveryReceiver() // Start discovery (will fail without proper permissions) val discoveryStarted = try { val started = adapter.startDiscovery() if (!started) { android.util.Log.e( "BluetoothDiscovery", "startDiscovery returned false - ensure location is enabled and permissions granted" ) } started } catch (e: SecurityException) { android.util.Log.e( "BluetoothDiscovery", "SecurityException: Missing BLUETOOTH_SCAN permission on Android 12+" ) false } catch (e: Exception) { android.util.Log.e( "BluetoothDiscovery", "Failed to start discovery: ${e.message}" ) false } if (!discoveryStarted) { result.putBoolean("success", false) result.putString("error", "Failed to start discovery") cancelDiscovery() unregisterDiscoveryReceiver() } else { isScanning = true result.putBoolean("success", true) android.util.Log.d("BluetoothDiscovery", "Discovery started successfully") } } catch (e: SecurityException) { result.putBoolean("success", false) result.putString("error", "Security exception: ${e.message}") } return result } /** Register broadcast receiver for device discovery */ private fun registerDiscoveryReceiver() { if (discoverReceiver != null) return discoverReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { val action = intent.action ?: return when (action) { BluetoothDevice.ACTION_FOUND -> { handleDeviceFound(intent) } BluetoothAdapter.ACTION_DISCOVERY_FINISHED -> { handleDiscoveryFinished() } } } } val filter = IntentFilter().apply { addAction(BluetoothDevice.ACTION_FOUND) addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED) } reactContext.registerReceiver(discoverReceiver, filter) } /** Unregister discovery receiver */ private fun unregisterDiscoveryReceiver() { discoverReceiver?.let { try { reactContext.unregisterReceiver(it) } catch (e: Exception) { // Ignore } discoverReceiver = null } } /** Handle device found during discovery */ private fun handleDeviceFound(intent: Intent) { android.util.Log.d("BluetoothDiscovery", "Device found event received") val device = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { intent.getParcelableExtra( BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java ) } else { @Suppress("DEPRECATION") intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) } ?: return // Skip if already bonded (handled in paired devices) if (device.bondState == BluetoothDevice.BOND_BONDED) { android.util.Log.d("BluetoothDiscovery", "Skipping bonded device: ${device.address}") return } try { val deviceName = try { device.name ?: "" } catch (e: SecurityException) { android.util.Log.e( "BluetoothDiscovery", "Cannot get device name: ${e.message}" ) "" } android.util.Log.d( "BluetoothDiscovery", "[Native:Android] BluetoothDiscovery.onReceive - PROCESS: Found device: $deviceName (${device.address})" ) val deviceFound = JSONObject() deviceFound.put("name", if (deviceName.isEmpty()) "Unknown Device" else deviceName) deviceFound.put("address", device.address) // Detect device type based on Bluetooth class val deviceType = when { device.type == BluetoothDevice.DEVICE_TYPE_LE -> "ble" device.type == BluetoothDevice.DEVICE_TYPE_CLASSIC -> "bt" device.type == BluetoothDevice.DEVICE_TYPE_DUAL -> "dual" else -> "unknown" } deviceFound.put("deviceType", deviceType) // Android supports most devices deviceFound.put("isSupported", true) // Check if not already in found list if (!isDeviceInArray(deviceFound, foundDevices)) { foundDevices.put(deviceFound) // Emit device found event (compatible with old lib) val params = Arguments.createMap() params.putString("device", deviceFound.toString()) emitRNEvent(EVENT_DEVICE_FOUND, params) android.util.Log.d( "BluetoothDiscovery", "Emitted EVENT_DEVICE_FOUND for ${device.address}" ) } } catch (e: Exception) { // Ignore individual device errors } } /** Handle discovery finished */ private fun handleDiscoveryFinished() { android.util.Log.d( "BluetoothDiscovery", "Discovery finished. Paired: ${pairedDevices.length()}, Found: ${foundDevices.length()}" ) try { val params = Arguments.createMap() params.putString("paired", pairedDevices.toString()) params.putString("found", foundDevices.toString()) // Emit old lib compatible event emitRNEvent(EVENT_DEVICE_DISCOVER_DONE, params) } catch (e: Exception) { // Ignore } finally { isScanning = false unregisterDiscoveryReceiver() } } /** Check if device already in array */ private fun isDeviceInArray(device: JSONObject, array: JSONArray): Boolean { val address = device.optString("address") for (i in 0 until array.length()) { val item = array.optJSONObject(i) if (item != null && item.optString("address") == address) { return true } } return false } /** Stop scan - public method for external use */ fun stopScan() { android.util.Log.d("BluetoothDiscovery", "Stopping scan") cancelDiscovery() unregisterDiscoveryReceiver() isScanning = false } /** Cancel discovery */ private fun cancelDiscovery() { try { bluetoothAdapter?.cancelDiscovery() } catch (e: SecurityException) { // Ignore } } /** Emit RN event (compatible with old lib) */ private fun emitRNEvent(event: String, params: WritableMap?) { reactContext .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) .emit(event, params) } }