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' diff --git a/README.md b/README.md index f48442c..139613e 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ This plugin's functionalities include: * ISO 18092 (NFC-F / FeliCa) * ISO 15963 (NFC-V) * R/W block / page / sector level data of tags complying with: - * MIFARE Classic / Ultralight (Android only) + * MIFARE Classic / Ultralight (Android only, MIFARE Classic Read & Write block for iOS) * ISO 15693 (iOS only) * transceive raw commands with tags / cards complying with: * ISO 7816 Smart Cards (layer 4, in APDUs) 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/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..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 @@ -89,7 +89,13 @@ class FlutterNfcKitPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { } } } - if (!nfcHandler.post(handledFn)) { + val looperThread = nfcHandler.looper?.thread + if (looperThread == null || !looperThread.isAlive) { + val thread = HandlerThread("FlutterNfcKit").apply { start() } + nfcHandler = Handler(thread.looper) + } + val posted = nfcHandler.post(handledFn) + if (!posted) { result.error("500", "Failed to post job to NFC Handler thread.", null) } } diff --git a/example/android/gradle.properties b/example/android/gradle.properties index 1e7bb61..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 @@ -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 diff --git a/ios/flutter_nfc_kit/Sources/flutter_nfc_kit/FlutterNfcKitPlugin.swift b/ios/flutter_nfc_kit/Sources/flutter_nfc_kit/FlutterNfcKitPlugin.swift index 3ef6b34..2bb6254 100644 --- a/ios/flutter_nfc_kit/Sources/flutter_nfc_kit/FlutterNfcKitPlugin.swift +++ b/ios/flutter_nfc_kit/Sources/flutter_nfc_kit/FlutterNfcKitPlugin.swift @@ -210,7 +210,19 @@ public class FlutterNfcKitPlugin: NSObject, FlutterPlugin, NFCTagReaderSessionDe let blockNumber = arguments["index"] as! Int tag.extendedReadSingleBlock(requestFlags: RequestFlag(rawValue: rawFlags), blockNumber: blockNumber, completionHandler: handler) } - } else { + } + else if case let .miFare(tag) = tag { + let blockNumber = arguments["index"] as! UInt8 + let commandPacket = Data([0x30, blockNumber]) //0x30 is the MIFARE Classic Read Command. + tag.sendMiFareCommand(commandPacket: commandPacket) { (data, error) in + if let error = error { + result(FlutterError(code: "405", message: "Something is wrong", details: nil)) + } else { + result(data) + } + } + } + else { result(FlutterError(code: "405", message: "readBlock not supported on this type of card", details: nil)) } } else if call.method == "writeBlock" { @@ -233,8 +245,22 @@ public class FlutterNfcKitPlugin: NSObject, FlutterPlugin, NFCTagReaderSessionDe let blockNumber = arguments["index"] as! Int tag.extendedWriteSingleBlock(requestFlags: RequestFlag(rawValue: rawFlags), blockNumber: blockNumber, dataBlock: data, completionHandler: handler) } - } else { - result(FlutterError(code: "405", message: "writeBlock not supported on this type of card", details: nil)) + } + else if case let .miFare(tag) = tag { + let blockNumber = arguments["index"] as! UInt8 + let writeCommand = Data([0xA2, blockNumber]) + data //0xA2 is the MIFARE Classic Write Command to write single block. + tag.sendMiFareCommand(commandPacket: writeCommand) { (response, error) in + if let error = error { + result(FlutterError(code: "500", message: "Communication error", details: nil)) + } + else + { + result(nil) + } + } + } + else { + result(FlutterError(code: "405", message: "writeBlock not supported on this type of card", details: nil)) } } else if call.method == "readNDEF" { if tag != nil { 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++; + } + } + } }