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 ): List { val packet = mutableListOf() 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)) }