diff --git a/CHANGELOG.md b/CHANGELOG.md index 53ade72..41b4905 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -191,3 +191,11 @@ This is a release candidate for 3.6.0. Please test it and report any issues. * Related issues / PRs: #179 #184, #186, #187 * Now requiring Java 17, Gradle 8.9, MinSDKVer 26, AGP 8.7, Kotlin 2.1.0 * Add Swift package manager support for iOS plugin, bump dependencies + +## 3.6.0-rc.3 + +This is a release candidate for 3.6.0. Please test it and report any issues. + +* All 3.6.0-rc.2 changes are included +* Fix WebUSB interop on Web +* Add support for foreground polling on Android (#16, #179) diff --git a/README.md b/README.md index 75dd7b4..eceb9c5 100644 --- a/README.md +++ b/README.md @@ -63,3 +63,7 @@ Refer to the [documentation](https://pub.dev/documentation/flutter_nfc_kit/) for ### Error codes We use error codes with similar meaning as HTTP status code. Brief explanation and error cause in string (if available) will also be returned when an error occurs. + +### 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. 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 2159fff..2ed8160 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 @@ -6,6 +6,7 @@ import android.nfc.NdefMessage import android.nfc.NdefRecord import android.nfc.NfcAdapter import android.nfc.NfcAdapter.* +import android.nfc.Tag import android.nfc.tech.* import android.os.Handler import android.os.HandlerThread @@ -24,6 +25,9 @@ import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.Result +import io.flutter.plugin.common.EventChannel +import io.flutter.plugin.common.EventChannel.EventSink +import io.flutter.plugin.common.EventChannel.StreamHandler import org.json.JSONArray import org.json.JSONObject import java.io.IOException @@ -45,6 +49,16 @@ class FlutterNfcKitPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { private lateinit var nfcHandlerThread: HandlerThread private lateinit var nfcHandler: Handler + private lateinit var methodChannel: MethodChannel + private lateinit var eventChannel: EventChannel + private var eventSink: EventSink? = null + + public fun handleTag(tag: Tag) { + val result = parseTag(tag) + Handler(Looper.getMainLooper()).post { + eventSink?.success(result) + } + } private fun TagTechnology.transceive(data: ByteArray, timeout: Int?): ByteArray { if (timeout != null) { @@ -79,6 +93,151 @@ class FlutterNfcKitPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { result.error("500", "Failed to post job to NFC Handler thread.", null) } } + + private fun parseTag(tag: Tag): String { + // common fields + val type: String + val id = tag.id.toHexString() + val standard: String + // ISO 14443 Type A + var atqa = "" + var sak = "" + // ISO 14443 Type B + var protocolInfo = "" + var applicationData = "" + // ISO 7816 + var historicalBytes = "" + var hiLayerResponse = "" + // NFC-F / Felica + var manufacturer = "" + var systemCode = "" + // NFC-V + var dsfId = "" + // NDEF + var ndefAvailable = false + var ndefWritable = false + var ndefCanMakeReadOnly = false + var ndefCapacity = 0 + var ndefType = "" + + if (tag.techList.contains(NfcA::class.java.name)) { + val aTag = NfcA.get(tag) + atqa = aTag.atqa.toHexString() + sak = byteArrayOf(aTag.sak.toByte()).toHexString() + tagTechnology = aTag + when { + tag.techList.contains(IsoDep::class.java.name) -> { + standard = "ISO 14443-4 (Type A)" + type = "iso7816" + val isoDep = IsoDep.get(tag) + tagTechnology = isoDep + historicalBytes = isoDep.historicalBytes.toHexString() + } + tag.techList.contains(MifareClassic::class.java.name) -> { + standard = "ISO 14443-3 (Type A)" + type = "mifare_classic" + with(MifareClassic.get(tag)) { + tagTechnology = this + mifareInfo = MifareInfo( + this.type, + size, + MifareClassic.BLOCK_SIZE, + blockCount, + sectorCount + ) + } + } + tag.techList.contains(MifareUltralight::class.java.name) -> { + standard = "ISO 14443-3 (Type A)" + type = "mifare_ultralight" + with(MifareUltralight.get(tag)) { + tagTechnology = this + mifareInfo = MifareInfo.fromUltralight(this.type) + } + } + else -> { + standard = "ISO 14443-3 (Type A)" + type = "unknown" + } + } + } else if (tag.techList.contains(NfcB::class.java.name)) { + val bTag = NfcB.get(tag) + protocolInfo = bTag.protocolInfo.toHexString() + applicationData = bTag.applicationData.toHexString() + if (tag.techList.contains(IsoDep::class.java.name)) { + type = "iso7816" + standard = "ISO 14443-4 (Type B)" + val isoDep = IsoDep.get(tag) + tagTechnology = isoDep + hiLayerResponse = isoDep.hiLayerResponse.toHexString() + } else { + type = "unknown" + standard = "ISO 14443-3 (Type B)" + tagTechnology = bTag + } + } else if (tag.techList.contains(NfcF::class.java.name)) { + standard = "ISO 18092 (FeliCa)" + type = "iso18092" + val fTag = NfcF.get(tag) + manufacturer = fTag.manufacturer.toHexString() + systemCode = fTag.systemCode.toHexString() + tagTechnology = fTag + } else if (tag.techList.contains(NfcV::class.java.name)) { + standard = "ISO 15693" + type = "iso15693" + val vTag = NfcV.get(tag) + dsfId = vTag.dsfId.toHexString() + tagTechnology = vTag + } else { + type = "unknown" + standard = "unknown" + } + + // detect ndef + if (tag.techList.contains(Ndef::class.java.name)) { + val ndefTag = Ndef.get(tag) + ndefTechnology = ndefTag + ndefAvailable = true + ndefType = ndefTag.type + ndefWritable = ndefTag.isWritable + ndefCanMakeReadOnly = ndefTag.canMakeReadOnly() + ndefCapacity = ndefTag.maxSize + } + + val jsonResult = JSONObject(mapOf( + "type" to type, + "id" to id, + "standard" to standard, + "atqa" to atqa, + "sak" to sak, + "historicalBytes" to historicalBytes, + "protocolInfo" to protocolInfo, + "applicationData" to applicationData, + "hiLayerResponse" to hiLayerResponse, + "manufacturer" to manufacturer, + "systemCode" to systemCode, + "dsfId" to dsfId, + "ndefAvailable" to ndefAvailable, + "ndefType" to ndefType, + "ndefWritable" to ndefWritable, + "ndefCanMakeReadOnly" to ndefCanMakeReadOnly, + "ndefCapacity" to ndefCapacity, + )) + + if (mifareInfo != null) { + with(mifareInfo!!) { + jsonResult.put("mifareInfo", JSONObject(mapOf( + "type" to typeStr, + "size" to size, + "blockSize" to blockSize, + "blockCount" to blockCount, + "sectorCount" to sectorCount + ))) + } + } + + return jsonResult.toString() + } } override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { @@ -86,11 +245,26 @@ class FlutterNfcKitPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { nfcHandlerThread.start() nfcHandler = Handler(nfcHandlerThread.looper) - val channel = MethodChannel(flutterPluginBinding.binaryMessenger, "flutter_nfc_kit") - channel.setMethodCallHandler(this) + methodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "flutter_nfc_kit/method") + methodChannel.setMethodCallHandler(this) + + eventChannel = EventChannel(flutterPluginBinding.binaryMessenger, "flutter_nfc_kit/event") + eventChannel.setStreamHandler(object : EventChannel.StreamHandler { + override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { + if (events != null) { + eventSink = events + } + } + + override fun onCancel(arguments: Any?) { + // No need to do anything here + } + }) } override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + methodChannel.setMethodCallHandler(null) + eventChannel.setStreamHandler(null) nfcHandlerThread.quitSafely() } @@ -418,148 +592,8 @@ class FlutterNfcKitPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { val pollHandler = NfcAdapter.ReaderCallback { tag -> pollingTimeoutTask?.cancel() - // common fields - val type: String - val id = tag.id.toHexString() - val standard: String - // ISO 14443 Type A - var atqa = "" - var sak = "" - // ISO 14443 Type B - var protocolInfo = "" - var applicationData = "" - // ISO 7816 - var historicalBytes = "" - var hiLayerResponse = "" - // NFC-F / Felica - var manufacturer = "" - var systemCode = "" - // NFC-V - var dsfId = "" - // NDEF - var ndefAvailable = false - var ndefWritable = false - var ndefCanMakeReadOnly = false - var ndefCapacity = 0 - var ndefType = "" - - if (tag.techList.contains(NfcA::class.java.name)) { - val aTag = NfcA.get(tag) - atqa = aTag.atqa.toHexString() - sak = byteArrayOf(aTag.sak.toByte()).toHexString() - tagTechnology = aTag - when { - tag.techList.contains(IsoDep::class.java.name) -> { - standard = "ISO 14443-4 (Type A)" - type = "iso7816" - val isoDep = IsoDep.get(tag) - tagTechnology = isoDep - historicalBytes = isoDep.historicalBytes.toHexString() - } - tag.techList.contains(MifareClassic::class.java.name) -> { - standard = "ISO 14443-3 (Type A)" - type = "mifare_classic" - with(MifareClassic.get(tag)) { - tagTechnology = this - mifareInfo = MifareInfo( - this.type, - size, - MifareClassic.BLOCK_SIZE, - blockCount, - sectorCount - ) - } - } - tag.techList.contains(MifareUltralight::class.java.name) -> { - standard = "ISO 14443-3 (Type A)" - type = "mifare_ultralight" - with(MifareUltralight.get(tag)) { - tagTechnology = this - mifareInfo = MifareInfo.fromUltralight(this.type) - } - } - else -> { - standard = "ISO 14443-3 (Type A)" - type = "unknown" - } - } - } else if (tag.techList.contains(NfcB::class.java.name)) { - val bTag = NfcB.get(tag) - protocolInfo = bTag.protocolInfo.toHexString() - applicationData = bTag.applicationData.toHexString() - if (tag.techList.contains(IsoDep::class.java.name)) { - type = "iso7816" - standard = "ISO 14443-4 (Type B)" - val isoDep = IsoDep.get(tag) - tagTechnology = isoDep - hiLayerResponse = isoDep.hiLayerResponse.toHexString() - } else { - type = "unknown" - standard = "ISO 14443-3 (Type B)" - tagTechnology = bTag - } - } else if (tag.techList.contains(NfcF::class.java.name)) { - standard = "ISO 18092 (FeliCa)" - type = "iso18092" - val fTag = NfcF.get(tag) - manufacturer = fTag.manufacturer.toHexString() - systemCode = fTag.systemCode.toHexString() - tagTechnology = fTag - } else if (tag.techList.contains(NfcV::class.java.name)) { - standard = "ISO 15693" - type = "iso15693" - val vTag = NfcV.get(tag) - dsfId = vTag.dsfId.toHexString() - tagTechnology = vTag - } else { - type = "unknown" - standard = "unknown" - } - - // detect ndef - if (tag.techList.contains(Ndef::class.java.name)) { - val ndefTag = Ndef.get(tag) - ndefTechnology = ndefTag - ndefAvailable = true - ndefType = ndefTag.type - ndefWritable = ndefTag.isWritable - ndefCanMakeReadOnly = ndefTag.canMakeReadOnly() - ndefCapacity = ndefTag.maxSize - } - - val jsonResult = JSONObject(mapOf( - "type" to type, - "id" to id, - "standard" to standard, - "atqa" to atqa, - "sak" to sak, - "historicalBytes" to historicalBytes, - "protocolInfo" to protocolInfo, - "applicationData" to applicationData, - "hiLayerResponse" to hiLayerResponse, - "manufacturer" to manufacturer, - "systemCode" to systemCode, - "dsfId" to dsfId, - "ndefAvailable" to ndefAvailable, - "ndefType" to ndefType, - "ndefWritable" to ndefWritable, - "ndefCanMakeReadOnly" to ndefCanMakeReadOnly, - "ndefCapacity" to ndefCapacity, - )) - - if (mifareInfo != null) { - with(mifareInfo!!) { - jsonResult.put("mifareInfo", JSONObject(mapOf( - "type" to typeStr, - "size" to size, - "blockSize" to blockSize, - "blockCount" to blockCount, - "sectorCount" to sectorCount - ))) - } - } - - result.success(jsonResult.toString()) + val jsonResult = parseTag(tag) + result.success(jsonResult) } nfcAdapter.enableReaderMode(activity.get(), pollHandler, technologies, null) diff --git a/example/.gitignore b/example/.gitignore index ae1f183..c42f173 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -31,7 +31,6 @@ /build/ # Web related -lib/generated_plugin_registrant.dart # Exceptions to above rules. !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index 5d955e7..765105b 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -5,7 +5,7 @@ android:name="${applicationName}" android:icon="@mipmap/ic_launcher"> . Please refer to the [GitHub repository](https://github.com/nfcim/flutter_nfc_kit). diff --git a/example/lib/main.dart b/example/lib/main.dart index 8e31891..4c8156a 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -52,6 +52,12 @@ class _MyAppState extends State with SingleTickerProviderStateMixin { initPlatformState(); _tabController = TabController(length: 2, vsync: this); _records = []; + FlutterNfcKit.tagStream.listen((tag) { + setState(() { + _tag = tag; + print(_tag); + }); + }); } // Platform messages are asynchronous, so we initialize in an async method. diff --git a/example/pubspec.lock b/example/pubspec.lock index 29e68ee..2f920ab 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -92,7 +92,7 @@ packages: path: ".." relative: true source: path - version: "3.6.0-rc.2" + version: "3.6.0-rc.3" flutter_test: dependency: "direct dev" description: flutter diff --git a/lib/flutter_nfc_kit.dart b/lib/flutter_nfc_kit.dart index 303f6bc..c051837 100644 --- a/lib/flutter_nfc_kit.dart +++ b/lib/flutter_nfc_kit.dart @@ -275,7 +275,21 @@ class FlutterNfcKit { /// Default timeout for [poll] (in milliseconds) static const int POLL_TIMEOUT = 20 * 1000; - static const MethodChannel _channel = MethodChannel('flutter_nfc_kit'); + static const MethodChannel _channel = MethodChannel('flutter_nfc_kit/method'); + + static const EventChannel _tagEventChannel = + EventChannel('flutter_nfc_kit/event'); + + /// Stream of NFC tag events. Each event is a [NFCTag] object. + /// + /// This is only supported on Android. + /// On other platforms, this stream will always be empty. + static Stream get tagStream { + return _tagEventChannel.receiveBroadcastStream().map((dynamic event) { + final Map json = jsonDecode(event as String); + return NFCTag.fromJson(json); + }); + } /// get the availablility of NFC reader on this device static Future get nfcAvailability async { @@ -338,10 +352,11 @@ class FlutterNfcKit { return NFCTag.fromJson(jsonDecode(data)); } - /// Works only on iOS - /// Calls NFCTagReaderSession.restartPolling() - /// Call this if you have received "Tag connection lost" exception - /// This will allow to reconnect to tag without closing system popup + /// Works only on iOS. + /// + /// Calls `NFCTagReaderSession.restartPolling()`. + /// Call this if you have received "Tag connection lost" exception. + /// This will allow to reconnect to tag without closing system popup. static Future iosRestartPolling() async => await _channel.invokeMethod("restartPolling"); @@ -409,7 +424,7 @@ class FlutterNfcKit { return await _channel.invokeMethod('writeNDEF', {'data': data}); } - /// Finish current session. + /// Finish current session in polling mode. /// /// You must invoke it before start a new session. /// diff --git a/lib/flutter_nfc_kit_web.dart b/lib/flutter_nfc_kit_web.dart index 170b103..1fbc876 100644 --- a/lib/flutter_nfc_kit_web.dart +++ b/lib/flutter_nfc_kit_web.dart @@ -20,7 +20,7 @@ import 'package:flutter_nfc_kit/webusb_interop.dart'; class FlutterNfcKitWeb { static void registerWith(Registrar registrar) { final MethodChannel channel = MethodChannel( - 'flutter_nfc_kit', + 'flutter_nfc_kit/method', const StandardMethodCodec(), registrar, ); diff --git a/lib/webusb_interop.dart b/lib/webusb_interop.dart index 76b0e40..e0e5cfa 100644 --- a/lib/webusb_interop.dart +++ b/lib/webusb_interop.dart @@ -21,27 +21,27 @@ final log = Logger('FlutterNFCKit:WebUSB'); const int USB_CLASS_CODE_VENDOR_SPECIFIC = 0xFF; @JS('navigator.usb') -class _USB { - external static dynamic requestDevice(_USBDeviceRequestOptions options); - // ignore: unused_field - external static Function ondisconnect; +extension type _USB._(JSObject _) implements JSObject { + external static JSObject requestDevice(_USBDeviceRequestOptions options); + external static set ondisconnect(JSFunction value); } @JS() @anonymous -class _USBDeviceRequestOptions { - external factory _USBDeviceRequestOptions({List<_USBDeviceFilter> filters}); +extension type _USBDeviceRequestOptions._(JSObject _) implements JSObject { + external factory _USBDeviceRequestOptions( + {JSArray<_USBDeviceFilter> filters}); } @JS() @anonymous -class _USBDeviceFilter { +extension type _USBDeviceFilter._(JSObject _) implements JSObject { external factory _USBDeviceFilter({int classCode}); } @JS() @anonymous -class _USBControlTransferParameters { +extension type _USBControlTransferParameters._(JSObject _) implements JSObject { external factory _USBControlTransferParameters( {String requestType, String recipient, @@ -61,7 +61,7 @@ class WebUSB { return _device != null && getProperty(_device, 'opened'); } - static void _onDisconnect(event) { + static void _onDisconnect() { _device = null; log.info('device is disconnected from WebUSB API'); } @@ -72,9 +72,9 @@ class WebUSB { static Future poll(int timeout, bool probeMagic) async { // request WebUSB device with custom classcode if (!_deviceAvailable()) { - var devicePromise = _USB.requestDevice(_USBDeviceRequestOptions(filters: [ - _USBDeviceFilter(classCode: USB_CLASS_CODE_VENDOR_SPECIFIC) - ])); + var devicePromise = _USB.requestDevice(_USBDeviceRequestOptions( + filters: [_USBDeviceFilter(classCode: USB_CLASS_CODE_VENDOR_SPECIFIC)] + .toJS)); dynamic device = await promiseToFuture(devicePromise); try { await promiseToFuture(callMethod(device, 'open', List.empty())) @@ -82,7 +82,7 @@ class WebUSB { promiseToFuture(callMethod(device, 'claimInterface', [1]))) .timeout(Duration(milliseconds: timeout)); _device = device; - _USB.ondisconnect = allowInterop(_onDisconnect); + _USB.ondisconnect = _onDisconnect.toJS; log.info("WebUSB device opened", _device); } on TimeoutException catch (_) { log.severe("Polling tag timeout"); diff --git a/pubspec.yaml b/pubspec.yaml index fc69c0f..5919544 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_nfc_kit description: Provide NFC functionality on Android, iOS & Web, including reading metadata, read & write NDEF records, and transceive layer 3 & 4 data with NFC tags / cards -version: 3.6.0-rc.2 +version: 3.6.0-rc.3 homepage: "https://github.com/nfcim/flutter_nfc_kit" environment: