@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user