Update release version and fix Flazz NFC parsing
This commit is contained in:
@ -13,8 +13,8 @@ android {
|
||||
applicationId = "com.iiyh.emoneyinfo"
|
||||
minSdk = 28
|
||||
targetSdk = 36
|
||||
versionCode = 4
|
||||
versionName = "1.0.2"
|
||||
versionCode = 5
|
||||
versionName = "1.0.3"
|
||||
|
||||
vectorDrawables {
|
||||
useSupportLibrary = true
|
||||
|
||||
@ -24,7 +24,8 @@ data class TransactionItem(
|
||||
val date: Date,
|
||||
val amount: Long,
|
||||
val isCredit: Boolean,
|
||||
val locationName: String = ""
|
||||
val locationName: String = "",
|
||||
val terminalId: String = ""
|
||||
) {
|
||||
fun formattedAmount(): String {
|
||||
val formatter = NumberFormat.getCurrencyInstance(Locale.forLanguageTag("id-ID")).apply {
|
||||
|
||||
@ -246,12 +246,23 @@ private object BrizziReader {
|
||||
|
||||
private object FlazzReader {
|
||||
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? {
|
||||
AppLog.d("EmoneyInfoFlazz", "Starting IsoDep Flazz detection")
|
||||
val select = isoDep.transceiveSelect(selectDfAid)
|
||||
AppLog.d("EmoneyInfoFlazz", "Select DF status=${"%02X%02X".format(select.sw1, select.sw2)}")
|
||||
val fallbackSelect = if (!select.isSuccess()) {
|
||||
AppLog.d("EmoneyInfoFlazz", "Select AID status=${"%02X%02X".format(select.sw1, select.sw2)}")
|
||||
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(
|
||||
cla = 0x00,
|
||||
ins = 0xA4,
|
||||
@ -261,10 +272,11 @@ private object FlazzReader {
|
||||
le = 0x00
|
||||
)
|
||||
} 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(
|
||||
cla = 0x00,
|
||||
@ -293,7 +305,7 @@ private object FlazzReader {
|
||||
)
|
||||
AppLog.d("EmoneyInfoFlazz", "Balance status=${"%02X%02X".format(balanceResp.sw1, balanceResp.sw2)}")
|
||||
val balance = if (balanceResp.isSuccess() && balanceResp.data.size >= 4) {
|
||||
balanceResp.data.toHex().substring(2, 8).hexToLong()
|
||||
balanceResp.data.copyOfRange(0, 4).bigEndianLong()
|
||||
} else {
|
||||
0L
|
||||
}
|
||||
@ -319,123 +331,106 @@ private object FlazzReader {
|
||||
|
||||
private fun readFlazzHistory(isoDep: IsoDep, strings: AndroidStrings): List<TransactionItem> {
|
||||
val logCheck = isoDep.transceiveApdu(cla = 0x00, ins = 0xB0, p1 = 0x81, p2 = 0x00, le = 0x00)
|
||||
return if (logCheck.isSuccess()) {
|
||||
readV2History(isoDep, strings)
|
||||
} else {
|
||||
readV1History(isoDep, strings)
|
||||
}
|
||||
val v1 = readV1History(isoDep, strings)
|
||||
val v2 = if (logCheck.isSuccess()) readV2History(isoDep, strings) else emptyList()
|
||||
return (v1 + v2).distinctBy { it.historyKey() }.sortedByDescending { it.date }
|
||||
}
|
||||
|
||||
private fun readV1History(isoDep: IsoDep, strings: AndroidStrings): List<TransactionItem> {
|
||||
val part1 = StringBuilder()
|
||||
|
||||
for (index in 0 until 16) {
|
||||
val offset = index * 15
|
||||
val first = isoDep.transceiveApdu(cla = 0x00, ins = 0xB0, p1 = 0x84, p2 = offset, le = 60)
|
||||
if (!first.isSuccess()) break
|
||||
part1.append(first.data.toHex())
|
||||
return buildList {
|
||||
for (offset in history60Offsets) {
|
||||
val resp = isoDep.transceiveApdu(cla = 0x00, ins = 0xB0, p1 = 0x84, p2 = offset, le = 0x3C)
|
||||
if (resp.hasStatus(0x6B, 0x00)) break
|
||||
if (!resp.isSuccess() || resp.data.size < 60) break
|
||||
parseFlazz60Record(resp.data, strings)?.let(::add)
|
||||
}
|
||||
}
|
||||
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> {
|
||||
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)
|
||||
if (random.isSuccess()) {
|
||||
val auth = isoDep.transceiveApdu(
|
||||
cla = 0x90,
|
||||
ins = 0x32,
|
||||
p1 = 0x03,
|
||||
p2 = 0x00,
|
||||
data = ("0801" + random.data.toHex()).hexToBytes(),
|
||||
le = 41
|
||||
if (!random.isSuccess()) return emptyList()
|
||||
|
||||
return buildList {
|
||||
for (index in 0 until 256) {
|
||||
val resp = isoDep.transceiveApdu(
|
||||
cla = 0x90,
|
||||
ins = 0x32,
|
||||
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> {
|
||||
if (logs.isBlank() || logs.length % 120 != 0) return emptyList()
|
||||
return buildList {
|
||||
for (offset in logs.indices step 120) {
|
||||
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 be24(b0: Byte, b1: Byte, b2: Byte): Int {
|
||||
return ((b0.toInt() and 0xFF) shl 16) or
|
||||
((b1.toInt() and 0xFF) shl 8) or
|
||||
(b2.toInt() and 0xFF)
|
||||
}
|
||||
|
||||
private fun parseFlazz64Logs(logs: String, strings: AndroidStrings): List<TransactionItem> {
|
||||
if (logs.isBlank() || logs.length % 64 != 0) return emptyList()
|
||||
return buildList {
|
||||
for (offset in logs.indices step 64) {
|
||||
val chunk = logs.substring(offset, offset + 64)
|
||||
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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
private fun be32Prefixed(b0: Byte, b1: Byte, b2: Byte, b3: Byte): Long {
|
||||
return ((b0.toLong() and 0xFF) shl 24) or
|
||||
((b1.toLong() and 0xFF) shl 16) or
|
||||
((b2.toLong() and 0xFF) shl 8) or
|
||||
(b3.toLong() and 0xFF)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user