Update release version and fix Flazz NFC parsing

This commit is contained in:
2026-05-01 20:37:19 +07:00
parent 648584f133
commit 2f48a34609
3 changed files with 108 additions and 112 deletions

View File

@ -13,8 +13,8 @@ android {
applicationId = "com.iiyh.emoneyinfo" applicationId = "com.iiyh.emoneyinfo"
minSdk = 28 minSdk = 28
targetSdk = 36 targetSdk = 36
versionCode = 4 versionCode = 5
versionName = "1.0.2" versionName = "1.0.3"
vectorDrawables { vectorDrawables {
useSupportLibrary = true useSupportLibrary = true

View File

@ -24,7 +24,8 @@ data class TransactionItem(
val date: Date, val date: Date,
val amount: Long, val amount: Long,
val isCredit: Boolean, val isCredit: Boolean,
val locationName: String = "" val locationName: String = "",
val terminalId: String = ""
) { ) {
fun formattedAmount(): String { fun formattedAmount(): String {
val formatter = NumberFormat.getCurrencyInstance(Locale.forLanguageTag("id-ID")).apply { val formatter = NumberFormat.getCurrencyInstance(Locale.forLanguageTag("id-ID")).apply {

View File

@ -246,12 +246,23 @@ private object BrizziReader {
private object FlazzReader { private object FlazzReader {
private val selectDfAid = "A0000000180F0000018001".hexToBytes() private val selectDfAid = "A0000000180F0000018001".hexToBytes()
private val history60Offsets = listOf(0x00, 0x0F, 0x1E, 0x2D, 0x3C, 0x4B, 0x5A, 0x69, 0x78, 0x87)
fun read(isoDep: IsoDep, strings: AndroidStrings): EmoneyUiState? { fun read(isoDep: IsoDep, strings: AndroidStrings): EmoneyUiState? {
AppLog.d("EmoneyInfoFlazz", "Starting IsoDep Flazz detection") AppLog.d("EmoneyInfoFlazz", "Starting IsoDep Flazz detection")
val select = isoDep.transceiveSelect(selectDfAid) val select = isoDep.transceiveSelect(selectDfAid)
AppLog.d("EmoneyInfoFlazz", "Select DF status=${"%02X%02X".format(select.sw1, select.sw2)}") AppLog.d("EmoneyInfoFlazz", "Select AID status=${"%02X%02X".format(select.sw1, select.sw2)}")
val fallbackSelect = if (!select.isSuccess()) { if (!select.isSuccess()) return null
val selectDf = isoDep.transceiveApdu(
cla = 0x00,
ins = 0xA4,
p1 = 0x01,
p2 = 0x00,
data = byteArrayOf(0x02, 0x00)
)
AppLog.d("EmoneyInfoFlazz", "Select DF 0200 status=${"%02X%02X".format(selectDf.sw1, selectDf.sw2)}")
if (!selectDf.isSuccess()) {
isoDep.transceiveApdu( isoDep.transceiveApdu(
cla = 0x00, cla = 0x00,
ins = 0xA4, ins = 0xA4,
@ -261,10 +272,11 @@ private object FlazzReader {
le = 0x00 le = 0x00
) )
} else { } else {
select selectDf
}.also {
AppLog.d("EmoneyInfoFlazz", "Fallback select DF status=${"%02X%02X".format(it.sw1, it.sw2)}")
if (!it.isSuccess()) return null
} }
AppLog.d("EmoneyInfoFlazz", "Fallback select status=${"%02X%02X".format(fallbackSelect.sw1, fallbackSelect.sw2)}")
if (!fallbackSelect.isSuccess()) return null
val cardInfo = isoDep.transceiveApdu( val cardInfo = isoDep.transceiveApdu(
cla = 0x00, cla = 0x00,
@ -293,7 +305,7 @@ private object FlazzReader {
) )
AppLog.d("EmoneyInfoFlazz", "Balance status=${"%02X%02X".format(balanceResp.sw1, balanceResp.sw2)}") AppLog.d("EmoneyInfoFlazz", "Balance status=${"%02X%02X".format(balanceResp.sw1, balanceResp.sw2)}")
val balance = if (balanceResp.isSuccess() && balanceResp.data.size >= 4) { val balance = if (balanceResp.isSuccess() && balanceResp.data.size >= 4) {
balanceResp.data.toHex().substring(2, 8).hexToLong() balanceResp.data.copyOfRange(0, 4).bigEndianLong()
} else { } else {
0L 0L
} }
@ -319,123 +331,106 @@ private object FlazzReader {
private fun readFlazzHistory(isoDep: IsoDep, strings: AndroidStrings): List<TransactionItem> { private fun readFlazzHistory(isoDep: IsoDep, strings: AndroidStrings): List<TransactionItem> {
val logCheck = isoDep.transceiveApdu(cla = 0x00, ins = 0xB0, p1 = 0x81, p2 = 0x00, le = 0x00) val logCheck = isoDep.transceiveApdu(cla = 0x00, ins = 0xB0, p1 = 0x81, p2 = 0x00, le = 0x00)
return if (logCheck.isSuccess()) { val v1 = readV1History(isoDep, strings)
readV2History(isoDep, strings) val v2 = if (logCheck.isSuccess()) readV2History(isoDep, strings) else emptyList()
} else { return (v1 + v2).distinctBy { it.historyKey() }.sortedByDescending { it.date }
readV1History(isoDep, strings)
}
} }
private fun readV1History(isoDep: IsoDep, strings: AndroidStrings): List<TransactionItem> { private fun readV1History(isoDep: IsoDep, strings: AndroidStrings): List<TransactionItem> {
val part1 = StringBuilder() return buildList {
for (offset in history60Offsets) {
for (index in 0 until 16) { val resp = isoDep.transceiveApdu(cla = 0x00, ins = 0xB0, p1 = 0x84, p2 = offset, le = 0x3C)
val offset = index * 15 if (resp.hasStatus(0x6B, 0x00)) break
val first = isoDep.transceiveApdu(cla = 0x00, ins = 0xB0, p1 = 0x84, p2 = offset, le = 60) if (!resp.isSuccess() || resp.data.size < 60) break
if (!first.isSuccess()) break parseFlazz60Record(resp.data, strings)?.let(::add)
part1.append(first.data.toHex()) }
} }
for (index in 0 until 16) {
val offset = index * 15
val second = isoDep.transceiveApdu(cla = 0x00, ins = 0xB0, p1 = 0x85, p2 = offset, le = 60)
if (!second.isSuccess()) break
part1.append(second.data.toHex())
}
return parseFlazz120Logs(part1.toString(), strings)
} }
private fun readV2History(isoDep: IsoDep, strings: AndroidStrings): List<TransactionItem> { private fun readV2History(isoDep: IsoDep, strings: AndroidStrings): List<TransactionItem> {
val mapV1 = StringBuilder()
val mapV2 = StringBuilder()
for (index in 0 until 5) {
val offset = index * 60
val resp = isoDep.transceiveApdu(cla = 0x00, ins = 0xB0, p1 = 0x85, p2 = offset, le = 120)
if (!resp.isSuccess()) break
mapV1.append(resp.data.toHex())
}
for (index in 0 until 5) {
val offset = index * 60
val resp = isoDep.transceiveApdu(cla = 0x00, ins = 0xB0, p1 = 0x84, p2 = offset, le = 240)
if (!resp.isSuccess()) break
mapV1.append(resp.data.toHex())
}
val random = isoDep.transceiveApdu(cla = 0x00, ins = 0x84, p1 = 0x00, p2 = 0x00, le = 8) val random = isoDep.transceiveApdu(cla = 0x00, ins = 0x84, p1 = 0x00, p2 = 0x00, le = 8)
if (random.isSuccess()) { if (!random.isSuccess()) return emptyList()
val auth = isoDep.transceiveApdu(
cla = 0x90, return buildList {
ins = 0x32, for (index in 0 until 256) {
p1 = 0x03, val resp = isoDep.transceiveApdu(
p2 = 0x00, cla = 0x90,
data = ("0801" + random.data.toHex()).hexToBytes(), ins = 0x32,
le = 41 p1 = 0x03,
p2 = 0x00,
data = byteArrayOf(index.toByte()),
le = 0x20
)
if (resp.hasStatus(0x6A, 0x80)) break
if (!resp.isSuccess() || resp.data.size < 32) break
parseFlazz32Record(resp.data, strings)?.let(::add)
}
}
}
private fun parseFlazz60Record(data: ByteArray, strings: AndroidStrings): TransactionItem? {
val typeRaw = ((data[0].toInt() and 0xFF) shl 8) or (data[1].toInt() and 0xFF)
val transactionTime = be32Prefixed(data[38], data[39], data[40], data[41])
if (transactionTime <= 0L) return null
val amount = be24(data[6], data[7], data[8]).toLong()
val terminalId = data.copyOfRange(30, 38).toString(Charsets.US_ASCII).trim('\u0000', ' ')
val isPayment = typeRaw == 0x0400
val transactionLabel = if (isPayment) strings.get(R.string.payment) else strings.get(R.string.topup)
return TransactionItem(
title = transactionLabel,
date = flazzSecondsFrom1980(transactionTime),
amount = amount,
isCredit = !isPayment,
locationName = transactionLabel,
terminalId = terminalId
)
}
private fun parseFlazz32Record(data: ByteArray, strings: AndroidStrings): TransactionItem? {
val typeRaw = data[0].toInt() and 0xFF
val transactionTime = be32Prefixed(data[4], data[5], data[6], data[7])
if (transactionTime <= 0L) return null
val rawAmount = be24(data[1], data[2], data[3]).toLong()
val terminalId = data.copyOfRange(14, 22).toString(Charsets.US_ASCII).trim('\u0000', ' ')
val isPayment = typeRaw == 0x04
val amount = if (isPayment) 0x1000000L - rawAmount else rawAmount
val transactionLabel = if (isPayment) strings.get(R.string.payment) else strings.get(R.string.topup)
AppLog.d(
"EmoneyInfoFlazz",
"V2 record type=%02X rawAmount=%d amount=%d location=%s time=%d tailAmount=%d".format(
typeRaw,
rawAmount,
amount,
terminalId,
transactionTime,
be24(data[22], data[23], data[24])
) )
if (auth.isSuccess()) { )
for (index in 0 until 256) {
val resp = isoDep.transceiveApdu(cla = 0x00, ins = 0xB0, p1 = 0x89, p2 = index, le = 64)
if (!resp.isSuccess()) break
mapV2.append(resp.data.toHex())
}
for (index in 0 until 256) {
val resp = isoDep.transceiveApdu(
cla = 0x90,
ins = 0x32,
p1 = 0x03,
p2 = 0x00,
data = byteArrayOf(index.toByte()),
le = 32
)
if (!resp.isSuccess()) break
mapV2.append(resp.data.toHex())
}
}
}
return (parseFlazz120Logs(mapV1.toString(), strings) + parseFlazz64Logs(mapV2.toString(), strings)) return TransactionItem(
title = transactionLabel,
date = flazzSecondsFrom1980(transactionTime),
amount = amount,
isCredit = !isPayment,
locationName = transactionLabel,
terminalId = terminalId
)
} }
private fun parseFlazz120Logs(logs: String, strings: AndroidStrings): List<TransactionItem> { private fun be24(b0: Byte, b1: Byte, b2: Byte): Int {
if (logs.isBlank() || logs.length % 120 != 0) return emptyList() return ((b0.toInt() and 0xFF) shl 16) or
return buildList { ((b1.toInt() and 0xFF) shl 8) or
for (offset in logs.indices step 120) { (b2.toInt() and 0xFF)
val chunk = logs.substring(offset, offset + 120)
val transactionTime = chunk.substring(76, 84).hexToLong()
if (transactionTime <= 0) continue
val amount = chunk.substring(12, 18).hexToLong()
val type = chunk.substring(0, 4).hexToLong()
add(
TransactionItem(
title = if (type == 1024L) strings.get(R.string.payment) else strings.get(R.string.topup),
date = flazzSecondsFrom1980(transactionTime),
amount = amount,
isCredit = type != 1024L
)
)
}
}
} }
private fun parseFlazz64Logs(logs: String, strings: AndroidStrings): List<TransactionItem> { private fun be32Prefixed(b0: Byte, b1: Byte, b2: Byte, b3: Byte): Long {
if (logs.isBlank() || logs.length % 64 != 0) return emptyList() return ((b0.toLong() and 0xFF) shl 24) or
return buildList { ((b1.toLong() and 0xFF) shl 16) or
for (offset in logs.indices step 64) { ((b2.toLong() and 0xFF) shl 8) or
val chunk = logs.substring(offset, offset + 64) (b3.toLong() and 0xFF)
val transactionTime = chunk.substring(8, 16).hexToLong()
if (transactionTime <= 0) continue
val type = chunk.substring(0, 2).hexToLong()
val rawAmount = chunk.substring(2, 8).hexToLong()
val amount = if (type == 4L) 16777216L - rawAmount else rawAmount
add(
TransactionItem(
title = if (type == 4L) strings.get(R.string.payment) else strings.get(R.string.topup),
date = flazzSecondsFrom1980(transactionTime),
amount = amount,
isCredit = type != 4L
)
)
}
}
} }
} }