Improvements on reading - optional on last movements

This commit is contained in:
Pablo
2026-03-09 22:20:10 +01:00
parent 77c2ded482
commit 487d1bb5dd
4 changed files with 96 additions and 5 deletions
@@ -209,6 +209,17 @@ fun CardReaderScreen(
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(top = 4.dp)
)
// Mostrar información adicional si está disponible
if (cardData.lastOperation != null) {
HorizontalDivider(modifier = Modifier.padding(vertical = 16.dp))
Text(
text = cardData.lastOperation,
fontSize = 14.sp,
color = MaterialTheme.colorScheme.secondary
)
}
}
}
@@ -5,6 +5,8 @@ import android.nfc.tech.IsoDep
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.text.SimpleDateFormat
import java.util.*
data class CardData(
val id: String,
@@ -36,9 +38,20 @@ class CardRepositoryImpl(
// Obtener saldo (bytes 29-31, 3 bytes, big endian, en céntimos)
val balance = toIntBigEndian(contract, 29, 3) / 100.0
// Intentar extraer información de última validación del contrato
val lastValidation = try {
extractLastValidation(contract)
} catch (e: Exception) {
null
}
isoDep.close()
Result.success(CardData(cardId, balance))
Result.success(CardData(
id = cardId,
balance = balance,
lastOperation = lastValidation
))
} catch (e: Exception) {
Result.failure(e)
}
@@ -78,10 +91,50 @@ class CardRepositoryImpl(
return sendApdu(isoDep, byteArrayOf(0x80.toByte(), 0x2E, 0x01, 0x00, 0x20), 34)
}
private fun extractLastValidation(contract: ByteArray): String? {
return try {
// El contrato tiene 32 bytes de datos útiles (34 total con SW)
// Intentar extraer fecha de última validación si está disponible
// Bytes 8-9: posible fecha de última validación (días desde epoch Calypso)
if (contract.size >= 11) {
val days = toIntBigEndian(contract, 8, 2)
if (days > 0 && days < 20000) { // Validación: entre 1997 y ~2050
val epochDate = Calendar.getInstance().apply {
set(1997, Calendar.JANUARY, 1, 0, 0, 0)
set(Calendar.MILLISECOND, 0)
add(Calendar.DAY_OF_YEAR, days)
}
val dateFormat = SimpleDateFormat("dd/MM/yyyy", Locale.getDefault())
return "Última validación: ${dateFormat.format(epochDate.time)}"
}
}
null
} catch (e: Exception) {
null
}
}
private fun sendApdu(isoDep: IsoDep, command: ByteArray, responseSize: Int, statusWord: String = "9000"): ByteArray {
val response = isoDep.transceive(command)
requireSize(response, responseSize)
requireStatusWord(response, statusWord)
// Verificar status word primero
if (response.size >= 2) {
val sw = String.format("%02X%02X", response[response.size - 2], response[response.size - 1])
// 6A83 = Record not found (es normal para registros vacíos)
if (sw == "6A83" || sw == "6A82") {
throw IllegalStateException("Registro no encontrado")
}
if (sw != statusWord) {
throw IllegalArgumentException("APDU error: expected status word $statusWord but was $sw")
}
}
// El tamaño puede variar ligeramente, ser flexible
if (response.size < responseSize - 2 || response.size > responseSize + 2) {
throw IllegalArgumentException("APDU response size error: expected ~$responseSize bytes but was ${response.size}")
}
return response
}