diff --git a/app/src/main/java/sh/sar/sw9000/MainActivity.kt b/app/src/main/java/sh/sar/sw9000/MainActivity.kt index 30dcf74..bcdd843 100644 --- a/app/src/main/java/sh/sar/sw9000/MainActivity.kt +++ b/app/src/main/java/sh/sar/sw9000/MainActivity.kt @@ -111,19 +111,25 @@ class MainActivity : ComponentActivity() { private fun enableReader(a: NfcAdapter) { val flags = NfcAdapter.FLAG_READER_NFC_A or NfcAdapter.FLAG_READER_NFC_B or NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK + // Raise the presence-check delay so the system doesn't poll the tag mid-read and drop + // the field during a slow card's dump. + val extras = Bundle().apply { + putInt(NfcAdapter.EXTRA_READER_PRESENCE_CHECK_DELAY, 5000) + } a.enableReaderMode(this, { tag -> - // Binder thread — safe to block on card I/O here. + // The reader-mode callback runs on a background (binder) thread. Read the card HERE, + // synchronously, rather than offloading to another thread and returning — returning + // early lets the system tear the field down before a slow card finishes, which drops + // the PAN on some cards. runOnUiThread { if (uiState is UiState.Waiting) uiState = UiState.Waiting(TapState.READING) } - thread { - val result = runCatching { EmvCardReader.read(tag) }.getOrElse { - EmvCardReader.Result( - uid = "", techList = emptyList(), historicalBytes = null, hiLayerResponse = null, - atqa = null, sak = null, apps = emptyList(), fields = emptyList(), apduLog = emptyList(), - error = "Read error: ${it.message ?: it.javaClass.simpleName}", - ) - } - runOnUiThread { uiState = UiState.Done(result) } + val result = runCatching { EmvCardReader.read(tag) }.getOrElse { + EmvCardReader.Result( + uid = "", techList = emptyList(), historicalBytes = null, hiLayerResponse = null, + atqa = null, sak = null, apps = emptyList(), fields = emptyList(), apduLog = emptyList(), + error = "Read error: ${it.message ?: it.javaClass.simpleName}", + ) } - }, flags, null) + runOnUiThread { uiState = UiState.Done(result) } + }, flags, extras) } } diff --git a/app/src/main/java/sh/sar/sw9000/nfc/EmvCardReader.kt b/app/src/main/java/sh/sar/sw9000/nfc/EmvCardReader.kt index 206b3c2..1274d2b 100644 --- a/app/src/main/java/sh/sar/sw9000/nfc/EmvCardReader.kt +++ b/app/src/main/java/sh/sar/sw9000/nfc/EmvCardReader.kt @@ -85,7 +85,7 @@ object EmvCardReader { var error: String? = null try { iso.connect() - iso.timeout = 5000 + iso.timeout = 10000 // 1) PPSE — the payment application directory. val ppse = exchange(iso, selectName("2PAY.SYS.DDF01"), "SELECT PPSE (2PAY.SYS.DDF01)", log) diff --git a/app/src/main/java/sh/sar/sw9000/ui/NfcCardAnimation.kt b/app/src/main/java/sh/sar/sw9000/ui/NfcCardAnimation.kt index cecb704..189c4c4 100644 --- a/app/src/main/java/sh/sar/sw9000/ui/NfcCardAnimation.kt +++ b/app/src/main/java/sh/sar/sw9000/ui/NfcCardAnimation.kt @@ -26,6 +26,7 @@ import androidx.compose.ui.graphics.drawscope.translate import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.material3.MaterialTheme @@ -49,6 +50,7 @@ fun NfcCardAnimation( state: TapState, label: String, modifier: Modifier = Modifier, + canvasHeight: Dp = 220.dp, ) { val cs = MaterialTheme.colorScheme var now by remember { mutableLongStateOf(0L) } @@ -72,7 +74,7 @@ fun NfcCardAnimation( Canvas( modifier = Modifier .fillMaxWidth() - .height(220.dp) + .height(canvasHeight) ) { drawScene(state, elapsed, cs.primary, cs.onSurface, cs.surfaceVariant, cs.error, cs.onPrimary) } diff --git a/app/src/main/java/sh/sar/sw9000/ui/Screens.kt b/app/src/main/java/sh/sar/sw9000/ui/Screens.kt index 6dc5eb2..bb80408 100644 --- a/app/src/main/java/sh/sar/sw9000/ui/Screens.kt +++ b/app/src/main/java/sh/sar/sw9000/ui/Screens.kt @@ -1,6 +1,5 @@ package sh.sar.sw9000.ui -import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -13,6 +12,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button import androidx.compose.material3.Card @@ -92,7 +92,8 @@ fun ResultScreen( NfcCardAnimation( state = if (result.hasData) TapState.SUCCESS else TapState.ERROR, label = if (result.hasData) "Card read" else "Couldn't read card", - modifier = Modifier.fillMaxWidth().padding(top = 8.dp), + canvasHeight = 96.dp, + modifier = Modifier.fillMaxWidth().padding(top = 4.dp), ) if (result.error != null && !result.hasData) { @@ -115,7 +116,13 @@ fun ResultScreen( .verticalScroll(rememberScrollState()) .padding(16.dp), ) { - if (tab == 0) FormattedView(result) else RawDumpView(result) + // Long-press any value to select and copy it. The inner Column arranges the cards + // vertically — SelectionContainer is a single slot and would otherwise stack them. + SelectionContainer { + Column { + if (tab == 0) FormattedView(result) else RawDumpView(result) + } + } Spacer(Modifier.height(16.dp)) Button(onClick = onReadAnother, modifier = Modifier.fillMaxWidth()) { Text("Read another card") } Spacer(Modifier.height(32.dp)) @@ -211,12 +218,19 @@ private fun TlvRow(node: TlvNode, depth: Int) { @Composable private fun MonoLabeled(prefix: String, text: String, color: androidx.compose.ui.graphics.Color) { - Row(Modifier.fillMaxWidth().horizontalScroll(rememberScrollState())) { + Row(Modifier.fillMaxWidth()) { if (prefix.isNotEmpty()) { Text(prefix, fontFamily = FontFamily.Monospace, fontSize = 12.sp, color = color) Spacer(Modifier.width(6.dp)) } - Text(text.ifBlank { "(empty)" }, fontFamily = FontFamily.Monospace, fontSize = 12.sp, color = color) + // Wrap long hex rather than scroll it — keeps row heights correct and is easy to select. + Text( + text.ifBlank { "(empty)" }, + modifier = Modifier.weight(1f), + fontFamily = FontFamily.Monospace, + fontSize = 12.sp, + color = color, + ) } }