Files
Emoney-Info---Android/app/src/main/java/com/iiyh/emoneyinfo/nfc/NfcUtils.kt
2026-04-22 22:31:52 +07:00

193 lines
5.7 KiB
Kotlin

package com.iiyh.emoneyinfo.nfc
import android.nfc.tech.IsoDep
import android.nfc.tech.NfcF
import com.iiyh.emoneyinfo.util.AppLog
import java.util.Calendar
import java.util.Date
import java.util.Locale
import java.util.TimeZone
private const val NFC_LOG_TAG = "EmoneyInfoNfc"
internal data class ApduResponse(
val data: ByteArray,
val sw1: Int,
val sw2: Int
) {
fun isSuccess(): Boolean = sw1 == 0x90 && sw2 == 0x00
fun hasStatus(sw1: Int, sw2: Int): Boolean = this.sw1 == sw1 && this.sw2 == sw2
}
internal fun IsoDep.transceiveSelect(aid: ByteArray): ApduResponse =
transceiveApdu(cla = 0x00, ins = 0xA4, p1 = 0x04, p2 = 0x00, data = aid)
internal fun IsoDep.transceiveApdu(
cla: Int,
ins: Int,
p1: Int,
p2: Int,
data: ByteArray? = null,
le: Int? = null
): ApduResponse {
val apdu = mutableListOf(
cla.toByte(),
ins.toByte(),
p1.toByte(),
p2.toByte()
)
if (data != null) {
apdu += data.size.toByte()
apdu += data.toList()
}
if (le != null) {
apdu += if (le == 256) 0x00 else (le and 0xFF).toByte()
}
val requestBytes = apdu.toByteArray()
AppLog.d(
NFC_LOG_TAG,
"APDU -> cla=%02X ins=%02X p1=%02X p2=%02X lc=%d le=%s data=%s".format(
cla and 0xFF,
ins and 0xFF,
p1 and 0xFF,
p2 and 0xFF,
data?.size ?: 0,
le?.toString() ?: "-",
data?.toHex() ?: ""
)
)
val response = try {
transceive(requestBytes)
} catch (error: Throwable) {
AppLog.e(
NFC_LOG_TAG,
"APDU !! cla=%02X ins=%02X failed: %s".format(
cla and 0xFF,
ins and 0xFF,
error.message ?: error.javaClass.simpleName
),
error
)
throw error
}
require(response.size >= 2) { "Invalid APDU response" }
val parsed = ApduResponse(
data = response.copyOf(response.size - 2),
sw1 = response[response.size - 2].toInt() and 0xFF,
sw2 = response[response.size - 1].toInt() and 0xFF
)
AppLog.d(
NFC_LOG_TAG,
"APDU <- sw=%02X%02X data=%s".format(parsed.sw1, parsed.sw2, parsed.data.toHex())
)
return parsed
}
internal fun NfcF.readWithoutEncryption(
serviceCode: ByteArray,
blockNumbers: List<Int>
): List<ByteArray> {
val packet = mutableListOf<Byte>()
packet += 0x00
packet += 0x06
packet += tag.id.toList()
packet += 0x01
packet += serviceCode.toList()
packet += blockNumbers.size.toByte()
blockNumbers.forEach { blockNo ->
packet += 0x80.toByte()
packet += blockNo.toByte()
}
packet[0] = packet.size.toByte()
val response = transceive(packet.toByteArray())
require(response.size >= 13) { "Invalid FeliCa response" }
val status1 = response[10].toInt() and 0xFF
val status2 = response[11].toInt() and 0xFF
require(status1 == 0x00 && status2 == 0x00) {
"FeliCa status error: $status1/$status2"
}
val blockCount = response[12].toInt() and 0xFF
var offset = 13
return buildList {
repeat(blockCount) {
add(response.copyOfRange(offset, offset + 16))
offset += 16
}
}
}
internal fun ByteArray.toHex(): String = joinToString("") { "%02X".format(it) }
internal fun String.hexToBytes(): ByteArray {
require(length % 2 == 0) { "Hex string must have even length" }
return chunked(2).map { it.toInt(16).toByte() }.toByteArray()
}
internal fun String.hexToLong(): Long = if (isBlank()) 0 else toLong(16)
internal fun String.hexToIntOrZero(): Int = toIntOrNull(16) ?: 0
internal fun String.reverseByteOrderHex(): String =
chunked(2).reversed().joinToString("")
internal fun String.safeSlice(start: Int, end: Int): String {
if (length <= start) return ""
return substring(start, minOf(end, length))
}
internal fun String.formatCardNumber(): String =
chunked(4).joinToString(" ").trim()
internal fun ByteArray.rotateLeftBytes(count: Int): ByteArray {
if (isEmpty()) return this
val shift = count % size
return copyOfRange(shift, size) + copyOfRange(0, shift)
}
internal fun String.twosComplementHexToLong(): Long {
val bits = length * 4
val value = hexToLong()
val signBit = 1L shl (bits - 1)
return if ((value and signBit) == 0L) value else value - (1L shl bits)
}
internal fun ByteArray.bigEndianLong(): Long =
fold(0L) { acc, byte -> (acc shl 8) or (byte.toInt() and 0xFF).toLong() }
internal fun ByteArray.littleEndianLong(): Long =
reversedArray().bigEndianLong()
internal fun parseDdmmyyHhmmss(datePart: String, timePart: String): Date? {
return runCatching {
val raw = datePart + timePart
java.text.SimpleDateFormat("ddMMyyHHmmss", Locale.US).parse(raw)
}.getOrNull()
}
internal fun julianSecondsFrom1995(seconds: Long): Date {
val calendar = Calendar.getInstance().apply {
timeZone = TimeZone.getTimeZone("UTC")
set(1995, Calendar.JANUARY, 1, 0, 0, 0)
set(Calendar.MILLISECOND, 0)
}
return Date(calendar.timeInMillis + (seconds * 1000))
}
internal fun kmtSecondsFrom2000(seconds: Long): Date {
val calendar = Calendar.getInstance(TimeZone.getTimeZone("Asia/Jakarta")).apply {
set(2000, Calendar.JANUARY, 1, 7, 0, 0)
set(Calendar.MILLISECOND, 0)
}
return Date(calendar.timeInMillis + (seconds * 1000))
}
internal fun flazzSecondsFrom1980(seconds: Long): Date {
val calendar = Calendar.getInstance(TimeZone.getTimeZone("Asia/Jakarta")).apply {
set(1980, Calendar.JANUARY, 1, 0, 0, 0)
set(Calendar.MILLISECOND, 0)
}
return Date(calendar.timeInMillis + (seconds * 1000))
}