Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import android.nfc.NdefRecord
import android.nfc.NfcAdapter
import android.nfc.NfcManager
import android.nfc.Tag
import android.nfc.tech.IsoDep
import android.nfc.tech.Ndef
import android.os.Bundle
import androidx.activity.compose.LocalActivity
Expand All @@ -29,6 +30,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

/**
* Android implementation of [NfcReadManager].
Expand All @@ -47,6 +49,7 @@ internal actual class NfcReadManager actual constructor(private val config: NfcC
private var scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
private var isScanning by mutableStateOf(false)
@Volatile private var timeoutJob: Job? = null
private var discoveredTag: Tag? = null

/**
* A [StateFlow] that emits the current [NfcReadResult] during the scanning process.
Expand Down Expand Up @@ -151,6 +154,7 @@ internal actual class NfcReadManager actual constructor(private val config: NfcC
timeoutJob = null
activity?.let { nfcAdapter?.disableReaderMode(it) }
isScanning = false
discoveredTag = null
}

/**
Expand All @@ -162,6 +166,7 @@ internal actual class NfcReadManager actual constructor(private val config: NfcC
@OptIn(ExperimentalStdlibApi::class)
override fun onTagDiscovered(tag: Tag?) {
timeoutJob?.cancel()
discoveredTag = tag

if (tag == null) {
scope.launch {
Expand Down Expand Up @@ -207,4 +212,23 @@ internal actual class NfcReadManager actual constructor(private val config: NfcC
_tagData.value = result
}
}
}

/**
* Sends an APDU (Application Protocol Data Unit) command to an ISO 7816-4 compatible tag.
*
* @param command The APDU command bytes.
* @return The response bytes from the tag.
* @throws Exception if the tag does not support ISO 7816 or the command fails.
*/
actual suspend fun sendApdu(command: ByteArray): ByteArray = withContext(Dispatchers.IO) {
val tag = discoveredTag ?: throw Exception("No tag discovered")
val isoDep = IsoDep.get(tag) ?: throw Exception("Tag does not support ISO 7816-4 (IsoDep)")

isoDep.use {
if (!it.isConnected) {
it.connect()
}
it.transceive(command)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,14 @@ internal expect class NfcReadManager(config: NfcConfig) {

/** Stops the NFC scanning process. */
fun stopScanning()

/**
* Sends an APDU (Application Protocol Data Unit) command to an ISO 7816-4 compatible tag.
* This is a low-level operation typically used with smart cards.
*
* @param command The APDU command bytes.
* @return The response bytes from the tag.
* @throws Exception if the tag does not support ISO 7816 or the command fails.
*/
suspend fun sendApdu(command: ByteArray): ByteArray
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,13 @@ interface NfcReadManagerState {
* Stops the NFC scanning process.
*/
fun stopScanning()

/**
* Sends an APDU (Application Protocol Data Unit) command to an ISO 7816-4 compatible tag.
*
* @param command The APDU command bytes.
* @return The response bytes from the tag.
* @throws Exception if the tag does not support ISO 7816 or the command fails.
*/
suspend fun sendApdu(command: ByteArray): ByteArray
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,12 @@ internal class NfcReadManagerStateImpl(config: NfcConfig) : NfcReadManagerState
override fun stopScanning() {
nfcReadManager.stopScanning()
}

/**
* Sends an APDU (Application Protocol Data Unit) command to an ISO 7816-4 compatible tag.
* Delegates to the underlying [NfcReadManager].
*/
override suspend fun sendApdu(command: ByteArray): ByteArray {
return nfcReadManager.sendApdu(command)
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@file:OptIn(ExperimentalForeignApi::class)
@file:OptIn(ExperimentalForeignApi::class, BetaInteropApi::class)

package com.devtamuno.kmp.nfcreader.contract

Expand All @@ -7,10 +7,17 @@ import com.devtamuno.kmp.nfcreader.data.NfcConfig
import com.devtamuno.kmp.nfcreader.data.NfcReadResult
import com.devtamuno.kmp.nfcreader.data.NfcTagData
import com.devtamuno.kmp.nfcreader.data.NfcTagType
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlinx.cinterop.BetaInteropApi
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.addressOf
import kotlinx.cinterop.usePinned
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.suspendCancellableCoroutine
import platform.CoreNFC.NFCISO7816APDU
import platform.CoreNFC.NFCNDEFMessage
import platform.CoreNFC.NFCNDEFPayload
import platform.CoreNFC.NFCNDEFStatusReadOnly
Expand All @@ -26,7 +33,9 @@ import platform.CoreNFC.NFCTagTypeISO15693
import platform.CoreNFC.NFCTagTypeISO7816Compatible
import platform.CoreNFC.NFCMiFareUnknown
import platform.CoreNFC.NFCTagTypeMiFare
import platform.Foundation.NSData
import platform.Foundation.NSError
import platform.Foundation.create
import platform.darwin.NSObject
import platform.darwin.dispatch_async
import platform.darwin.dispatch_get_main_queue
Expand All @@ -44,8 +53,6 @@ private const val NFC_SESSION_TIMEOUT_ERROR_CODE = 201L
internal actual class NfcReadManager actual constructor(private val config: NfcConfig) :
NSObject(), NFCTagReaderSessionDelegateProtocol {



private val _nfcResult = MutableStateFlow<NfcReadResult>(NfcReadResult.Initial)

/**
Expand All @@ -62,6 +69,8 @@ internal actual class NfcReadManager actual constructor(private val config: NfcC
*/
private var session: NFCTagReaderSession? = null

private var discoveredTag: NFCTagProtocol? = null

/** A [StateFlow] that emits the current [NfcReadResult]. */
actual val nfcResult: StateFlow<NfcReadResult>
get() = _nfcResult.asStateFlow()
Expand All @@ -81,6 +90,7 @@ internal actual class NfcReadManager actual constructor(private val config: NfcC
}

pendingResult = null
discoveredTag = null
updateState(NfcReadResult.Scanning)
session =
NFCTagReaderSession(
Expand All @@ -99,6 +109,7 @@ internal actual class NfcReadManager actual constructor(private val config: NfcC
actual fun stopScanning() {
session?.invalidateSession()
session = null
discoveredTag = null
}

override fun tagReaderSessionDidBecomeActive(session: NFCTagReaderSession) = Unit
Expand All @@ -109,6 +120,7 @@ internal actual class NfcReadManager actual constructor(private val config: NfcC
*/
override fun tagReaderSession(session: NFCTagReaderSession, didInvalidateWithError: NSError) {
this.session = null
this.discoveredTag = null

val result =
pendingResult
Expand All @@ -125,6 +137,7 @@ internal actual class NfcReadManager actual constructor(private val config: NfcC
/** Invoked when tags are detected. Only the first tag in the field is processed. */
override fun tagReaderSession(session: NFCTagReaderSession, didDetectTags: List<*>) {
val tag = didDetectTags.firstOrNull() as? NFCTagProtocol ?: return
this.discoveredTag = tag

// MIFARE Classic (NFCMiFareUnknown) uses Crypto1 which CoreNFC does not support.
// The UID and mifareFamily are set during ISO 14443-3A anticollision and are available
Expand Down Expand Up @@ -257,4 +270,40 @@ internal actual class NfcReadManager actual constructor(private val config: NfcC
private fun updateState(result: NfcReadResult) {
dispatch_async(dispatch_get_main_queue()) { _nfcResult.value = result }
}

/**
* Sends an APDU (Application Protocol Data Unit) command to an ISO 7816-4 compatible tag.
*
* @param command The APDU command bytes.
* @return The response bytes from the tag.
* @throws Exception if the tag does not support ISO 7816 or the command fails.
*/
actual suspend fun sendApdu(command: ByteArray): ByteArray = suspendCancellableCoroutine { continuation ->
val tag = discoveredTag?.asNFCISO7816Tag()
if (tag == null) {
continuation.resumeWithException(Exception("Tag does not support ISO 7816-4"))
return@suspendCancellableCoroutine
}

val apdu = command.toNSData()?.let { NFCISO7816APDU(data = it) }
if (apdu == null) {
continuation.resumeWithException(Exception("Invalid APDU command"))
return@suspendCancellableCoroutine
}

tag.sendCommandAPDU(apdu) { responseData, sw1, sw2, error ->
if (error != null) {
continuation.resumeWithException(Exception(error.localizedDescription))
} else {
val responseBytes = responseData?.toByteArray() ?: byteArrayOf()
continuation.resume(responseBytes + sw1.toByte() + sw2.toByte())
}
}
}

private fun ByteArray.toNSData(): NSData? = if (isEmpty()) null else {
this.usePinned { pinned ->
NSData.create(bytes = pinned.addressOf(0), length = size.toULong())
}
}
}