diff --git a/app/desktop/src/jvmMain/kotlin/com/codebutler/farebot/desktop/DesktopCardScanner.kt b/app/desktop/src/jvmMain/kotlin/com/codebutler/farebot/desktop/DesktopCardScanner.kt index 6031eded3..86d63e6ac 100644 --- a/app/desktop/src/jvmMain/kotlin/com/codebutler/farebot/desktop/DesktopCardScanner.kt +++ b/app/desktop/src/jvmMain/kotlin/com/codebutler/farebot/desktop/DesktopCardScanner.kt @@ -26,6 +26,7 @@ import com.codebutler.farebot.card.RawCard import com.codebutler.farebot.card.nfc.pn533.PN533 import com.codebutler.farebot.card.nfc.pn533.PN533Device import com.codebutler.farebot.shared.nfc.CardScanner +import com.codebutler.farebot.shared.nfc.ReadingProgress import com.codebutler.farebot.shared.nfc.ScannedTag import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -62,6 +63,9 @@ class DesktopCardScanner : CardScanner { private val _isScanning = MutableStateFlow(false) override val isScanning: StateFlow = _isScanning.asStateFlow() + private val _readingProgress = MutableStateFlow(null) + override val readingProgress: StateFlow = _readingProgress.asStateFlow() + private var scanJob: Job? = null private val scope = CoroutineScope(Dispatchers.IO) @@ -83,11 +87,16 @@ class DesktopCardScanner : CardScanner { _scannedTags.tryEmit(tag) }, onCardRead = { rawCard -> + _readingProgress.value = null _scannedCards.tryEmit(rawCard) }, onError = { error -> + _readingProgress.value = null _scanErrors.tryEmit(error) }, + onProgress = { current, total -> + _readingProgress.value = ReadingProgress(current, total) + }, ) } catch (e: Exception) { if (isActive) { @@ -119,7 +128,13 @@ class DesktopCardScanner : CardScanner { private suspend fun discoverBackends(): List { val backends = mutableListOf(PcscReaderBackend()) - val transports = PN533Device.openAll() + val transports = + try { + PN533Device.openAll() + } catch (e: UnsatisfiedLinkError) { + println("[DesktopCardScanner] libusb not available: ${e.message}") + emptyList() + } if (transports.isEmpty()) { backends.add(PN533ReaderBackend()) } else { diff --git a/app/desktop/src/jvmMain/kotlin/com/codebutler/farebot/desktop/NfcReaderBackend.kt b/app/desktop/src/jvmMain/kotlin/com/codebutler/farebot/desktop/NfcReaderBackend.kt index 04605ce14..6f24123b8 100644 --- a/app/desktop/src/jvmMain/kotlin/com/codebutler/farebot/desktop/NfcReaderBackend.kt +++ b/app/desktop/src/jvmMain/kotlin/com/codebutler/farebot/desktop/NfcReaderBackend.kt @@ -40,5 +40,6 @@ interface NfcReaderBackend { onCardDetected: (ScannedTag) -> Unit, onCardRead: (RawCard<*>) -> Unit, onError: (Throwable) -> Unit, + onProgress: (suspend (current: Int, total: Int) -> Unit)? = null, ) } diff --git a/app/desktop/src/jvmMain/kotlin/com/codebutler/farebot/desktop/PN53xReaderBackend.kt b/app/desktop/src/jvmMain/kotlin/com/codebutler/farebot/desktop/PN53xReaderBackend.kt index 9b273215c..4a472440d 100644 --- a/app/desktop/src/jvmMain/kotlin/com/codebutler/farebot/desktop/PN53xReaderBackend.kt +++ b/app/desktop/src/jvmMain/kotlin/com/codebutler/farebot/desktop/PN53xReaderBackend.kt @@ -62,6 +62,7 @@ abstract class PN53xReaderBackend( onCardDetected: (ScannedTag) -> Unit, onCardRead: (RawCard<*>) -> Unit, onError: (Throwable) -> Unit, + onProgress: (suspend (current: Int, total: Int) -> Unit)?, ) { val transport = preOpenedTransport @@ -72,7 +73,7 @@ abstract class PN53xReaderBackend( val pn533 = PN533(transport) try { initDevice(pn533) - pollLoop(pn533, onCardDetected, onCardRead, onError) + pollLoop(pn533, onCardDetected, onCardRead, onError, onProgress) } finally { pn533.close() } @@ -83,6 +84,7 @@ abstract class PN53xReaderBackend( onCardDetected: (ScannedTag) -> Unit, onCardRead: (RawCard<*>) -> Unit, onError: (Throwable) -> Unit, + onProgress: (suspend (current: Int, total: Int) -> Unit)?, ) { while (true) { println("[$name] Polling for cards...") @@ -118,7 +120,7 @@ abstract class PN53xReaderBackend( onCardDetected(ScannedTag(id = tagId, techList = listOf(cardTypeName))) try { - val rawCard = readTarget(pn533, target) + val rawCard = readTarget(pn533, target, onProgress) onCardRead(rawCard) println("[$name] Card read successfully") } catch (e: Exception) { @@ -141,15 +143,17 @@ abstract class PN53xReaderBackend( private suspend fun readTarget( pn533: PN533, target: PN533.TargetInfo, + onProgress: (suspend (current: Int, total: Int) -> Unit)?, ): RawCard<*> = when (target) { - is PN533.TargetInfo.TypeA -> readTypeACard(pn533, target) - is PN533.TargetInfo.FeliCa -> readFeliCaCard(pn533, target) + is PN533.TargetInfo.TypeA -> readTypeACard(pn533, target, onProgress) + is PN533.TargetInfo.FeliCa -> readFeliCaCard(pn533, target, onProgress) } private suspend fun readTypeACard( pn533: PN533, target: PN533.TargetInfo.TypeA, + onProgress: (suspend (current: Int, total: Int) -> Unit)?, ): RawCard<*> { val info = PN533CardInfo.fromTypeA(target) val tagId = target.uid @@ -158,27 +162,27 @@ abstract class PN53xReaderBackend( return when (info.cardType) { CardType.MifareDesfire, CardType.ISO7816 -> { val transceiver = createTransceiver(pn533, target.tg) - ISO7816Dispatcher.readCard(tagId, transceiver) + ISO7816Dispatcher.readCard(tagId, transceiver, onProgress) } CardType.MifareClassic -> { val tech = PN533ClassicTechnology(pn533, target.tg, tagId, info) - ClassicCardReader.readCard(tagId, tech, null) + ClassicCardReader.readCard(tagId, tech, null, onProgress = onProgress) } CardType.MifareUltralight -> { val tech = PN533UltralightTechnology(pn533, target.tg, info) - UltralightCardReader.readCard(tagId, tech) + UltralightCardReader.readCard(tagId, tech, onProgress) } CardType.CEPAS -> { val transceiver = createTransceiver(pn533, target.tg) - CEPASCardReader.readCard(tagId, transceiver) + CEPASCardReader.readCard(tagId, transceiver, onProgress) } else -> { val transceiver = createTransceiver(pn533, target.tg) - ISO7816Dispatcher.readCard(tagId, transceiver) + ISO7816Dispatcher.readCard(tagId, transceiver, onProgress) } } } @@ -186,11 +190,12 @@ abstract class PN53xReaderBackend( private suspend fun readFeliCaCard( pn533: PN533, target: PN533.TargetInfo.FeliCa, + onProgress: (suspend (current: Int, total: Int) -> Unit)?, ): RawCard<*> { val tagId = target.idm println("[$name] FeliCa card: IDm=${tagId.hex()}") val adapter = PN533FeliCaTagAdapter(pn533, target.idm) - return FeliCaReader.readTag(tagId, adapter) + return FeliCaReader.readTag(tagId, adapter, onProgress = onProgress) } private suspend fun waitForRemoval(pn533: PN533) { diff --git a/app/desktop/src/jvmMain/kotlin/com/codebutler/farebot/desktop/PcscReaderBackend.kt b/app/desktop/src/jvmMain/kotlin/com/codebutler/farebot/desktop/PcscReaderBackend.kt index 07000aa47..37240c486 100644 --- a/app/desktop/src/jvmMain/kotlin/com/codebutler/farebot/desktop/PcscReaderBackend.kt +++ b/app/desktop/src/jvmMain/kotlin/com/codebutler/farebot/desktop/PcscReaderBackend.kt @@ -54,6 +54,7 @@ class PcscReaderBackend : NfcReaderBackend { onCardDetected: (ScannedTag) -> Unit, onCardRead: (RawCard<*>) -> Unit, onError: (Throwable) -> Unit, + onProgress: (suspend (current: Int, total: Int) -> Unit)?, ) { val factory = TerminalFactory.getDefault() val terminals = @@ -95,7 +96,7 @@ class PcscReaderBackend : NfcReaderBackend { println("[PC/SC] Tag ID: ${tagId.hex()}") onCardDetected(ScannedTag(id = tagId, techList = listOf(info.cardType.name))) - val rawCard = readCard(info, channel, tagId) + val rawCard = readCard(info, channel, tagId, onProgress) onCardRead(rawCard) println("[PC/SC] Card read successfully") } finally { @@ -118,31 +119,32 @@ class PcscReaderBackend : NfcReaderBackend { info: PCSCCardInfo, channel: javax.smartcardio.CardChannel, tagId: ByteArray, + onProgress: (suspend (current: Int, total: Int) -> Unit)?, ): RawCard<*> = when (info.cardType) { CardType.MifareDesfire, CardType.ISO7816 -> { val transceiver = PCSCCardTransceiver(channel) - ISO7816Dispatcher.readCard(tagId, transceiver) + ISO7816Dispatcher.readCard(tagId, transceiver, onProgress) } CardType.MifareClassic -> { val tech = PCSCClassicTechnology(channel, info) - ClassicCardReader.readCard(tagId, tech, null) + ClassicCardReader.readCard(tagId, tech, null, onProgress = onProgress) } CardType.MifareUltralight -> { val tech = PCSCUltralightTechnology(channel, info) - UltralightCardReader.readCard(tagId, tech) + UltralightCardReader.readCard(tagId, tech, onProgress) } CardType.FeliCa -> { val adapter = PCSCFeliCaTagAdapter(channel) - FeliCaReader.readTag(tagId, adapter) + FeliCaReader.readTag(tagId, adapter, onProgress = onProgress) } CardType.CEPAS -> { val transceiver = PCSCCardTransceiver(channel) - CEPASCardReader.readCard(tagId, transceiver) + CEPASCardReader.readCard(tagId, transceiver, onProgress) } CardType.Vicinity -> { @@ -152,7 +154,7 @@ class PcscReaderBackend : NfcReaderBackend { else -> { val transceiver = PCSCCardTransceiver(channel) - ISO7816Dispatcher.readCard(tagId, transceiver) + ISO7816Dispatcher.readCard(tagId, transceiver, onProgress) } } diff --git a/app/src/androidMain/kotlin/com/codebutler/farebot/app/core/nfc/ISO7816TagReader.kt b/app/src/androidMain/kotlin/com/codebutler/farebot/app/core/nfc/ISO7816TagReader.kt index 0de5437ba..57b36bf19 100644 --- a/app/src/androidMain/kotlin/com/codebutler/farebot/app/core/nfc/ISO7816TagReader.kt +++ b/app/src/androidMain/kotlin/com/codebutler/farebot/app/core/nfc/ISO7816TagReader.kt @@ -48,5 +48,6 @@ class ISO7816TagReader( tag: Tag, tech: CardTransceiver, cardKeys: CardKeys?, - ): RawCard<*> = ISO7816Dispatcher.readCard(tagId, tech) + onProgress: (suspend (current: Int, total: Int) -> Unit)?, + ): RawCard<*> = ISO7816Dispatcher.readCard(tagId, tech, onProgress) } diff --git a/app/src/androidMain/kotlin/com/codebutler/farebot/app/feature/home/AndroidCardScanner.kt b/app/src/androidMain/kotlin/com/codebutler/farebot/app/feature/home/AndroidCardScanner.kt index 4f8f816cb..d91cddc6d 100644 --- a/app/src/androidMain/kotlin/com/codebutler/farebot/app/feature/home/AndroidCardScanner.kt +++ b/app/src/androidMain/kotlin/com/codebutler/farebot/app/feature/home/AndroidCardScanner.kt @@ -10,6 +10,7 @@ import com.codebutler.farebot.key.CardKeys import com.codebutler.farebot.persist.CardKeysPersister import com.codebutler.farebot.shared.nfc.CardScanner import com.codebutler.farebot.shared.nfc.CardUnauthorizedException +import com.codebutler.farebot.shared.nfc.ReadingProgress import com.codebutler.farebot.shared.nfc.ScannedTag import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -51,6 +52,9 @@ class AndroidCardScanner( private val _isScanning = MutableStateFlow(false) override val isScanning: StateFlow = _isScanning.asStateFlow() + private val _readingProgress = MutableStateFlow(null) + override val readingProgress: StateFlow = _readingProgress.asStateFlow() + private var isObserving = false fun startObservingTags() { @@ -65,12 +69,17 @@ class AndroidCardScanner( _isScanning.value = true try { val cardKeys = getCardKeys(ByteUtils.getHexString(tag.id)) - val rawCard = tagReaderFactory.getTagReader(tag.id, tag, cardKeys).readTag() + val rawCard = + tagReaderFactory.getTagReader(tag.id, tag, cardKeys).readTag { current, total -> + _readingProgress.value = ReadingProgress(current, total) + } + _readingProgress.value = null if (rawCard.isUnauthorized()) { throw CardUnauthorizedException(rawCard.tagId(), rawCard.cardType()) } _scannedCards.emit(rawCard) } catch (error: Throwable) { + _readingProgress.value = null _scanErrors.emit(error) } finally { _isScanning.value = false diff --git a/app/src/commonMain/composeResources/values/strings.xml b/app/src/commonMain/composeResources/values/strings.xml index b53891c4e..0d1b71628 100644 --- a/app/src/commonMain/composeResources/values/strings.xml +++ b/app/src/commonMain/composeResources/values/strings.xml @@ -80,6 +80,7 @@ Explore Scan Reading\u2026 + Hold your card near the reader Search Enable NFC to scan cards diff --git a/app/src/commonMain/kotlin/com/codebutler/farebot/shared/App.kt b/app/src/commonMain/kotlin/com/codebutler/farebot/shared/App.kt index be0b18ff3..758c0c319 100644 --- a/app/src/commonMain/kotlin/com/codebutler/farebot/shared/App.kt +++ b/app/src/commonMain/kotlin/com/codebutler/farebot/shared/App.kt @@ -152,6 +152,7 @@ fun FareBotApp( navController.navigate(Screen.AddKey.createRoute(tagId, cardType)) }, onScanCard = { homeViewModel.startActiveScan() }, + onCancelScan = { homeViewModel.stopActiveScan() }, historyUiState = historyUiState, onNavigateToCard = { itemId -> val cardKey = historyViewModel.getCardNavKey(itemId) diff --git a/app/src/commonMain/kotlin/com/codebutler/farebot/shared/nfc/CardScanner.kt b/app/src/commonMain/kotlin/com/codebutler/farebot/shared/nfc/CardScanner.kt index 80d53c45d..ad141f1b7 100644 --- a/app/src/commonMain/kotlin/com/codebutler/farebot/shared/nfc/CardScanner.kt +++ b/app/src/commonMain/kotlin/com/codebutler/farebot/shared/nfc/CardScanner.kt @@ -24,9 +24,15 @@ package com.codebutler.farebot.shared.nfc import com.codebutler.farebot.card.RawCard import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow +data class ReadingProgress( + val current: Int, + val total: Int, +) + data class ScannedTag( val id: ByteArray, val techList: List, @@ -66,6 +72,10 @@ interface CardScanner { /** Whether scanning is currently in progress. */ val isScanning: StateFlow + /** Reading progress — non-null when a card read is in progress with known total. */ + val readingProgress: StateFlow + get() = MutableStateFlow(null) + /** * Start an active scan session (e.g., iOS NFC dialog). * Results are emitted to [scannedCards]. diff --git a/app/src/commonMain/kotlin/com/codebutler/farebot/shared/nfc/ISO7816Dispatcher.kt b/app/src/commonMain/kotlin/com/codebutler/farebot/shared/nfc/ISO7816Dispatcher.kt index 08965a503..79f5aaee9 100644 --- a/app/src/commonMain/kotlin/com/codebutler/farebot/shared/nfc/ISO7816Dispatcher.kt +++ b/app/src/commonMain/kotlin/com/codebutler/farebot/shared/nfc/ISO7816Dispatcher.kt @@ -39,23 +39,25 @@ object ISO7816Dispatcher { suspend fun readCard( tagId: ByteArray, transceiver: CardTransceiver, + onProgress: (suspend (current: Int, total: Int) -> Unit)? = null, ): RawCard<*> { - val iso7816Card = tryISO7816(tagId, transceiver) + val iso7816Card = tryISO7816(tagId, transceiver, onProgress) if (iso7816Card != null) { return iso7816Card } - return DesfireCardReader.readCard(tagId, transceiver) + return DesfireCardReader.readCard(tagId, transceiver, onProgress) } private suspend fun tryISO7816( tagId: ByteArray, transceiver: CardTransceiver, + onProgress: (suspend (current: Int, total: Int) -> Unit)? = null, ): RawCard<*>? { val appConfigs = buildAppConfigs() if (appConfigs.isEmpty()) return null return try { - ISO7816CardReader.readCard(tagId, transceiver, appConfigs) + ISO7816CardReader.readCard(tagId, transceiver, appConfigs, onProgress) } catch (e: Exception) { println("[ISO7816Dispatcher] ISO7816 read attempt failed: $e") null diff --git a/app/src/commonMain/kotlin/com/codebutler/farebot/shared/ui/screen/HomeScreen.kt b/app/src/commonMain/kotlin/com/codebutler/farebot/shared/ui/screen/HomeScreen.kt index e9a1b4d69..1f1eee84c 100644 --- a/app/src/commonMain/kotlin/com/codebutler/farebot/shared/ui/screen/HomeScreen.kt +++ b/app/src/commonMain/kotlin/com/codebutler/farebot/shared/ui/screen/HomeScreen.kt @@ -17,6 +17,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width @@ -42,7 +43,6 @@ import androidx.compose.material.icons.filled.VpnKey import androidx.compose.material3.AlertDialog import androidx.compose.material3.Badge import androidx.compose.material3.Button -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api @@ -50,6 +50,7 @@ import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.ListItem import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.MaterialTheme @@ -57,6 +58,7 @@ import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.NavigationDrawerItem +import androidx.compose.material3.OutlinedButton import androidx.compose.material3.PermanentDrawerSheet import androidx.compose.material3.PermanentNavigationDrawer import androidx.compose.material3.Scaffold @@ -107,6 +109,7 @@ import farebot.app.generated.resources.app_name import farebot.app.generated.resources.cancel import farebot.app.generated.resources.delete import farebot.app.generated.resources.delete_selected_cards +import farebot.app.generated.resources.hold_card_near_reader import farebot.app.generated.resources.ic_cards_stack import farebot.app.generated.resources.ic_launcher import farebot.app.generated.resources.import_source @@ -148,6 +151,7 @@ fun HomeScreen( onDismissError: () -> Unit, onNavigateToAddKeyForCard: (tagId: String, cardType: CardType) -> Unit, onScanCard: () -> Unit, + onCancelScan: () -> Unit, historyUiState: HistoryUiState, onNavigateToCard: (String) -> Unit, onImportFile: () -> Unit, @@ -658,22 +662,10 @@ fun HomeScreen( } }, icon = { - if (homeUiState.isLoading) { - CircularProgressIndicator( - modifier = Modifier.padding(4.dp), - color = MaterialTheme.colorScheme.onPrimaryContainer, - strokeWidth = 2.dp, - ) - } else { - Icon(Icons.Default.Nfc, contentDescription = null) - } + Icon(Icons.Default.Nfc, contentDescription = null) }, text = { - Text( - stringResource( - if (homeUiState.isReadingCard) Res.string.reading_card else Res.string.scan, - ), - ) + Text(stringResource(Res.string.scan)) }, ) } @@ -867,6 +859,64 @@ fun HomeScreen( } } + // Reading progress bottom sheet — shown while scanning/reading a card + if (homeUiState.isLoading || homeUiState.isReadingCard) { + ModalBottomSheet( + onDismissRequest = onCancelScan, + sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true), + ) { + Column( + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp) + .padding(bottom = 32.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Icon( + Icons.Default.Nfc, + contentDescription = null, + modifier = Modifier.size(48.dp), + tint = MaterialTheme.colorScheme.primary, + ) + + Text( + text = + if (homeUiState.isReadingCard) { + stringResource(Res.string.reading_card) + } else { + stringResource(Res.string.hold_card_near_reader) + }, + style = MaterialTheme.typography.titleMedium, + ) + + val progress = homeUiState.readingProgress + if (progress != null) { + LinearProgressIndicator( + progress = { progress.current.toFloat() / progress.total.toFloat() }, + modifier = Modifier.fillMaxWidth().height(4.dp), + ) + Text( + text = "${progress.current} / ${progress.total}", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } else { + LinearProgressIndicator( + modifier = Modifier.fillMaxWidth().height(4.dp), + ) + } + + if (homeUiState.requiresActiveScan) { + OutlinedButton(onClick = onCancelScan) { + Text(stringResource(Res.string.cancel)) + } + } + } + } + } + if (showImportSheet) { ModalBottomSheet( onDismissRequest = { showImportSheet = false }, diff --git a/app/src/commonMain/kotlin/com/codebutler/farebot/shared/ui/screen/HomeUiState.kt b/app/src/commonMain/kotlin/com/codebutler/farebot/shared/ui/screen/HomeUiState.kt index 4fbaf35ce..fc07f3a02 100644 --- a/app/src/commonMain/kotlin/com/codebutler/farebot/shared/ui/screen/HomeUiState.kt +++ b/app/src/commonMain/kotlin/com/codebutler/farebot/shared/ui/screen/HomeUiState.kt @@ -1,5 +1,6 @@ package com.codebutler.farebot.shared.ui.screen +import com.codebutler.farebot.shared.nfc.ReadingProgress import com.codebutler.farebot.shared.platform.NfcStatus data class HomeUiState( @@ -7,4 +8,5 @@ data class HomeUiState( val isLoading: Boolean = false, val isReadingCard: Boolean = false, val requiresActiveScan: Boolean = true, + val readingProgress: ReadingProgress? = null, ) diff --git a/app/src/commonMain/kotlin/com/codebutler/farebot/shared/viewmodel/CardViewModel.kt b/app/src/commonMain/kotlin/com/codebutler/farebot/shared/viewmodel/CardViewModel.kt index 0247f0202..197c4bde4 100644 --- a/app/src/commonMain/kotlin/com/codebutler/farebot/shared/viewmodel/CardViewModel.kt +++ b/app/src/commonMain/kotlin/com/codebutler/farebot/shared/viewmodel/CardViewModel.kt @@ -154,6 +154,8 @@ class CardViewModel( ) } } catch (ex: Exception) { + println("[FareBot] Card load error: ${ex::class.simpleName}: ${ex.message}") + ex.printStackTrace() _uiState.value = CardUiState( isLoading = false, diff --git a/app/src/commonMain/kotlin/com/codebutler/farebot/shared/viewmodel/HomeViewModel.kt b/app/src/commonMain/kotlin/com/codebutler/farebot/shared/viewmodel/HomeViewModel.kt index c84e88fdd..6c4fb4313 100644 --- a/app/src/commonMain/kotlin/com/codebutler/farebot/shared/viewmodel/HomeViewModel.kt +++ b/app/src/commonMain/kotlin/com/codebutler/farebot/shared/viewmodel/HomeViewModel.kt @@ -82,16 +82,24 @@ class HomeViewModel( } } + viewModelScope.launch { + cardScanner.readingProgress.collect { progress -> + _uiState.value = _uiState.value.copy(readingProgress = progress) + } + } + viewModelScope.launch { cardScanner.scannedCards.collect { rawCard -> - _uiState.value = _uiState.value.copy(isReadingCard = false) + _uiState.value = _uiState.value.copy(isReadingCard = false, readingProgress = null) processScannedCard(rawCard) } } viewModelScope.launch { cardScanner.scanErrors.collect { error -> - _uiState.value = _uiState.value.copy(isReadingCard = false) + _uiState.value = _uiState.value.copy(isReadingCard = false, readingProgress = null) + println("[FareBot] Scan error: ${error::class.simpleName}: ${error.message}") + error.printStackTrace() val scanError = categorizeError(error) analytics.logEvent( "scan_card_error", @@ -109,6 +117,10 @@ class HomeViewModel( cardScanner?.startActiveScan() } + fun stopActiveScan() { + cardScanner?.stopActiveScan() + } + fun dismissError() { _errorMessage.value = null } @@ -155,6 +167,8 @@ class HomeViewModel( val key = navDataHolder.put(rawCard) _navigateToCard.emit(key) } catch (e: Exception) { + println("[FareBot] Process card error: ${e::class.simpleName}: ${e.message}") + e.printStackTrace() _errorMessage.value = ScanError( title = getString(Res.string.error), diff --git a/app/src/iosMain/kotlin/com/codebutler/farebot/shared/nfc/IosNfcScanner.kt b/app/src/iosMain/kotlin/com/codebutler/farebot/shared/nfc/IosNfcScanner.kt index e11c808ab..2fe6a7aa0 100644 --- a/app/src/iosMain/kotlin/com/codebutler/farebot/shared/nfc/IosNfcScanner.kt +++ b/app/src/iosMain/kotlin/com/codebutler/farebot/shared/nfc/IosNfcScanner.kt @@ -185,7 +185,10 @@ class IosNfcScanner : CardScanner { CoroutineScope(Dispatchers.IO).launch { try { - rawCard = readTag(tag) + rawCard = + readTag(tag) { current, total -> + session.alertMessage = "Reading… $current / $total" + } } catch (e: Exception) { readException = e } finally { @@ -228,15 +231,21 @@ class IosNfcScanner : CardScanner { override fun tagReaderSessionDidBecomeActive(session: NFCTagReaderSession) { } - private suspend fun readTag(tag: Any): RawCard<*> = + private suspend fun readTag( + tag: Any, + onProgress: (suspend (current: Int, total: Int) -> Unit)? = null, + ): RawCard<*> = when (tag) { - is NFCFeliCaTagProtocol -> readFelicaTag(tag) - is NFCMiFareTagProtocol -> readMiFareTag(tag) + is NFCFeliCaTagProtocol -> readFelicaTag(tag, onProgress) + is NFCMiFareTagProtocol -> readMiFareTag(tag, onProgress) is NFCISO15693TagProtocol -> readVicinityTag(tag) else -> throw Exception("Unsupported NFC tag type") } - private suspend fun readFelicaTag(tag: NFCFeliCaTagProtocol): RawCard<*> { + private suspend fun readFelicaTag( + tag: NFCFeliCaTagProtocol, + onProgress: (suspend (current: Int, total: Int) -> Unit)?, + ): RawCard<*> { val tagId = tag.currentIDm.toByteArray() /* * onlyFirst = true is an iOS-specific hack to work around @@ -250,10 +259,13 @@ class IosNfcScanner : CardScanner { * * Once iOS fixes this, do an iOS version check instead. */ - return FeliCaReader.readTag(tagId, IosFeliCaTagAdapter(tag), onlyFirst = true) + return FeliCaReader.readTag(tagId, IosFeliCaTagAdapter(tag), onlyFirst = true, onProgress = onProgress) } - private suspend fun readMiFareTag(tag: NFCMiFareTagProtocol): RawCard<*> { + private suspend fun readMiFareTag( + tag: NFCMiFareTagProtocol, + onProgress: (suspend (current: Int, total: Int) -> Unit)?, + ): RawCard<*> { val tagId = tag.identifier.toByteArray() return when (tag.mifareFamily) { NFCMiFareDESFire -> { @@ -264,7 +276,7 @@ class IosNfcScanner : CardScanner { val transceiver = IosCardTransceiver(tag) transceiver.connect() try { - DesfireCardReader.readCard(tagId, transceiver) + DesfireCardReader.readCard(tagId, transceiver, onProgress) } finally { if (transceiver.isConnected) { try { @@ -279,7 +291,7 @@ class IosNfcScanner : CardScanner { val tech = IosUltralightTechnology(tag) tech.connect() try { - UltralightCardReader.readCard(tagId, tech) + UltralightCardReader.readCard(tagId, tech, onProgress) } finally { if (tech.isConnected) { try { @@ -295,7 +307,7 @@ class IosNfcScanner : CardScanner { val transceiver = IosCardTransceiver(tag) transceiver.connect() try { - CEPASCardReader.readCard(tagId, transceiver) + CEPASCardReader.readCard(tagId, transceiver, onProgress) } finally { if (transceiver.isConnected) { try { diff --git a/app/web/src/wasmJsMain/kotlin/com/codebutler/farebot/web/WebCardScanner.kt b/app/web/src/wasmJsMain/kotlin/com/codebutler/farebot/web/WebCardScanner.kt index 986de24d6..960cabd41 100644 --- a/app/web/src/wasmJsMain/kotlin/com/codebutler/farebot/web/WebCardScanner.kt +++ b/app/web/src/wasmJsMain/kotlin/com/codebutler/farebot/web/WebCardScanner.kt @@ -16,6 +16,7 @@ import com.codebutler.farebot.card.nfc.pn533.WebUsbPN533Transport import com.codebutler.farebot.card.ultralight.UltralightCardReader import com.codebutler.farebot.shared.nfc.CardScanner import com.codebutler.farebot.shared.nfc.ISO7816Dispatcher +import com.codebutler.farebot.shared.nfc.ReadingProgress import com.codebutler.farebot.shared.nfc.ScannedTag import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope @@ -60,6 +61,9 @@ class WebCardScanner : CardScanner { private val _isScanning = MutableStateFlow(false) override val isScanning: StateFlow = _isScanning.asStateFlow() + private val _readingProgress = MutableStateFlow(null) + override val readingProgress: StateFlow = _readingProgress.asStateFlow() + private var scanJob: Job? = null private var transport: WebUsbPN533Transport? = null private val scope = CoroutineScope(SupervisorJob()) @@ -108,6 +112,7 @@ class WebCardScanner : CardScanner { transport?.close() transport = null _isScanning.value = false + _readingProgress.value = null } private suspend fun pollLoop(transport: WebUsbPN533Transport) { @@ -157,9 +162,11 @@ class WebCardScanner : CardScanner { try { val rawCard = readTarget(pn533, target) + _readingProgress.value = null _scannedCards.tryEmit(rawCard) println("[WebUSB] Card read successfully") } catch (e: Exception) { + _readingProgress.value = null println("[WebUSB] Read error: ${e.message}") _scanErrors.tryEmit(e) } @@ -185,6 +192,10 @@ class WebCardScanner : CardScanner { is PN533.TargetInfo.FeliCa -> readFeliCaCard(pn533, target) } + private val onProgress: suspend (Int, Int) -> Unit = { current, total -> + _readingProgress.value = ReadingProgress(current, total) + } + private suspend fun readTypeACard( pn533: PN533, target: PN533.TargetInfo.TypeA, @@ -200,27 +211,27 @@ class WebCardScanner : CardScanner { return when (info.cardType) { CardType.MifareDesfire, CardType.ISO7816 -> { val transceiver = PN533CardTransceiver(pn533, target.tg) - ISO7816Dispatcher.readCard(tagId, transceiver) + ISO7816Dispatcher.readCard(tagId, transceiver, onProgress) } CardType.MifareClassic -> { val tech = PN533ClassicTechnology(pn533, target.tg, tagId, info) - ClassicCardReader.readCard(tagId, tech, null) + ClassicCardReader.readCard(tagId, tech, null, onProgress = onProgress) } CardType.MifareUltralight -> { val tech = PN533UltralightTechnology(pn533, target.tg, info) - UltralightCardReader.readCard(tagId, tech) + UltralightCardReader.readCard(tagId, tech, onProgress) } CardType.CEPAS -> { val transceiver = PN533CardTransceiver(pn533, target.tg) - CEPASCardReader.readCard(tagId, transceiver) + CEPASCardReader.readCard(tagId, transceiver, onProgress) } else -> { val transceiver = PN533CardTransceiver(pn533, target.tg) - ISO7816Dispatcher.readCard(tagId, transceiver) + ISO7816Dispatcher.readCard(tagId, transceiver, onProgress) } } } @@ -232,7 +243,7 @@ class WebCardScanner : CardScanner { val tagId = target.idm println("[WebUSB] FeliCa card: IDm=${tagId.hex()}") val adapter = PN533FeliCaTagAdapter(pn533, tagId) - return FeliCaReader.readTag(tagId, adapter) + return FeliCaReader.readTag(tagId, adapter, onProgress = onProgress) } private suspend fun waitForRemoval(pn533: PN533) { diff --git a/card/cepas/src/androidMain/kotlin/com/codebutler/farebot/card/cepas/CEPASTagReader.kt b/card/cepas/src/androidMain/kotlin/com/codebutler/farebot/card/cepas/CEPASTagReader.kt index 68316666d..b15a7455a 100644 --- a/card/cepas/src/androidMain/kotlin/com/codebutler/farebot/card/cepas/CEPASTagReader.kt +++ b/card/cepas/src/androidMain/kotlin/com/codebutler/farebot/card/cepas/CEPASTagReader.kt @@ -42,5 +42,6 @@ class CEPASTagReader( tag: Tag, tech: CardTransceiver, cardKeys: CardKeys?, - ): RawCEPASCard = CEPASCardReader.readCard(tagId, tech) + onProgress: (suspend (current: Int, total: Int) -> Unit)?, + ): RawCEPASCard = CEPASCardReader.readCard(tagId, tech, onProgress) } diff --git a/card/cepas/src/commonMain/kotlin/com/codebutler/farebot/card/cepas/CEPASCardReader.kt b/card/cepas/src/commonMain/kotlin/com/codebutler/farebot/card/cepas/CEPASCardReader.kt index bcf20c6b8..8f3dd840c 100644 --- a/card/cepas/src/commonMain/kotlin/com/codebutler/farebot/card/cepas/CEPASCardReader.kt +++ b/card/cepas/src/commonMain/kotlin/com/codebutler/farebot/card/cepas/CEPASCardReader.kt @@ -34,6 +34,7 @@ object CEPASCardReader { suspend fun readCard( tagId: ByteArray, tech: CardTransceiver, + onProgress: (suspend (current: Int, total: Int) -> Unit)? = null, ): RawCEPASCard { val purses = arrayOfNulls(16) val histories = arrayOfNulls(16) @@ -41,6 +42,7 @@ object CEPASCardReader { val protocol = CEPASProtocol(ISO7816Protocol(tech)) for (purseId in purses.indices) { + onProgress?.invoke(purseId, 32) val purseData = protocol.getPurse(purseId) purses[purseId] = if (purseData != null) { @@ -51,6 +53,7 @@ object CEPASCardReader { } for (historyId in histories.indices) { + onProgress?.invoke(16 + historyId, 32) val rawCEPASPurse = purses[historyId]!! if (rawCEPASPurse.isValid) { val historyData = protocol.getHistory(historyId) diff --git a/card/classic/src/androidMain/kotlin/com/codebutler/farebot/card/classic/ClassicTagReader.kt b/card/classic/src/androidMain/kotlin/com/codebutler/farebot/card/classic/ClassicTagReader.kt index 03ebbe197..65eadc90f 100644 --- a/card/classic/src/androidMain/kotlin/com/codebutler/farebot/card/classic/ClassicTagReader.kt +++ b/card/classic/src/androidMain/kotlin/com/codebutler/farebot/card/classic/ClassicTagReader.kt @@ -44,5 +44,6 @@ class ClassicTagReader( tag: Tag, tech: ClassicTechnology, cardKeys: ClassicCardKeys?, - ): RawClassicCard = ClassicCardReader.readCard(tagId, tech, cardKeys) + onProgress: (suspend (current: Int, total: Int) -> Unit)?, + ): RawClassicCard = ClassicCardReader.readCard(tagId, tech, cardKeys, onProgress = onProgress) } diff --git a/card/classic/src/commonMain/kotlin/com/codebutler/farebot/card/classic/ClassicCardReader.kt b/card/classic/src/commonMain/kotlin/com/codebutler/farebot/card/classic/ClassicCardReader.kt index 9c88513b4..831579fa1 100644 --- a/card/classic/src/commonMain/kotlin/com/codebutler/farebot/card/classic/ClassicCardReader.kt +++ b/card/classic/src/commonMain/kotlin/com/codebutler/farebot/card/classic/ClassicCardReader.kt @@ -49,10 +49,13 @@ object ClassicCardReader { tech: ClassicTechnology, cardKeys: ClassicCardKeys?, globalKeys: List? = null, + onProgress: (suspend (current: Int, total: Int) -> Unit)? = null, ): RawClassicCard { val sectors = ArrayList() + val sectorCount = tech.sectorCount - for (sectorIndex in 0 until tech.sectorCount) { + for (sectorIndex in 0 until sectorCount) { + onProgress?.invoke(sectorIndex, sectorCount) try { var authSuccess = false var successfulKey: ByteArray? = null diff --git a/card/desfire/src/androidMain/kotlin/com/codebutler/farebot/card/desfire/DesfireTagReader.kt b/card/desfire/src/androidMain/kotlin/com/codebutler/farebot/card/desfire/DesfireTagReader.kt index 898eac857..90c6d0e5c 100644 --- a/card/desfire/src/androidMain/kotlin/com/codebutler/farebot/card/desfire/DesfireTagReader.kt +++ b/card/desfire/src/androidMain/kotlin/com/codebutler/farebot/card/desfire/DesfireTagReader.kt @@ -42,5 +42,6 @@ class DesfireTagReader( tag: Tag, tech: CardTransceiver, cardKeys: CardKeys?, - ): RawDesfireCard = DesfireCardReader.readCard(tagId, tech) + onProgress: (suspend (current: Int, total: Int) -> Unit)?, + ): RawDesfireCard = DesfireCardReader.readCard(tagId, tech, onProgress) } diff --git a/card/desfire/src/commonMain/kotlin/com/codebutler/farebot/card/desfire/DesfireCardReader.kt b/card/desfire/src/commonMain/kotlin/com/codebutler/farebot/card/desfire/DesfireCardReader.kt index 33db1135a..ec2ad2d0c 100644 --- a/card/desfire/src/commonMain/kotlin/com/codebutler/farebot/card/desfire/DesfireCardReader.kt +++ b/card/desfire/src/commonMain/kotlin/com/codebutler/farebot/card/desfire/DesfireCardReader.kt @@ -34,6 +34,7 @@ object DesfireCardReader { suspend fun readCard( tagId: ByteArray, tech: CardTransceiver, + onProgress: (suspend (current: Int, total: Int) -> Unit)? = null, ): RawDesfireCard { val desfireProtocol = DesfireProtocol(tech) @@ -53,7 +54,7 @@ object DesfireCardReader { intArrayOf() } - val apps = readApplications(desfireProtocol, appIds) + val apps = readApplications(desfireProtocol, appIds, onProgress) return RawDesfireCard.create(tagId, Clock.System.now(), apps, manufData, appListLocked) } @@ -61,6 +62,7 @@ object DesfireCardReader { private suspend fun readApplications( desfireProtocol: DesfireProtocol, appIds: IntArray, + onProgress: (suspend (current: Int, total: Int) -> Unit)? = null, ): List { val apps = ArrayList() @@ -70,7 +72,8 @@ object DesfireCardReader { // - Use unlocker.getOrder() to reorder file IDs // - Call unlocker.unlock() before reading each file - for (appId in appIds) { + for ((appIndex, appId) in appIds.withIndex()) { + onProgress?.invoke(appIndex, appIds.size) try { desfireProtocol.selectApp(appId) } catch (e: NotFoundException) { diff --git a/card/felica/src/androidMain/kotlin/com/codebutler/farebot/card/felica/FelicaTagReader.kt b/card/felica/src/androidMain/kotlin/com/codebutler/farebot/card/felica/FelicaTagReader.kt index db7fefb7d..d50a5a360 100644 --- a/card/felica/src/androidMain/kotlin/com/codebutler/farebot/card/felica/FelicaTagReader.kt +++ b/card/felica/src/androidMain/kotlin/com/codebutler/farebot/card/felica/FelicaTagReader.kt @@ -43,8 +43,9 @@ class FelicaTagReader( tag: Tag, tech: NfcFTechnology, cardKeys: CardKeys?, + onProgress: (suspend (current: Int, total: Int) -> Unit)?, ): RawFelicaCard { val adapter = AndroidFeliCaTagAdapter(tag) - return FeliCaReader.readTag(tagId, adapter) + return FeliCaReader.readTag(tagId, adapter, onProgress = onProgress) } } diff --git a/card/felica/src/commonMain/kotlin/com/codebutler/farebot/card/felica/FeliCaReader.kt b/card/felica/src/commonMain/kotlin/com/codebutler/farebot/card/felica/FeliCaReader.kt index 37c74aec4..8021999d3 100644 --- a/card/felica/src/commonMain/kotlin/com/codebutler/farebot/card/felica/FeliCaReader.kt +++ b/card/felica/src/commonMain/kotlin/com/codebutler/farebot/card/felica/FeliCaReader.kt @@ -45,6 +45,7 @@ object FeliCaReader { tagId: ByteArray, adapter: FeliCaTagAdapter, onlyFirst: Boolean = false, + onProgress: (suspend (current: Int, total: Int) -> Unit)? = null, ): RawFelicaCard { val idmBytes = adapter.getIDm() val idm = FeliCaIdm(idmBytes) @@ -85,6 +86,7 @@ object FeliCaReader { val systems = mutableListOf() for ((systemNumber, systemCode) in systemCodes.withIndex()) { + onProgress?.invoke(systemNumber, systemCodes.size) // We can get System Code 0 from magic fallbacks -- drop this. if (systemCode == 0) continue diff --git a/card/iso7816/src/commonMain/kotlin/com/codebutler/farebot/card/iso7816/ISO7816CardReader.kt b/card/iso7816/src/commonMain/kotlin/com/codebutler/farebot/card/iso7816/ISO7816CardReader.kt index 6bae932de..bc3d3ca39 100644 --- a/card/iso7816/src/commonMain/kotlin/com/codebutler/farebot/card/iso7816/ISO7816CardReader.kt +++ b/card/iso7816/src/commonMain/kotlin/com/codebutler/farebot/card/iso7816/ISO7816CardReader.kt @@ -67,11 +67,13 @@ object ISO7816CardReader { tagId: ByteArray, transceiver: CardTransceiver, appConfigs: List, + onProgress: (suspend (current: Int, total: Int) -> Unit)? = null, ): RawISO7816Card? { val protocol = ISO7816Protocol(transceiver) val applications = mutableListOf() - for (config in appConfigs) { + for ((configIndex, config) in appConfigs.withIndex()) { + onProgress?.invoke(configIndex, appConfigs.size) val app = tryReadApplication(protocol, config) ?: continue applications.add(app) } diff --git a/card/src/androidMain/kotlin/com/codebutler/farebot/card/TagReader.kt b/card/src/androidMain/kotlin/com/codebutler/farebot/card/TagReader.kt index 1336a98da..0ad6d0d8b 100644 --- a/card/src/androidMain/kotlin/com/codebutler/farebot/card/TagReader.kt +++ b/card/src/androidMain/kotlin/com/codebutler/farebot/card/TagReader.kt @@ -36,11 +36,11 @@ abstract class TagReader< private val mCardKeys: K?, ) { @Throws(Exception::class) - suspend fun readTag(): C { + suspend fun readTag(onProgress: (suspend (current: Int, total: Int) -> Unit)? = null): C { val tech = getTech(mTag) try { tech.connect() - return readTag(mTagId, mTag, tech, mCardKeys) + return readTag(mTagId, mTag, tech, mCardKeys, onProgress) } finally { if (tech.isConnected) { try { @@ -58,6 +58,7 @@ abstract class TagReader< tag: Tag, tech: T, cardKeys: K?, + onProgress: (suspend (current: Int, total: Int) -> Unit)? = null, ): C protected abstract fun getTech(tag: Tag): T diff --git a/card/src/wasmJsMain/kotlin/com/codebutler/farebot/card/nfc/pn533/WebUsbPN533Transport.kt b/card/src/wasmJsMain/kotlin/com/codebutler/farebot/card/nfc/pn533/WebUsbPN533Transport.kt index 141fd106e..9d20b5d63 100644 --- a/card/src/wasmJsMain/kotlin/com/codebutler/farebot/card/nfc/pn533/WebUsbPN533Transport.kt +++ b/card/src/wasmJsMain/kotlin/com/codebutler/farebot/card/nfc/pn533/WebUsbPN533Transport.kt @@ -286,6 +286,11 @@ private fun jsWebUsbStartTransferOut(dataStr: JsString) { """ (function() { window._fbUsbOut = { ready: false, error: null }; + if (!window._fbUsb || !window._fbUsb.device) { + window._fbUsbOut.error = 'Device not connected'; + window._fbUsbOut.ready = true; + return; + } var parts = dataStr.split(','); var bytes = new Uint8Array(parts.length); for (var i = 0; i < parts.length; i++) bytes[i] = parseInt(parts[i]); @@ -309,6 +314,10 @@ private fun jsWebUsbStartTransferIn(timeoutMs: Int) { """ (function() { window._fbUsbIn = { data: null, ready: false }; + if (!window._fbUsb || !window._fbUsb.device) { + window._fbUsbIn.ready = true; + return; + } var timer = setTimeout(function() { if (!window._fbUsbIn.ready) window._fbUsbIn.ready = true; }, timeoutMs); diff --git a/card/ultralight/src/androidMain/kotlin/com/codebutler/farebot/card/ultralight/UltralightTagReader.kt b/card/ultralight/src/androidMain/kotlin/com/codebutler/farebot/card/ultralight/UltralightTagReader.kt index 213fa6adb..95697f466 100644 --- a/card/ultralight/src/androidMain/kotlin/com/codebutler/farebot/card/ultralight/UltralightTagReader.kt +++ b/card/ultralight/src/androidMain/kotlin/com/codebutler/farebot/card/ultralight/UltralightTagReader.kt @@ -42,5 +42,6 @@ class UltralightTagReader( tag: Tag, tech: UltralightTechnology, cardKeys: CardKeys?, - ): RawUltralightCard = UltralightCardReader.readCard(tagId, tech) + onProgress: (suspend (current: Int, total: Int) -> Unit)?, + ): RawUltralightCard = UltralightCardReader.readCard(tagId, tech, onProgress) } diff --git a/card/ultralight/src/commonMain/kotlin/com/codebutler/farebot/card/ultralight/UltralightCardReader.kt b/card/ultralight/src/commonMain/kotlin/com/codebutler/farebot/card/ultralight/UltralightCardReader.kt index 08440a1e2..c4179058e 100644 --- a/card/ultralight/src/commonMain/kotlin/com/codebutler/farebot/card/ultralight/UltralightCardReader.kt +++ b/card/ultralight/src/commonMain/kotlin/com/codebutler/farebot/card/ultralight/UltralightCardReader.kt @@ -31,6 +31,7 @@ object UltralightCardReader { suspend fun readCard( tagId: ByteArray, tech: UltralightTechnology, + onProgress: (suspend (current: Int, total: Int) -> Unit)? = null, ): RawUltralightCard { // Detect card type using protocol commands (GET_VERSION, AUTH_1) val detectedType = detectCardType(tech) @@ -56,6 +57,7 @@ object UltralightCardReader { val pages = mutableListOf() var unauthorized = false while (pageNumber < size) { + onProgress?.invoke(pageNumber, size) if (pageNumber % 4 == 0) { try { pageBuffer = tech.readPages(pageNumber) diff --git a/card/vicinity/src/androidMain/kotlin/com/codebutler/farebot/card/vicinity/VicinityTagReader.kt b/card/vicinity/src/androidMain/kotlin/com/codebutler/farebot/card/vicinity/VicinityTagReader.kt index a603bafcc..9c50c44ee 100644 --- a/card/vicinity/src/androidMain/kotlin/com/codebutler/farebot/card/vicinity/VicinityTagReader.kt +++ b/card/vicinity/src/androidMain/kotlin/com/codebutler/farebot/card/vicinity/VicinityTagReader.kt @@ -48,5 +48,6 @@ class VicinityTagReader( tag: Tag, tech: VicinityTechnology, cardKeys: CardKeys?, + onProgress: (suspend (current: Int, total: Int) -> Unit)?, ): RawVicinityCard = VicinityCardReader.readCard(tagId, tech) } diff --git a/card/vicinity/src/commonMain/kotlin/com/codebutler/farebot/card/vicinity/VicinityCardReader.kt b/card/vicinity/src/commonMain/kotlin/com/codebutler/farebot/card/vicinity/VicinityCardReader.kt index 785f10b3c..f4cad34c5 100644 --- a/card/vicinity/src/commonMain/kotlin/com/codebutler/farebot/card/vicinity/VicinityCardReader.kt +++ b/card/vicinity/src/commonMain/kotlin/com/codebutler/farebot/card/vicinity/VicinityCardReader.kt @@ -48,6 +48,7 @@ object VicinityCardReader { suspend fun readCard( tagId: ByteArray, tech: VicinityTechnology, + onProgress: (suspend (current: Int, total: Int) -> Unit)? = null, ): RawVicinityCard { val uid = tech.uid