Initial import
This commit is contained in:
192
app/src/main/java/com/iiyh/emoneyinfo/nfc/NfcUtils.kt
Normal file
192
app/src/main/java/com/iiyh/emoneyinfo/nfc/NfcUtils.kt
Normal file
@ -0,0 +1,192 @@
|
||||
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))
|
||||
}
|
||||
Reference in New Issue
Block a user