From eb585d5cfd066cca1ee1b66728816d02817493f6 Mon Sep 17 00:00:00 2001 From: albertoboccolini Date: Thu, 19 Jun 2025 21:04:41 +0200 Subject: [PATCH 1/6] Add custom read/write helpers for MIFARE Classic tags --- lib/flutter_nfc_kit.dart | 134 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) diff --git a/lib/flutter_nfc_kit.dart b/lib/flutter_nfc_kit.dart index c051837..e414b23 100644 --- a/lib/flutter_nfc_kit.dart +++ b/lib/flutter_nfc_kit.dart @@ -523,4 +523,138 @@ class FlutterNfcKit { static Future readSector(int index) async { return await _channel.invokeMethod('readSector', {'index': index}); } + + /// Read a custom number of bytes from a MIFARE Classic tag (Android only). + /// + /// Requires a valid NFC session and a MIFARE Classic tag. + /// + /// Authentication is performed on each sector before reading. + /// Reading starts from [startSector] and [startBlockInSector], and continues + /// until [numBytes] are read. + /// + /// Note: Trailer blocks (last block of each sector) are skipped for safety, + /// as they contain key and access bits. + /// + /// Throws an [Exception] if the tag is not MIFARE Classic or if authentication fails. + /// + /// Returns a [List] of the requested bytes (up to [numBytes]). + Future> readDataFromTag({ + required NFCTag tag, + int startSector = 1, + int startBlockInSector = 1, + int numBytes = 36, + String keyA = 'FFFFFFFFFFFF', + String? keyB, + }) async { + if (tag.type != NFCTagType.mifare_classic) { + throw Exception( + 'Only MIFARE Classic tags are supported for this operation.'); + } + + final List result = []; + final int neededBlocks = (numBytes / 16).ceil(); + int blocksRead = 0; + + for (int currentSector = startSector; + currentSector < tag.mifareInfo!.sectorCount!; + currentSector++) { + if (blocksRead >= neededBlocks) { + break; + } + + final bool authOk = await FlutterNfcKit.authenticateSector(currentSector, + keyA: keyA, keyB: keyB); + if (!authOk) { + throw Exception('Authentication failed for $currentSector sector.'); + } + + int blockStart = (currentSector == startSector) ? startBlockInSector : 0; + int blocksInSector = (currentSector < 32) ? 4 : 16; + + for (int blockOffset = blockStart; + blockOffset < blocksInSector - 1; + blockOffset++) { + if (blocksRead >= neededBlocks) break; + + final int blockIndex = (currentSector < 32) + ? currentSector * 4 + blockOffset + : 32 * 4 + (currentSector - 32) * 16 + blockOffset; + + final Uint8List block = await FlutterNfcKit.readBlock(blockIndex); + result.addAll(block); + blocksRead++; + } + } + + return result.take(numBytes).toList(); + } + + /// Write a list of bytes to a MIFARE Classic tag (Android only). + /// + /// Requires a valid NFC session and a MIFARE Classic tag. + /// + /// Authentication is performed on each sector before writing. + /// Writing starts from [startSector] and [startBlockInSector], and continues + /// until all bytes in [data] are written. + /// + /// Note: Trailer blocks (last block of each sector) are skipped to prevent + /// overwriting sector keys and access conditions. + /// + /// Each block is 16 bytes. If a block is partially filled, it is padded with zeros. + /// + /// Throws an [Exception] if the tag is not MIFARE Classic or if authentication fails. + Future writeDataToTag({ + required NFCTag tag, + required List data, + int startSector = 1, + int startBlockInSector = 1, + String keyA = 'FFFFFFFFFFFF', + String? keyB, + }) async { + if (tag.type != NFCTagType.mifare_classic) { + throw Exception( + 'Only MIFARE Classic tags are supported for this operation.'); + } + + final int neededBlocks = (data.length / 16).ceil(); + int blocksWritten = 0; + + for (int currentSector = startSector; + currentSector < tag.mifareInfo!.sectorCount!; + currentSector++) { + if (blocksWritten >= neededBlocks) { + break; + } + + final bool authOk = await FlutterNfcKit.authenticateSector(currentSector, + keyA: keyA, keyB: keyB); + if (!authOk) { + throw Exception('Authentication failed for $currentSector sector.'); + } + + int blockStart = (currentSector == startSector) ? startBlockInSector : 0; + int blocksInSector = (currentSector < 32) ? 4 : 16; + + for (int blockOffset = blockStart; + blockOffset < blocksInSector - 1; + blockOffset++) { + if (blocksWritten >= neededBlocks) { + break; + } + + final int blockIndex = (currentSector < 32) + ? currentSector * 4 + blockOffset + : 32 * 4 + (currentSector - 32) * 16 + blockOffset; + + final List blockData = List.filled(16, 0); + for (int i = 0; i < 16 && (blocksWritten * 16 + i) < data.length; i++) { + blockData[i] = data[blocksWritten * 16 + i]; + } + + await FlutterNfcKit.writeBlock( + blockIndex, Uint8List.fromList(blockData)); + blocksWritten++; + } + } + } } From e34a7552ae40fde193707f55c774d6aefe8dc348 Mon Sep 17 00:00:00 2001 From: Akshya Date: Thu, 4 Sep 2025 17:41:33 +0530 Subject: [PATCH 2/6] Fix: Ensure NFC Handler thread is alive for Android SDK 35 --- .../kotlin/im/nfc/flutter_nfc_kit/FlutterNfcKitPlugin.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/android/src/main/kotlin/im/nfc/flutter_nfc_kit/FlutterNfcKitPlugin.kt b/android/src/main/kotlin/im/nfc/flutter_nfc_kit/FlutterNfcKitPlugin.kt index 2ed8160..f476c22 100644 --- a/android/src/main/kotlin/im/nfc/flutter_nfc_kit/FlutterNfcKitPlugin.kt +++ b/android/src/main/kotlin/im/nfc/flutter_nfc_kit/FlutterNfcKitPlugin.kt @@ -89,9 +89,12 @@ class FlutterNfcKitPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { } } } - if (!nfcHandler.post(handledFn)) { - result.error("500", "Failed to post job to NFC Handler thread.", null) + val looperThread = nfcHandler.looper?.thread + if (looperThread == null || !looperThread.isAlive) { + val thread = HandlerThread("FlutterNfcKit").apply { start() } + nfcHandler = Handler(thread.looper) } + nfcHandler.post(handledFn) } private fun parseTag(tag: Tag): String { From f583e3ff0ac6e49a4d3a59b870af132a9ee0d376 Mon Sep 17 00:00:00 2001 From: Akshya Date: Thu, 4 Sep 2025 19:52:27 +0530 Subject: [PATCH 3/6] fix: add error handling for failed NFC Handler.post --- .../kotlin/im/nfc/flutter_nfc_kit/FlutterNfcKitPlugin.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/android/src/main/kotlin/im/nfc/flutter_nfc_kit/FlutterNfcKitPlugin.kt b/android/src/main/kotlin/im/nfc/flutter_nfc_kit/FlutterNfcKitPlugin.kt index f476c22..49095c3 100644 --- a/android/src/main/kotlin/im/nfc/flutter_nfc_kit/FlutterNfcKitPlugin.kt +++ b/android/src/main/kotlin/im/nfc/flutter_nfc_kit/FlutterNfcKitPlugin.kt @@ -94,7 +94,10 @@ class FlutterNfcKitPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { val thread = HandlerThread("FlutterNfcKit").apply { start() } nfcHandler = Handler(thread.looper) } - nfcHandler.post(handledFn) + val posted = nfcHandler.post(handledFn) + if (!posted) { + result.error("500", "Failed to post job to NFC Handler thread.", null) + } } private fun parseTag(tag: Tag): String { From 251e79462930281a6af75d069768b9d0a60fc1c0 Mon Sep 17 00:00:00 2001 From: Shengqi Chen Date: Fri, 5 Sep 2025 11:00:19 +0800 Subject: [PATCH 4/6] ci: bump action version Signed-off-by: Shengqi Chen --- .github/workflows/example-app.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/example-app.yml b/.github/workflows/example-app.yml index f7faa45..93bfe6b 100644 --- a/.github/workflows/example-app.yml +++ b/.github/workflows/example-app.yml @@ -17,8 +17,8 @@ jobs: variant: [debug, release] steps: - - uses: actions/checkout@v4 - - uses: actions/setup-java@v4 + - uses: actions/checkout@v5 + - uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: '21' @@ -49,7 +49,7 @@ jobs: variant: [debug, release] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: subosito/flutter-action@v2 with: channel: 'stable' From c96c7c3eaa97a83c7330776a4c7e5149ca7fa758 Mon Sep 17 00:00:00 2001 From: Shengqi Chen Date: Fri, 5 Sep 2025 11:05:04 +0800 Subject: [PATCH 5/6] build: bump to Gradle 9.0.0, AGP 8.13.0, Kotlin 2.2.10 Signed-off-by: Shengqi Chen --- android/gradle.properties | 6 +++--- android/gradle/wrapper/gradle-wrapper.properties | 2 +- example/android/gradle.properties | 4 ++-- example/android/gradle/wrapper/gradle-wrapper.properties | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/android/gradle.properties b/android/gradle.properties index 48dd80c..c800c46 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4096M android.enableJetifier=true -AGPVersion=8.7.3 -KotlinVersion=2.1.0 +AGPVersion=8.13.0 +KotlinVersion=2.2.10 diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 543c293..c9c48b5 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Fri Sep 08 22:01:14 CST 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/example/android/gradle.properties b/example/android/gradle.properties index 1e7bb61..ee38cb9 100644 --- a/example/android/gradle.properties +++ b/example/android/gradle.properties @@ -6,5 +6,5 @@ android.defaults.buildfeatures.buildconfig=true android.nonTransitiveRClass=false android.nonFinalResIds=false -AGPVersion=8.7.3 -KotlinVersion=2.1.0 +AGPVersion=8.13.0 +KotlinVersion=2.2.10 diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index 371c357..d054919 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip From c452afab81d1881963c42714b352594c4f803ccf Mon Sep 17 00:00:00 2001 From: Shengqi Chen Date: Fri, 5 Sep 2025 11:09:05 +0800 Subject: [PATCH 6/6] build: increase max Java heap to avoid OOM when building debug variant Signed-off-by: Shengqi Chen --- example/android/gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/android/gradle.properties b/example/android/gradle.properties index ee38cb9..0977159 100644 --- a/example/android/gradle.properties +++ b/example/android/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4096M android.useAndroidX=true android.enableJetifier=true