Skip to content

Commit

Permalink
Merge pull request andras-adam#19 from NeoAren/nfc-data
Browse files Browse the repository at this point in the history
Nfc data
  • Loading branch information
andras-adam committed Oct 13, 2022
2 parents 48ee89a + 8d2ac1c commit 7e4cb62
Show file tree
Hide file tree
Showing 16 changed files with 530 additions and 108 deletions.
61 changes: 61 additions & 0 deletions app/src/main/java/com/virtualtag/app/data/MifareClassicInfo.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.virtualtag.app.data

import android.nfc.tech.MifareClassic
import android.nfc.tech.NfcA
import android.util.Log
import com.virtualtag.app.utils.toHex
import java.io.IOException

class MifareClassicInfo(tag: MifareClassic) {
val timeout: Int
val maxTransceiveLength: Int
val atqa: String
val sak: Int
val size: Int
val type: Int
val sectorCount: Int
val blockCount: Int
val data: String

init {
timeout = tag.timeout
maxTransceiveLength = tag.maxTransceiveLength
size = tag.size
type = tag.type
sectorCount = tag.sectorCount
blockCount = tag.blockCount
// MifareClassic tags are also NfcA tags
val nfca = NfcA.get(tag.tag)
atqa = nfca.atqa.toHex()
sak = nfca.sak.toInt()
// Read memory data
data = readData(tag)
}

private fun readData(tag: MifareClassic): String {
try {
var dataAccumulator = ""
// Connect to the tag
tag.connect()
// Loop through sectors on the tag
for (currentSector in 0 until tag.sectorCount) {
// Authenticate sector with the default key
val auth = tag.authenticateSectorWithKeyA(currentSector, MifareClassic.KEY_DEFAULT)
if (!auth) throw IOException("Failed to authenticate sector")
// Get block count in sector
val sectorBlockCount = tag.getBlockCountInSector(currentSector)
// Loop through blocks in the sector
for (currentBlockInSector in 0 until sectorBlockCount) {
// Calculate the block's index
val blockIndex = tag.sectorToBlock(currentSector) + currentBlockInSector
// Read the block's data and store as HEX
dataAccumulator += tag.readBlock(blockIndex).toHex()
}
}
return dataAccumulator
} catch (e: IOException) {
e.localizedMessage?.let { Log.e("MifareClassicHelper", it) }
return ""
}
}
}
52 changes: 52 additions & 0 deletions app/src/main/java/com/virtualtag/app/data/MifareUltralightInfo.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.virtualtag.app.data

import android.nfc.tech.MifareUltralight
import android.nfc.tech.NfcA
import android.util.Log
import com.virtualtag.app.utils.toHex
import java.io.IOException

class MifareUltralightInfo(tag: MifareUltralight) {
val type: Int
val timeout: Int
val maxTransceiveLength: Int
val atqa: String
val sak: Int
val data: String

init {
type = tag.type
timeout = tag.timeout
maxTransceiveLength = tag.maxTransceiveLength
// MifareUltralight tags are also NfcA tags
val nfca = NfcA.get(tag.tag)
atqa = nfca.atqa.toHex()
sak = nfca.sak.toInt()
// Read memory data
data = readData(tag)
}

private fun readData(tag: MifareUltralight): String {
try {
var dataAccumulator = ""
// Connect to the tag
tag.connect()
// Get the number of readable pages
val readablePagesCount = when (type) {
// Ultralight has 16 pages, all readable
MifareUltralight.TYPE_ULTRALIGHT -> 16
// Ultralight C has 48 pages, but the last 4 are unreadable
MifareUltralight.TYPE_ULTRALIGHT_C -> 44
else -> 0
}
// Read every fourth page, as 4 pages are read at a time by `readPages()`
for (currentPage in 0 until readablePagesCount) {
if (currentPage % 4 == 0) dataAccumulator += tag.readPages(currentPage).toHex()
}
return dataAccumulator
} catch (e: IOException) {
e.localizedMessage?.let { Log.e("MifareUltralightHelper", it) }
return ""
}
}
}
27 changes: 16 additions & 11 deletions app/src/main/java/com/virtualtag/app/data/ScanningViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,20 @@ import android.nfc.NfcAdapter
import android.nfc.Tag
import android.nfc.tech.MifareClassic
import android.nfc.tech.MifareUltralight
import android.nfc.tech.NdefFormatable
import android.nfc.tech.NfcA
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel

// Byte array to HEX helper
fun ByteArray.toHex(): String = joinToString(separator = "") { "%02x".format(it) }
import com.virtualtag.app.utils.toHex

class ScanningViewModel : ViewModel() {
// Currently scanned tag
// Currently scanned tag, mifare classic and ultralight information
private val _scannedTag by lazy { MutableLiveData<Tag>() }
val scannedTag = _scannedTag as LiveData<Tag>
private val _mifareClassicInfo by lazy { MutableLiveData<MifareClassicInfo>() }
val mifareClassicInfo = _mifareClassicInfo as LiveData<MifareClassicInfo>
private val _mifareUltralightInfo by lazy { MutableLiveData<MifareUltralightInfo>() }
val mifareUltralightInfo = _mifareUltralightInfo as LiveData<MifareUltralightInfo>

// Is scanning allowed state variable
private var isScanning = false
Expand All @@ -40,11 +40,8 @@ class ScanningViewModel : ViewModel() {
val intentFilter = IntentFilter(NfcAdapter.ACTION_TECH_DISCOVERED)
val intentFiltersArray = arrayOf(intentFilter)
val techListsArray = arrayOf(
arrayOf(
NfcA::class.java.name,
MifareUltralight::class.java.name,
NdefFormatable::class.java.name
)
arrayOf(MifareClassic::class.java.name),
arrayOf(MifareUltralight::class.java.name),
)
// Enable foreground dispatch
adapter.enableForegroundDispatch(
Expand Down Expand Up @@ -74,6 +71,14 @@ class ScanningViewModel : ViewModel() {
if (intent?.action == NfcAdapter.ACTION_TECH_DISCOVERED) {
val tag: Tag? = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG)
Log.i("NFC", "scanned ${tag?.id?.toHex()}")
if (tag?.techList?.contains(MifareClassic::class.java.name) == true) {
val mfc = MifareClassic.get(tag)
_mifareClassicInfo.value = MifareClassicInfo(mfc)
}
if (tag?.techList?.contains(MifareUltralight::class.java.name) == true) {
val mfu = MifareUltralight.get(tag)
_mifareUltralightInfo.value = MifareUltralightInfo(mfu)
}
_scannedTag.value = tag
}
}
Expand Down
21 changes: 19 additions & 2 deletions app/src/main/java/com/virtualtag/app/db/Card.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,26 @@ data class Card(
*/
@PrimaryKey
val id: String,
// TODO - figure out what kind of card data to save (Tag cannot be saved in Room)
// val data: Tag,
val name: String,
val color: String,
val techList: String,

// MifareClassic properties
val mifareClassicAtqa: String? = null,
val mifareClassicSak: Int? = null,
val mifareClassicTimeout: Int? = null,
val mifareClassicMaxTransceiveLength: Int? = null,
val mifareClassicSize: Int? = null,
val mifareClassicType: Int? = null,
val mifareClassicSectorCount: Int? = null,
val mifareClassicBlockCount: Int? = null,
val mifareClassicData: String? = null,

// MifareUltralight properties
val mifareUltralightAtqa: String? = null,
val mifareUltralightSak: Int? = null,
val mifareUltralightTimeout: Int? = null,
val mifareUltralightMaxTransceiveLength: Int? = null,
val mifareUltralightType: Int? = null,
val mifareUltralightData: String? = null,
)
4 changes: 2 additions & 2 deletions app/src/main/java/com/virtualtag/app/db/CardDAO.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ interface CardDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun addCard(card: Card)

@Update
fun updateCard(card: Card)
@Query("UPDATE card SET name = :name, color = :color WHERE card.id = :id")
fun updateCard(id: String, name: String, color: String)

@Delete
fun deleteCard(card: Card)
Expand Down
4 changes: 2 additions & 2 deletions app/src/main/java/com/virtualtag/app/db/CardDatabase.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import androidx.room.RoomDatabase

@Database(
entities = [Card::class],
version = 1
version = 3
)
abstract class CardDB : RoomDatabase() {
abstract fun cardDao(): CardDao
Expand All @@ -23,7 +23,7 @@ abstract class CardDB : RoomDatabase() {
Room.databaseBuilder(
context.applicationContext,
CardDB::class.java, "cards.db"
).build()
).fallbackToDestructiveMigration().build()
}
return sInstance!!
}
Expand Down
52 changes: 52 additions & 0 deletions app/src/main/java/com/virtualtag/app/ui/components/DataRow.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.virtualtag.app.ui.components

import android.widget.Toast
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.Divider
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.virtualtag.app.R

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun DataRow(title: String, content: String, icon: ImageVector) {
val context = LocalContext.current
val clipboardManager: ClipboardManager = LocalClipboardManager.current
Column(modifier = Modifier
.fillMaxWidth()
.combinedClickable(
onLongClick = {
clipboardManager.setText(AnnotatedString(content))
Toast
.makeText(context, context.getString(R.string.clipboard), Toast.LENGTH_SHORT)
.show()
},
onClick = { /* do nothing */ })) {
Row(modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp)) {
Icon(icon, contentDescription = null, modifier = Modifier
.size(48.dp)
.padding(horizontal = 4.dp))
Column(modifier = Modifier
.fillMaxWidth()
.padding(top = 2.dp, end = 16.dp)) {
Text(title, fontWeight = FontWeight.Bold, modifier = Modifier.padding(bottom = 4.dp))
Text(content, modifier = Modifier.padding(bottom = 8.dp))
}
}
Divider(color = Color.LightGray)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package com.virtualtag.app.ui.components

import android.nfc.tech.MifareClassic
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.virtualtag.app.db.Card
import com.virtualtag.app.utils.formatHex
import com.virtualtag.app.R

@Composable
fun MifareClassicView(card: Card) {
val id = formatHex(card.id)
val techList = card.techList
.replace("android.nfc.tech.", "")
.replace(",", ", ")
val type = when (card.mifareClassicType ?: MifareClassic.TYPE_UNKNOWN) {
MifareClassic.TYPE_CLASSIC -> "MifareClassic Classic"
MifareClassic.TYPE_PLUS -> "MifareClassic Plus"
MifareClassic.TYPE_PRO -> "MifareClassic Pro"
else -> "MifareClassic ${stringResource(R.string.unknown)}"
}
val sectorCount = card.mifareClassicSectorCount ?: 0
val blockCount = card.mifareClassicBlockCount ?: 0
val size = (card.mifareClassicSize ?: 0) * MifareClassic.BLOCK_SIZE
val timeout = card.mifareClassicTimeout ?: 0
val maxTranscieveLength = card.mifareClassicMaxTransceiveLength ?: 0
val atqa = card.mifareClassicAtqa ?: ""
val sak = card.mifareClassicSak ?: 0
val data = card.mifareClassicData ?: ""

LazyColumn(modifier = Modifier.fillMaxWidth()) {
item {
DataRow(
title = stringResource(R.string.serial_number),
content = id,
icon = Icons.Filled.Key)
}
item {
DataRow(
title = stringResource(R.string.technologies),
content = techList,
icon = Icons.Filled.Style)
}
item {
DataRow(
title = stringResource(R.string.type),
content = type,
icon = Icons.Filled.Category)
}
item {
DataRow(
title = stringResource(R.string.mem_size),
content = "$size bytes",
icon = Icons.Filled.Save)
}
item {
DataRow(
title = stringResource(R.string.mem_sector_count),
content = "$sectorCount",
icon = Icons.Filled.Save)
}
item {
DataRow(
title = stringResource(R.string.mem_block_count),
content = "$blockCount",
icon = Icons.Filled.Save)
}
item {
DataRow(
title = stringResource(R.string.transcieve_timeout),
content = "$timeout ${stringResource(R.string.ms)}",
icon = Icons.Filled.Sensors)
}
item {
DataRow(
title = stringResource(R.string.transcieve_max_length),
content = "$maxTranscieveLength ${stringResource(R.string.bytes)}",
icon = Icons.Filled.Sensors)
}
item {
DataRow(
title = stringResource(R.string.atqa),
content = atqa,
icon = Icons.Filled.Code)
}
item {
DataRow(
title = stringResource(R.string.sak),
content = "$sak",
icon = Icons.Filled.Code)
}
if (data.isNotEmpty()) {
items(blockCount) { index ->
DataRow(
title = "${stringResource(R.string.mem_block)} ${index + 1}",
content = formatHex(data.substring(index * 32, index * 32 + 32)),
icon = Icons.Filled.Memory)
}
}
}
}
Loading

0 comments on commit 7e4cb62

Please sign in to comment.