Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ On Android, missing Bluetooth/location permission may report as `denied` before
- `addReaderDiscoveredListener()` — receive Readers during the scan window.
- `stopReaderScan()` — end the active scan early.

Continuous scans emit discovered Readers in real time and use `timeoutMs` (5 seconds by default) for each supported Reader model before rotating to the next. Their promise resolves when `stopReaderScan()` or `connectReader()` stops the scan. Call `stopReaderScan()` when leaving the owning screen.
Continuous scans emit discovered Readers in real time. Android uses one filtered, low-latency scan for all supported Reader models; iOS uses the vendor's model-specific scan API. Their promise resolves when `stopReaderScan()` or `connectReader()` stops the scan. Call `stopReaderScan()` when leaving the owning screen.

Starting a new `scanReaders()` while one is active supersedes the prior scan: the prior promise resolves with partial results collected so far, and discovery events for that scan stop once it ends.

Expand Down
4 changes: 2 additions & 2 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ plugins {
}

group = 'expo.modules.blenfcreader'
version = '0.2.3'
version = '0.2.4'

android {
namespace "expo.modules.blenfcreader"
defaultConfig {
minSdkVersion 23
versionCode 1
versionName "0.2.3"
versionName "0.2.4"
}
lintOptions {
abortOnError false
Expand Down
Binary file modified android/libs/acssmcio-0.6.2.aar
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
package expo.modules.blenfcreader

import android.Manifest
import android.bluetooth.BluetoothManager
import android.bluetooth.le.BluetoothLeScanner
import android.bluetooth.le.ScanCallback
import android.bluetooth.le.ScanFilter
import android.bluetooth.le.ScanResult
import android.bluetooth.le.ScanSettings
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import androidx.core.app.ActivityCompat
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import com.acs.smartcardio.BluetoothSmartCard
import android.os.ParcelUuid
import androidx.core.app.ActivityCompat
import com.acs.smartcardio.BluetoothTerminalManager
import expo.modules.interfaces.permissions.PermissionsStatus
import expo.modules.kotlin.Promise
Expand All @@ -21,6 +27,7 @@ import javax.smartcardio.Card
import javax.smartcardio.CardException
import javax.smartcardio.CardTerminal
import javax.smartcardio.CommandAPDU
import java.util.UUID

private const val READER_DISCOVERED_EVENT = "onReaderDiscovered"
private const val CARD_PRESENT_EVENT = "onCardPresent"
Expand All @@ -38,16 +45,24 @@ class ReactNativeBleNfcReaderModule : Module() {
private val scanHandler = Handler(Looper.getMainLooper())
private val readerLock = Any()
private val terminalIoLock = Any()
private val scanTerminalTypes = intArrayOf(
BluetoothTerminalManager.TERMINAL_TYPE_ACR3901U_S1,
BluetoothTerminalManager.TERMINAL_TYPE_ACR1255U_J1,
BluetoothTerminalManager.TERMINAL_TYPE_AMR220_C,
BluetoothTerminalManager.TERMINAL_TYPE_ACR1255U_J1_V2,
BluetoothTerminalManager.TERMINAL_TYPE_ACR1555U
private val scanTerminalTypes = linkedMapOf(
UUID.fromString("00003970-817C-48DF-8DB2-476A8134EDE0") to
BluetoothTerminalManager.TERMINAL_TYPE_ACR1555U,
UUID.fromString("0000FFF0-0000-1000-8000-00805F9B34FB") to
BluetoothTerminalManager.TERMINAL_TYPE_ACR1255U_J1,
UUID.fromString("3C4AFFF0-4783-3DE5-A983-D348718EF133") to
BluetoothTerminalManager.TERMINAL_TYPE_ACR1255U_J1_V2,
UUID.fromString("AE448001-6E87-DF33-ADB8-51DFD29BE725") to
BluetoothTerminalManager.TERMINAL_TYPE_ACR3901U_S1,
UUID.fromString("01A50001-BA65-489A-B49C-4FF986E262EF") to
BluetoothTerminalManager.TERMINAL_TYPE_AMR220_C
)
private val discoveredReaders = LinkedHashMap<String, Map<String, Any>>()
private val knownTerminals = LinkedHashMap<String, CardTerminal>()
private val knownTerminalManagers = LinkedHashMap<String, BluetoothTerminalManager>()
private var activeScanManager: BluetoothTerminalManager? = null
private var activeBleScanner: BluetoothLeScanner? = null
private var activeBleScanCallback: ScanCallback? = null
private var activeReaderId: String? = null
private var activeReaderTerminal: CardTerminal? = null
private var activeReaderManager: BluetoothTerminalManager? = null
Expand All @@ -59,9 +74,6 @@ class ReactNativeBleNfcReaderModule : Module() {
private var activeCardMonitorOptions: NormalizedCardMonitorOptions? = null
private var scanPromise: Promise? = null
private var scanStopRunnable: Runnable? = null
private var scanTypeRunnable: Runnable? = null
private var scanTypeIndex = 0
private var scanTypeDelayMs = 1000L
private var readerPermissionsRequested = false

override fun definition() = ModuleDefinition {
Expand Down Expand Up @@ -329,26 +341,69 @@ class ReactNativeBleNfcReaderModule : Module() {
val timeoutMs = normalizedTimeoutMs(options)
finishScan()

val manager = BluetoothSmartCard.getInstance(context()).manager ?: throw ReaderScanUnavailableException()
activeScanManager = manager
val context = context()
scanPromise = promise
scanTypeIndex = 0
if (options?.continuous == true) {
scanTypeDelayMs = timeoutMs
} else {
scanTypeDelayMs = maxOf(250L, timeoutMs / scanTerminalTypes.size)
}
discoveredReaders.clear()
synchronized(readerLock) {
val activeId = activeReaderId
val activeTerminal = activeReaderTerminal
val activeManager = activeReaderManager
knownTerminals.clear()
if (activeId != null && activeTerminal != null) {
knownTerminalManagers.clear()
if (activeId != null && activeTerminal != null && activeManager != null) {
knownTerminals[activeId] = activeTerminal
knownTerminalManagers[activeId] = activeManager
}
}

startCurrentScanType()
val bluetoothManager =
context.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager
?: throw ReaderScanUnavailableException()
val scanner = bluetoothManager.adapter?.bluetoothLeScanner
?: throw ReaderScanUnavailableException()
val manager = BluetoothTerminalManager(context)
val terminalCallback = BluetoothTerminalManager.TerminalScanCallback { terminal ->
scanHandler.post {
addDiscoveredReader(terminal, manager)
}
}
val callback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
val terminalType = terminalType(result) ?: return
val name = result.scanRecord?.deviceName ?: result.device.name ?: return

try {
manager.a(terminalCallback, terminalType, result.device, name)
} catch (_: Exception) {
}
}

override fun onScanFailed(errorCode: Int) {
scanHandler.post {
failScan()
}
}
}
val filters = scanTerminalTypes.keys.map { serviceUuid ->
ScanFilter.Builder()
.setServiceUuid(ParcelUuid(serviceUuid))
.build()
}
val settings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.build()

activeScanManager = manager
activeBleScanner = scanner
activeBleScanCallback = callback
try {
scanner.startScan(filters, settings, callback)
} catch (error: Exception) {
stopActiveScan()
scanPromise = null
discoveredReaders.clear()
throw error
}

if (options?.continuous == true) {
return
Expand Down Expand Up @@ -397,29 +452,11 @@ class ReactNativeBleNfcReaderModule : Module() {
)
}

private fun startCurrentScanType() {
if (scanPromise == null) {
return
}

val manager = activeScanManager ?: return

manager.stopScan()
manager.startScan(scanTerminalTypes[scanTypeIndex]) { terminal ->
scanHandler.post {
addDiscoveredReader(terminal)
}
}

scanTypeIndex = (scanTypeIndex + 1) % scanTerminalTypes.size
scanTypeRunnable = Runnable {
startCurrentScanType()
}
scanHandler.postDelayed(scanTypeRunnable!!, scanTypeDelayMs)
}

private fun addDiscoveredReader(terminal: CardTerminal) {
if (activeScanManager == null || scanPromise == null) {
private fun addDiscoveredReader(
terminal: CardTerminal,
manager: BluetoothTerminalManager
) {
if (scanPromise == null || manager !== activeScanManager) {
return
}

Expand All @@ -431,18 +468,34 @@ class ReactNativeBleNfcReaderModule : Module() {

synchronized(readerLock) {
knownTerminals[terminal.name] = terminal
knownTerminalManagers[terminal.name] = manager
}

sendEvent(READER_DISCOVERED_EVENT, Bundle().apply {
putBundle("reader", readerBundle(reader))
})
}

private fun terminalType(result: ScanResult): Int? {
val serviceUuids = result.scanRecord?.serviceUuids ?: return null

return scanTerminalTypes.entries.firstOrNull { (serviceUuid) ->
serviceUuids.any { it.uuid == serviceUuid }
}?.value
}

private fun failScan() {
val promise = scanPromise ?: return
scanPromise = null
stopActiveScan()
discoveredReaders.clear()
promise.reject(ReaderScanUnavailableException())
}

private fun connectReader(readerId: String): Map<String, Any> {
val manager = readerManager()
finishScan()

val terminal = synchronized(readerLock) {
val connection = synchronized(readerLock) {
val activeId = activeReaderId

if (activeId != null && activeId != readerId) {
Expand All @@ -451,17 +504,20 @@ class ReactNativeBleNfcReaderModule : Module() {

if (activeId == readerId) {
val activeTerminal = activeReaderTerminal ?: throw ReaderNotConnectedException(readerId)
return@synchronized activeTerminal
val activeManager = activeReaderManager ?: throw ReaderConnectionUnavailableException()
return@synchronized Pair(activeManager, activeTerminal)
}

val knownTerminal = knownTerminals[readerId] ?: throw ReaderNotFoundException(readerId)
val knownManager =
knownTerminalManagers[readerId] ?: throw ReaderConnectionUnavailableException()
activeReaderId = readerId
activeReaderTerminal = knownTerminal
activeReaderManager = manager
knownTerminal
activeReaderManager = knownManager
Pair(knownManager, knownTerminal)
}

return readerForTerminal(terminal, manager)
return readerForTerminal(connection.second, connection.first)
}

private fun disconnectReader(readerId: String) {
Expand Down Expand Up @@ -912,10 +968,6 @@ class ReactNativeBleNfcReaderModule : Module() {
}
}

private fun readerManager(): BluetoothTerminalManager {
return BluetoothSmartCard.getInstance(context()).manager ?: throw ReaderConnectionUnavailableException()
}

private fun readerForTerminal(
terminal: CardTerminal,
manager: BluetoothTerminalManager? = null
Expand Down Expand Up @@ -996,6 +1048,7 @@ class ReactNativeBleNfcReaderModule : Module() {
activeReaderTerminal = null
activeReaderManager = null
knownTerminals.clear()
knownTerminalManagers.clear()

if (manager == null || terminal == null) {
null
Expand All @@ -1016,12 +1069,9 @@ class ReactNativeBleNfcReaderModule : Module() {

private fun finishScan(): ArrayList<Map<String, Any>> {
scanStopRunnable?.let(scanHandler::removeCallbacks)
scanTypeRunnable?.let(scanHandler::removeCallbacks)
scanStopRunnable = null
scanTypeRunnable = null

activeScanManager?.stopScan()
activeScanManager = null
stopActiveScan()

val readers = ArrayList(discoveredReaders.values)
discoveredReaders.clear()
Expand All @@ -1032,6 +1082,21 @@ class ReactNativeBleNfcReaderModule : Module() {
return readers
}

private fun stopActiveScan() {
val callback = activeBleScanCallback

if (callback != null) {
try {
activeBleScanner?.stopScan(callback)
} catch (_: Exception) {
}
}

activeBleScanCallback = null
activeBleScanner = null
activeScanManager = null
}

private fun readerBundle(reader: Map<String, Any>): Bundle {
return Bundle().apply {
putString("id", reader["id"] as? String)
Expand Down
2 changes: 2 additions & 0 deletions docs/vendor/acs-evk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

This folder keeps the upstream ACS BLE EVK Android and iOS demo projects as reference material. The package runtime uses the vendored binaries under `android/libs` and `ios/Frameworks`; this folder is not part of the build.

`android/libs/acssmcio-0.6.2.aar` is patched to replace the vendor's hardcoded `SCAN_MODE_LOW_POWER` with `SCAN_MODE_LOW_LATENCY` and expose its terminal factory to the library's single filtered Android scan. Run `node internal/patches/patch-acs-android-scan-mode.mjs --check` to verify the binary patch.

Demo capabilities not currently exposed by the library:

- Reader admin settings: set/reset master key and get/set terminal timeouts.
Expand Down
Loading
Loading