Skip to content

Commit

Permalink
Refactor MIFARE-related interfaces, clean up code
Browse files Browse the repository at this point in the history
Signed-off-by: Harry Chen <harry-chen@outlook.com>
  • Loading branch information
Harry-Chen committed Sep 8, 2023
1 parent 09a85cf commit d47c05c
Show file tree
Hide file tree
Showing 5 changed files with 165 additions and 198 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ class FlutterNfcKitPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
}
}

/// NDEF-related methods below
"readNDEF" -> {
if (!ensureNDEF()) return
val ndef = ndefTechnology!!
Expand Down Expand Up @@ -300,6 +301,7 @@ class FlutterNfcKitPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
}
}

/// MIFARE/NTAG-related methods below
"authenticateSector" -> {
val tagTech = tagTechnology
if (tagTech == null || mifareInfo == null || mifareInfo!!.sectorCount == null) {
Expand Down Expand Up @@ -341,11 +343,16 @@ class FlutterNfcKitPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
result.error("406", "No Mifare tag polled", null)
return
}
val blockIndex = call.argument<Int>("index")!!
val index = call.argument<Int>("index")!!
val maxBlock = mifareInfo!!.blockCount
if (index !in 0 until maxBlock) {
result.error("400", "Invalid block/page index $index, should be in (0, $maxBlock)", null)
return
}
thread {
try {
switchTechnology(tagTech, ndefTechnology)
tagTech.readBlock(mifareInfo!!, blockIndex, result)
tagTech.readBlock(index, result)
} catch (ex: IOException) {
Log.e(TAG, "Read block error", ex)
result.error("500", "Communication error", ex.localizedMessage)
Expand All @@ -360,8 +367,9 @@ class FlutterNfcKitPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
return
}
val index = call.argument<Int>("index")!!
if (!(0 < index && index < mifareInfo!!.sectorCount!!)) {
result.error("400", "Invalid sector index $index", null)
val maxSector = mifareInfo!!.sectorCount!!
if (index !in 0 until maxSector) {
result.error("400", "Invalid sector index $index, should be in (0, $maxSector)", null)
return
}
thread {
Expand All @@ -381,17 +389,26 @@ class FlutterNfcKitPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
result.error("406", "No Mifare tag polled", null)
return
}
val blockIndex = call.argument<Int>("index")!!
val index = call.argument<Int>("index")!!
val maxBlock = mifareInfo!!.blockCount
if (index !in 0 until maxBlock) {
result.error("400", "Invalid block/page index $index, should be in (0, $maxBlock)", null)
return
}
val data = call.argument<Any>("data")
if (data == null || (data !is String && data !is ByteArray)) {
result.error("400", "Bad argument", null)
return
}
val (bytes, _) = canonicalizeData(data)
if (bytes.size != mifareInfo!!.blockSize) {
result.error("400", "Invalid data size ${bytes.size}, should be ${mifareInfo!!.blockSize}", null)
return
}
thread {
try {
switchTechnology(tagTech, ndefTechnology)
tagTech.writeBlock(mifareInfo!!, blockIndex, bytes, result)
tagTech.writeBlock(index, bytes, result)
} catch (ex: IOException) {
Log.e(TAG, "Read block error", ex)
result.error("500", "Communication error", ex.localizedMessage)
Expand Down
52 changes: 16 additions & 36 deletions android/src/main/kotlin/im/nfc/flutter_nfc_kit/MifareUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ package im.nfc.flutter_nfc_kit
import android.nfc.tech.MifareClassic
import android.nfc.tech.MifareUltralight
import android.nfc.tech.TagTechnology
import io.flutter.Log
import io.flutter.plugin.common.MethodChannel.Result
import java.io.IOException


data class MifareInfo(
Expand Down Expand Up @@ -80,13 +78,8 @@ object MifareUtils {

private val TAG = MifareUtils::class.java.name


/// read one block (16 bytes)
fun TagTechnology.readBlock(mifareInfo: MifareInfo, offset: Int, result: Result){
if (!(0 < offset && offset < mifareInfo.blockCount)) {
result.error("400", "Invalid block/page offset $offset", null)
return
}
fun TagTechnology.readBlock(offset: Int, result: Result){
when (this) {
is MifareClassic -> {
val data = readBlock(offset)
Expand All @@ -106,35 +99,22 @@ object MifareUtils {
}

/// write one smallest unit (1 block for Classic, 1 page for Ultralight)
fun TagTechnology.writeBlock(mifareInfo: MifareInfo, offset: Int, data: ByteArray, result: Result) {
if (!(0 < offset && offset < mifareInfo.blockCount)) {
result.error("400", "Invalid block/page offset $offset", null)
return
}
if (data.size != mifareInfo.blockSize) {
result.error("400", "Invalid data size ${data.size}, should be ${mifareInfo.blockSize}", null)
return
}
try {
when (this) {
is MifareClassic -> {
writeBlock(offset, data)
result.success("")
return
}
is MifareUltralight -> {
writePage(offset, data)
result.success("")
return
}
else -> {
result.error("405", "Cannot invoke write on non-Mifare card", null)
return
}
fun TagTechnology.writeBlock(offset: Int, data: ByteArray, result: Result) {
when (this) {
is MifareClassic -> {
writeBlock(offset, data)
result.success("")
return
}
is MifareUltralight -> {
writePage(offset, data)
result.success("")
return
}
else -> {
result.error("405", "Cannot invoke write on non-Mifare card", null)
return
}
} catch (ex: IOException) {
Log.e(TAG, "Read Error", ex)
result.error("500", "Communication error", ex.localizedMessage)
}
}

Expand Down
23 changes: 2 additions & 21 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -119,26 +119,7 @@ class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
setState(() {
_result = '1: $result1\n';
});
} else if (tag.type == NFCTagType.mifare_ultralight ||
tag.type == NFCTagType.mifare_classic ||
tag.type == NFCTagType.iso15693) {
if (tag.type == NFCTagType.mifare_classic ||
tag.type == NFCTagType.mifare_ultralight) {
List<List<String>> data = await FlutterNfcKit.readAll();
if (data != null) {
StringBuffer result = StringBuffer();
for(int i = 0; i < data.length; i++) {
List<String> sectorData = data[i];
for(int i = 0; i < sectorData.length; i ++) {
result.write(sectorData[i]);
result.write("\n");
}
}
setState(() {
_mifareResult = result.toString();
});
}
}
} else if (tag.ndefAvailable ?? false) {
var ndefRecords = await FlutterNfcKit.readNDEFRecords();
var ndefString = '';
for (int i = 0; i < ndefRecords.length; i++) {
Expand Down Expand Up @@ -169,7 +150,7 @@ class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
padding: const EdgeInsets.symmetric(horizontal: 20),
child: _tag != null
? Text(
'ID: ${_tag!.id}\nStandard: ${_tag!.standard}\nType: ${_tag!.type}\nATQA: ${_tag!.atqa}\nSAK: ${_tag!.sak}\nHistorical Bytes: ${_tag!.historicalBytes}\nProtocol Info: ${_tag!.protocolInfo}\nApplication Data: ${_tag!.applicationData}\nHigher Layer Response: ${_tag!.hiLayerResponse}\nManufacturer: ${_tag!.manufacturer}\nSystem Code: ${_tag!.systemCode}\nDSF ID: ${_tag!.dsfId}\nNDEF Available: ${_tag!.ndefAvailable}\nNDEF Type: ${_tag!.ndefType}\nNDEF Writable: ${_tag!.ndefWritable}\nNDEF Can Make Read Only: ${_tag!.ndefCanMakeReadOnly}\nNDEF Capacity: ${_tag!.ndefCapacity}\nMifareClassicType:${_tag!.mifareClassType}\nMifareClassicSize:${_tag!.mifareClassSize}\nMifareClassicBlockCount:${_tag!.mifareClassicBlockCount}\nMifareClassSectorCount:${_tag!.mifareClassSectorCount}\nMifareMaxTransceiveLength:${_tag!.mifareMaxTransceiveLength}\n\n Transceive Result:\n$_result\n\nBlock Message:\n$_mifareResult')
'ID: ${_tag!.id}\nStandard: ${_tag!.standard}\nType: ${_tag!.type}\nATQA: ${_tag!.atqa}\nSAK: ${_tag!.sak}\nHistorical Bytes: ${_tag!.historicalBytes}\nProtocol Info: ${_tag!.protocolInfo}\nApplication Data: ${_tag!.applicationData}\nHigher Layer Response: ${_tag!.hiLayerResponse}\nManufacturer: ${_tag!.manufacturer}\nSystem Code: ${_tag!.systemCode}\nDSF ID: ${_tag!.dsfId}\nNDEF Available: ${_tag!.ndefAvailable}\nNDEF Type: ${_tag!.ndefType}\nNDEF Writable: ${_tag!.ndefWritable}\nNDEF Can Make Read Only: ${_tag!.ndefCanMakeReadOnly}\nNDEF Capacity: ${_tag!.ndefCapacity}\nMifare Info:${_tag!.mifareInfo} Transceive Result:\n$_result\n\nBlock Message:\n$_mifareResult')
: const Text('No tag polled yet.')),
])))),
Center(
Expand Down
141 changes: 74 additions & 67 deletions lib/flutter_nfc_kit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,32 @@ enum NFCTagType {
unknown,
}

/// Metadata of a MIFARE-compatible tag
@JsonSerializable()
class MifareInfo {
/// MIFARE type
final String type;

/// Size in bytes
final int size;

/// Size of a block (Classic) / page (Ultralight) in bytes
final int blockSize;

/// Number of blocks (Classic) / pages (Ultralight), -1 if type is unknown
final int blockCount;

/// Number of sectors (Classic only)
final int? sectorCount;

MifareInfo(
this.type, this.size, this.blockSize, this.blockCount, this.sectorCount);

factory MifareInfo.fromJson(Map<String, dynamic> json) =>
_$MifareInfoFromJson(json);
Map<String, dynamic> toJson() => _$MifareInfoToJson(this);
}

/// Metadata of the polled NFC tag.
///
/// All fields except [type] and [standard] are in the format of hex string.
Expand Down Expand Up @@ -91,17 +117,9 @@ class NFCTag {
/// Custom probe data returned by WebUSB device (see [FlutterNfcKitWeb] for detail, only on Web)
final String? webUSBCustomProbeData;

/// Mifare
/// Return the type of this MIFARE Classic compatible tag
final int? mifareClassType;
/// Return the size of the tag in bytes
final int? mifareClassSize;
/// Return the number of MIFARE Classic sectors
final int? mifareClassSectorCount;
/// Return the total number of MIFARE Classic blocks
final int? mifareClassicBlockCount;
/// Return max transceive length
final int? mifareMaxTransceiveLength;
/// Mifare-related information (if available)
final MifareInfo? mifareInfo;

NFCTag(
this.type,
this.id,
Expand All @@ -121,12 +139,7 @@ class NFCTag {
this.ndefWritable,
this.ndefCanMakeReadOnly,
this.webUSBCustomProbeData,
this.mifareClassType,
this.mifareClassSize,
this.mifareClassSectorCount,
this.mifareClassicBlockCount,
this.mifareMaxTransceiveLength
);
this.mifareInfo);

factory NFCTag.fromJson(Map<String, dynamic> json) => _$NFCTagFromJson(json);
Map<String, dynamic> toJson() => _$NFCTagToJson(this);
Expand Down Expand Up @@ -331,6 +344,7 @@ class FlutterNfcKit {
}

/// iOS only, change currently displayed NFC reader session alert message with [message].
///
/// There must be a valid session when invoking.
/// On Android, call to this function does nothing.
static Future<void> setIosAlertMessage(String message) async {
Expand All @@ -346,66 +360,59 @@ class FlutterNfcKit {
return await _channel.invokeMethod('makeNdefReadOnly');
}

static Future<void> setAuthenticateKey({
required String authenticateKeyA,
String? authenticateKeyB
}) async {
await _channel.invokeMethod("setAuthenticateKey");
}

/// Read block message in given index, if [authenticateKeyA] null, the MifareClassic authenticateKeyA will use default.
/// There must be a valid session when invoking.
/// This would cause any other open TagTechnology to be closed.
static Future<String> readBlock({
required int blockIndex,
String? authenticateKeyA
}) async {
final data = await _channel.invokeMethod('readBlock', {
'blockIndex': blockIndex,
'authenticateKeyA': authenticateKeyA
/// Authenticate against a sector of MIFARE Classic tag.
///
/// Either one of [keyA] or [keyB] must be provided.
/// If both are provided, [keyA] will be used.
/// Returns whether authentication succeeds.
static Future<bool> authenticateSector<T>(
int index, {T? keyA, T? keyB}
) async {
assert(T is String || T is Uint8List);
return await _channel.invokeMethod('authenticateSector', {
'index': index,
'keyA': keyA,
'keyB': keyB
});
return data as String;
}

/// Read sector message in given index, if [authenticateKeyA] null, the MifareClassic authenticateKeyA will use default.
/// Read one block (16 bytes) from tag
///
/// There must be a valid session when invoking.
/// This would cause any other open TagTechnology to be closed.
static Future<List<String>> readSector({
required int sectorIndex,
String? authenticateKeyA,
}) async {
final data = await _channel.invokeMethod('readSector',
{ 'sectorIndex': sectorIndex,
'authenticateKeyA': authenticateKeyA
});
return List<String>.from(data);
/// [index] refers to the block / page index.
/// For MIFARE Classic tags, you must first authenticate against the corresponding sector.
/// For MIFARE Ultralight tags, four consecutive pages will be read.
/// Returns data in [Uint8List].
static Future<Uint8List> readBlock(int index) async {
return await _channel.invokeMethod('readBlock', {
'index': index
});
}

/// Read all message for MifareClassic or MifareUltralight.
/// Write one block (16B) / page (4B) to MIFARE Classic / Ultralight tag
///
/// There must be a valid session when invoking.
/// This would cause any other open TagTechnology to be closed.
static Future<List<List<String>>> readAll({String? authenticateKeyA}) async {
final data = await _channel.invokeMethod('readAll', {
'authenticateKeyA': authenticateKeyA,
}) as Map<dynamic, dynamic>;
final listOfSectors = <List<String>>[];
data.forEach((_, list) => listOfSectors.add(List<String>.from(list)));
return listOfSectors;
/// [index] refers to the block / page index.
/// For MIFARE Classic tags, you must first authenticate against the corresponding sector.
static Future<void> writeBlock<T>(int index, T data) async {
assert(T is String || T is Uint8List);
await _channel.invokeMethod('writeBlock', {
'index': index,
'data': data,
});
}


/// Write message (String type) to block in given index, if [authenticateKeyA] null, the MifareClassic authenticateKeyA will use default.
/// Read one sector from MIFARE Classic tag
///
/// There must be a valid session when invoking.
/// This would cause any other open TagTechnology to be closed.
static Future<void> writeBlock({
required int blockIndex,
required String message,
String? authenticateKeyA
}) async {
await _channel.invokeMethod('writeBlock', {
'blockIndex': blockIndex,
'data': message,
'authenticateKeyA': authenticateKeyA
/// [index] refers to the sector index.
/// You must first authenticate against the corresponding sector.
/// Note: not all sectors are 64B long, some tags might have 256B sectors.
/// Returns data in [Uint8List].
static Future<Uint8List> readSector(int index) async {
return await _channel.invokeMethod('readSector', {
'index': index
});
}

}
Loading

0 comments on commit d47c05c

Please sign in to comment.