From b860c911a4dea7bda431671cdbc4028863ce69c2 Mon Sep 17 00:00:00 2001 From: Robert Poll Date: Sun, 23 Nov 2025 13:52:29 +0000 Subject: [PATCH 1/3] androidReaderModeFlags Add optional flags to reader mode --- CHANGELOG.md | 9 +++++++ README.md | 23 ++++++++++++++++ .../flutter_nfc_kit/FlutterNfcKitPlugin.kt | 14 +++++++--- lib/flutter_nfc_kit.dart | 26 +++++++++++++++++++ 4 files changed, 69 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5717d9..49b3a59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## Unreleased + +### Added +* New `androidReaderModeFlags` parameter for `poll()` method to customize Android Reader Mode behavior + * Allows passing flags like `FLAG_READER_SKIP_NDEF_CHECK` (0x80) for faster tag detection (~500ms improvement) + * Supports `FLAG_READER_NO_PLATFORM_SOUNDS` (0x100) to disable system beeps for custom feedback + * Flags are combined with technology flags using bitwise OR + * Fully backward compatible - optional parameter defaults to null + ## 0.0.1 * Initial release diff --git a/README.md b/README.md index 03f9546..efb9fd7 100644 --- a/README.md +++ b/README.md @@ -72,3 +72,26 @@ We use error codes with similar meaning as HTTP status code. Brief explanation a ### Operation Mode We provide two operation modes: polling (default) and event streaming. Both can give the same `NFCTag` object. Please see [example](example/example.md) for more details. + +### Performance Optimization (Android) + +For Android applications, you can optimize NFC tag detection performance using the `androidReaderModeFlags` parameter: + +```dart +// Skip automatic NDEF discovery for ~500ms faster detection +// and disable platform sounds for custom feedback +const flags = 0x80 | 0x100; // FLAG_READER_SKIP_NDEF_CHECK | FLAG_READER_NO_PLATFORM_SOUNDS + +final tag = await FlutterNfcKit.poll( + androidReaderModeFlags: flags, +); +``` + +Common flags from [`NfcAdapter`](https://developer.android.com/reference/android/nfc/NfcAdapter#enableReaderMode(android.app.Activity,%20android.nfc.NfcAdapter.ReaderCallback,%20int,%20android.os.Bundle)): +- `0x80` - `FLAG_READER_SKIP_NDEF_CHECK`: Skip automatic NDEF discovery, improving detection speed by ~500ms +- `0x100` - `FLAG_READER_NO_PLATFORM_SOUNDS`: Disable system beep/vibration for custom audio/haptic feedback + +These flags are particularly useful for applications that: +- Need fast tag detection (e.g., access control, fast payments) +- Implement custom user feedback instead of system sounds +- Use custom NDEF reading logic instead of automatic discovery 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 e54c4c3..ca3e507 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 @@ -329,8 +329,9 @@ class FlutterNfcKitPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { val timeout = call.argument("timeout")!! // technology and option bits are set in Dart code val technologies = call.argument("technologies")!! + val readerModeFlags = call.argument("readerModeFlags") runOnNfcThread(result, "Poll") { - pollTag(nfcAdapter, result, timeout, technologies) + pollTag(nfcAdapter, result, timeout, technologies, readerModeFlags) } } @@ -583,7 +584,7 @@ class FlutterNfcKitPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { override fun onDetachedFromActivityForConfigChanges() {} - private fun pollTag(nfcAdapter: NfcAdapter, result: Result, timeout: Int, technologies: Int) { + private fun pollTag(nfcAdapter: NfcAdapter, result: Result, timeout: Int, technologies: Int, readerModeFlags: Int?) { pollingTimeoutTask = Timer().schedule(timeout.toLong()) { try { @@ -604,7 +605,14 @@ class FlutterNfcKitPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { result.success(jsonResult) } - nfcAdapter.enableReaderMode(activity.get(), pollHandler, technologies, null) + // Build final flags: combine technology flags with optional reader mode flags + val finalFlags = if (readerModeFlags != null) { + technologies or readerModeFlags + } else { + technologies + } + + nfcAdapter.enableReaderMode(activity.get(), pollHandler, finalFlags, null) } private class MethodResultWrapper(result: Result) : Result { diff --git a/lib/flutter_nfc_kit.dart b/lib/flutter_nfc_kit.dart index 1df0778..40006cd 100644 --- a/lib/flutter_nfc_kit.dart +++ b/lib/flutter_nfc_kit.dart @@ -325,10 +325,34 @@ class FlutterNfcKit { /// /// Note: Sometimes NDEF check [leads to error](https://github.com/nfcim/flutter_nfc_kit/issues/11), and disabling it might help. /// If disabled, you will not be able to use any NDEF-related methods in the current session. + /// + /// **Android Reader Mode Flags** ([androidReaderModeFlags]): + /// + /// Optional flags to customize Android's NFC Reader Mode behavior. These are combined + /// with the technology flags using bitwise OR. Common flags from `android.nfc.NfcAdapter`: + /// + /// - `0x80` (`FLAG_READER_SKIP_NDEF_CHECK`) - Skip automatic NDEF discovery, improving + /// tag detection speed by ~500ms. Use this for faster polling when you don't need + /// automatic NDEF parsing. + /// + /// - `0x100` (`FLAG_READER_NO_PLATFORM_SOUNDS`) - Disable system beep/vibration when + /// tags are detected. Useful when implementing custom audio/haptic feedback. + /// + /// Example for fast tag detection with custom feedback: + /// ```dart + /// const flags = 0x80 | 0x100; // SKIP_NDEF_CHECK | NO_PLATFORM_SOUNDS + /// final tag = await FlutterNfcKit.poll( + /// androidReaderModeFlags: flags, + /// ); + /// ``` + /// + /// See [Android NfcAdapter documentation](https://developer.android.com/reference/android/nfc/NfcAdapter#enableReaderMode(android.app.Activity,%20android.nfc.NfcAdapter.ReaderCallback,%20int,%20android.os.Bundle)) + /// for all available flags. static Future poll({ Duration? timeout, bool androidPlatformSound = true, bool androidCheckNDEF = true, + int? androidReaderModeFlags, String iosAlertMessage = "Hold your iPhone near the card", String iosMultipleTagMessage = "More than one tags are detected, please leave only one tag and try again.", @@ -354,6 +378,7 @@ class FlutterNfcKit { 'iosMultipleTagMessage': iosMultipleTagMessage, 'technologies': technologies, 'probeWebUSBMagic': probeWebUSBMagic, + 'readerModeFlags': androidReaderModeFlags, }); return NFCTag.fromJson(jsonDecode(data)); } @@ -529,4 +554,5 @@ class FlutterNfcKit { static Future readSector(int index) async { return await _channel.invokeMethod('readSector', {'index': index}); } + } From 0163bfd46b9ec60ee26da21d4d7a6913f7f232c7 Mon Sep 17 00:00:00 2001 From: Dang Fan Date: Tue, 20 Jan 2026 22:25:00 +0800 Subject: [PATCH 2/3] format flutter_nfc_kit.dart --- lib/flutter_nfc_kit.dart | 178 ++++++++++++++++++++++----------------- 1 file changed, 103 insertions(+), 75 deletions(-) diff --git a/lib/flutter_nfc_kit.dart b/lib/flutter_nfc_kit.dart index eab4ec8..51dc0cd 100644 --- a/lib/flutter_nfc_kit.dart +++ b/lib/flutter_nfc_kit.dart @@ -12,11 +12,7 @@ import 'package:universal_platform/universal_platform.dart'; part 'flutter_nfc_kit.g.dart'; /// Availability of the NFC reader. -enum NFCAvailability { - not_supported, - disabled, - available, -} +enum NFCAvailability { not_supported, disabled, available } /// Type of NFC tag. enum NFCTagType { @@ -50,7 +46,12 @@ class MifareInfo { final int? sectorCount; MifareInfo( - this.type, this.size, this.blockSize, this.blockCount, this.sectorCount); + this.type, + this.size, + this.blockSize, + this.blockCount, + this.sectorCount, + ); factory MifareInfo.fromJson(Map json) => _$MifareInfoFromJson(json); @@ -121,25 +122,26 @@ class NFCTag { final MifareInfo? mifareInfo; NFCTag( - this.type, - this.id, - this.standard, - this.atqa, - this.sak, - this.historicalBytes, - this.protocolInfo, - this.applicationData, - this.hiLayerResponse, - this.manufacturer, - this.systemCode, - this.dsfId, - this.ndefAvailable, - this.ndefType, - this.ndefCapacity, - this.ndefWritable, - this.ndefCanMakeReadOnly, - this.webUSBCustomProbeData, - this.mifareInfo); + this.type, + this.id, + this.standard, + this.atqa, + this.sak, + this.historicalBytes, + this.protocolInfo, + this.applicationData, + this.hiLayerResponse, + this.manufacturer, + this.systemCode, + this.dsfId, + this.ndefAvailable, + this.ndefType, + this.ndefCapacity, + this.ndefWritable, + this.ndefCanMakeReadOnly, + this.webUSBCustomProbeData, + this.mifareInfo, + ); factory NFCTag.fromJson(Map json) => _$NFCTagFromJson(json); Map toJson() => _$NFCTagToJson(this); @@ -173,16 +175,23 @@ class NDEFRawRecord { extension NDEFRecordConvert on ndef.NDEFRecord { /// Convert an [ndef.NDEFRecord] to encoded [NDEFRawRecord] NDEFRawRecord toRaw() { - return NDEFRawRecord(id?.toHexString() ?? '', payload?.toHexString() ?? '', - type?.toHexString() ?? '', tnf); + return NDEFRawRecord( + id?.toHexString() ?? '', + payload?.toHexString() ?? '', + type?.toHexString() ?? '', + tnf, + ); } /// Convert an [NDEFRawRecord] to decoded [ndef.NDEFRecord]. /// Use `NDEFRecordConvert.fromRaw` to invoke. static ndef.NDEFRecord fromRaw(NDEFRawRecord raw) { return ndef.decodePartialNdefMessage( - raw.typeNameFormat, raw.type.toBytes(), raw.payload.toBytes(), - id: raw.identifier == "" ? null : raw.identifier.toBytes()); + raw.typeNameFormat, + raw.type.toBytes(), + raw.payload.toBytes(), + id: raw.identifier == "" ? null : raw.identifier.toBytes(), + ); } } @@ -242,28 +251,30 @@ class Iso15693RequestFlags { return result; } - Iso15693RequestFlags( - {this.dualSubCarriers = false, - this.highDataRate = false, - this.inventory = false, - this.protocolExtension = false, - this.select = false, - this.address = false, - this.option = false, - this.commandSpecificBit8 = false}); + Iso15693RequestFlags({ + this.dualSubCarriers = false, + this.highDataRate = false, + this.inventory = false, + this.protocolExtension = false, + this.select = false, + this.address = false, + this.option = false, + this.commandSpecificBit8 = false, + }); /// decode bits from one byte as specified in ISO15693-3 factory Iso15693RequestFlags.fromRaw(int r) { assert(r >= 0 && r <= 0xFF, "raw flags must be in range [0, 255]"); var f = Iso15693RequestFlags( - dualSubCarriers: (r & 0x01) != 0, - highDataRate: (r & 0x02) != 0, - inventory: (r & 0x04) != 0, - protocolExtension: (r & 0x08) != 0, - select: (r & 0x10) != 0, - address: (r & 0x20) != 0, - option: (r & 0x40) != 0, - commandSpecificBit8: (r & 0x80) != 0); + dualSubCarriers: (r & 0x01) != 0, + highDataRate: (r & 0x02) != 0, + inventory: (r & 0x04) != 0, + protocolExtension: (r & 0x08) != 0, + select: (r & 0x10) != 0, + address: (r & 0x20) != 0, + option: (r & 0x40) != 0, + commandSpecificBit8: (r & 0x80) != 0, + ); return f; } } @@ -278,8 +289,9 @@ class FlutterNfcKit { static const MethodChannel _channel = MethodChannel('flutter_nfc_kit/method'); - static final EventChannel _tagEventChannel = - EventChannel('flutter_nfc_kit/event'); + static final EventChannel _tagEventChannel = EventChannel( + 'flutter_nfc_kit/event', + ); /// Stream of NFC tag events. Each event is a [NFCTag] object. /// @@ -297,10 +309,12 @@ class FlutterNfcKit { /// get the availablility of NFC reader on this device static Future get nfcAvailability async { - final String availability = - await _channel.invokeMethod('getNFCAvailability'); - return NFCAvailability.values - .firstWhere((it) => it.toString() == "NFCAvailability.$availability"); + final String availability = await _channel.invokeMethod( + 'getNFCAvailability', + ); + return NFCAvailability.values.firstWhere( + (it) => it.toString() == "NFCAvailability.$availability", + ); } /// Try to poll a NFC tag from reader. @@ -410,7 +424,7 @@ class FlutterNfcKit { assert(capdu is String || capdu is Uint8List); return await _channel.invokeMethod('transceive', { 'data': capdu, - 'timeout': timeout?.inMilliseconds ?? TRANSCEIVE_TIMEOUT + 'timeout': timeout?.inMilliseconds ?? TRANSCEIVE_TIMEOUT, }); } @@ -421,9 +435,9 @@ class FlutterNfcKit { /// On Android, this would cause any other open TagTechnology to be closed. /// See [ndef](https://pub.dev/packages/ndef) for usage of [ndef.NDEFRecord]. static Future> readNDEFRecords({bool? cached}) async { - return (await readNDEFRawRecords(cached: cached)) - .map((r) => NDEFRecordConvert.fromRaw(r)) - .toList(); + return (await readNDEFRawRecords( + cached: cached, + )).map((r) => NDEFRecordConvert.fromRaw(r)).toList(); } /// Read NDEF records (in raw data, Android & iOS only). @@ -433,8 +447,9 @@ class FlutterNfcKit { /// On Android, this would cause any other open TagTechnology to be closed. /// Please use [readNDEFRecords] if you want decoded NDEF records static Future> readNDEFRawRecords({bool? cached}) async { - final String data = - await _channel.invokeMethod('readNDEF', {'cached': cached ?? false}); + final String data = await _channel.invokeMethod('readNDEF', { + 'cached': cached ?? false, + }); return (jsonDecode(data) as List) .map((object) => NDEFRawRecord.fromJson(object)) .toList(); @@ -466,10 +481,11 @@ class FlutterNfcKit { /// On iOS, use [iosAlertMessage] to indicate success or [iosErrorMessage] to indicate failure. /// If both parameters are set, [iosErrorMessage] will be used. /// On Web, set [closeWebUSB] to `true` to end the session, so that user can choose a different device in next [poll]. - static Future finish( - {String? iosAlertMessage, - String? iosErrorMessage, - bool? closeWebUSB}) async { + static Future finish({ + String? iosAlertMessage, + String? iosErrorMessage, + bool? closeWebUSB, + }) async { return await _channel.invokeMethod('finish', { 'iosErrorMessage': iosErrorMessage, 'iosAlertMessage': iosAlertMessage, @@ -499,12 +515,20 @@ class FlutterNfcKit { /// Either one of [keyA] or [keyB] must be provided. /// If both are provided, [keyA] will be used. /// Returns whether authentication succeeds. - static Future authenticateSector(int index, - {T? keyA, T? keyB}) async { - assert((keyA is String || keyA is Uint8List) || - (keyB is String || keyB is Uint8List)); - return await _channel.invokeMethod( - 'authenticateSector', {'index': index, 'keyA': keyA, 'keyB': keyB}); + static Future authenticateSector( + int index, { + T? keyA, + T? keyB, + }) async { + assert( + (keyA is String || keyA is Uint8List) || + (keyB is String || keyB is Uint8List), + ); + return await _channel.invokeMethod('authenticateSector', { + 'index': index, + 'keyA': keyA, + 'keyB': keyB, + }); } /// Read one unit of data (specified below) from: @@ -516,9 +540,11 @@ class FlutterNfcKit { /// 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 readBlock(int index, - {Iso15693RequestFlags? iso15693Flags, - bool iso15693ExtendedMode = false}) async { + static Future readBlock( + int index, { + Iso15693RequestFlags? iso15693Flags, + bool iso15693ExtendedMode = false, + }) async { var flags = iso15693Flags ?? Iso15693RequestFlags(); return await _channel.invokeMethod('readBlock', { 'index': index, @@ -535,9 +561,12 @@ class FlutterNfcKit { /// There must be a valid session when invoking. /// [index] refers to the block / page index. /// For MIFARE Classic tags, you must first authenticate against the corresponding sector. - static Future writeBlock(int index, T data, - {Iso15693RequestFlags? iso15693Flags, - bool iso15693ExtendedMode = false}) async { + static Future writeBlock( + int index, + T data, { + Iso15693RequestFlags? iso15693Flags, + bool iso15693ExtendedMode = false, + }) async { assert(data is String || data is Uint8List); var flags = iso15693Flags ?? Iso15693RequestFlags(); await _channel.invokeMethod('writeBlock', { @@ -558,5 +587,4 @@ class FlutterNfcKit { static Future readSector(int index) async { return await _channel.invokeMethod('readSector', {'index': index}); } - } From 2c930fbc5993a4d339e68495622b832039057f18 Mon Sep 17 00:00:00 2001 From: Dang Fan Date: Tue, 20 Jan 2026 23:08:18 +0800 Subject: [PATCH 3/3] Update flutter_nfc_kit.dart run `dart format` --- lib/flutter_nfc_kit.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/flutter_nfc_kit.dart b/lib/flutter_nfc_kit.dart index 51dc0cd..e7a5bf7 100644 --- a/lib/flutter_nfc_kit.dart +++ b/lib/flutter_nfc_kit.dart @@ -437,7 +437,9 @@ class FlutterNfcKit { static Future> readNDEFRecords({bool? cached}) async { return (await readNDEFRawRecords( cached: cached, - )).map((r) => NDEFRecordConvert.fromRaw(r)).toList(); + )) + .map((r) => NDEFRecordConvert.fromRaw(r)) + .toList(); } /// Read NDEF records (in raw data, Android & iOS only).