diff --git a/.github/workflows/example-app.yml b/.github/workflows/example-app.yml deleted file mode 100644 index 9990d56..0000000 --- a/.github/workflows/example-app.yml +++ /dev/null @@ -1,36 +0,0 @@ -# This is a basic workflow to help you get started with Actions - -name: Build Example App - -# Controls when the action will run. Triggers the workflow on push or pull request -# events but only for the master branch -on: [push, pull_request] - -# A workflow run is made up of one or more jobs that can run sequentially or in parallel -jobs: - # This workflow contains a single job called "build" - build: - # The type of runner that the job will run on - runs-on: ubuntu-latest - if: "!contains(github.event.head_commit.message, 'ci skip')" - - # Steps represent a sequence of tasks that will be executed as part of the job - steps: - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v3 - - uses: actions/setup-java@v2 - with: - distribution: 'temurin' - java-version: '17' - - uses: subosito/flutter-action@v1 - with: - channel: 'stable' # or: 'dev' or 'beta' - - run: flutter pub get - working-directory: example/ - #- run: flutter test - - run: flutter build apk --debug --verbose - working-directory: example/ - - uses: actions/upload-artifact@v3 - with: - name: example-apk-debug - path: example/build/app/outputs/apk/debug/app-debug.apk diff --git a/CHANGELOG.md b/CHANGELOG.md index 87465c5..1b8fb99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -132,3 +132,64 @@ ## 3.3.3 * Fix build script of Android plugin and remove AGP version requirement (#110) + +## 3.4.0 + +* Add support for reading / write MIFARE Classic / Ultralight tags on Android (merged #82, partially fixes #82) +* Add support for reading / write ISO 15693 tags on iOS (merged #117, partially fixes #68) +* Fix compiling issues (#123) +* Other minor fixes (#114, #115) + +## 3.4.1 + +**This version is *deprecated* due to a bug in Mifare tag handling. Please upgrade to 3.4.2.** + +* Fix & split examples to example/ dir +* Publish examples to pub.dev +* Support transceiving of raw ISO15693 commands on iOS + +## 3.4.2 + +* Fix polling error on Mifare tags (#126, #128, #129, #133) + +## 3.5.0 + +* Some FeliCa improvements by @shiwano: + * Fix missing `id` field in FeliCa card reading on iOS (#140) + * Set the IDm to the `id` and the PMm to the `manufacturer` on iOS (#140) +* Add `iosRestartPolling` method by @rostopira (#151) +* Fix type assertion in `authenticateSector` (fix #148) +* Refine exception handling in Android plugin (fix #91 and #149) +* Bump multiple dependencies: + * Android plugin / example app: Java 17, AGP 7.4.2, Kotlin 1.9.23, minSdkVersion 26 (fix #127, #144, #145) + * `js` library: 0.7.1 + +## 3.5.1 + +* Fix multiple issues related to `authenticateSector` (#159): + * Fix type checking assertions of arguments + * Add missing call to `connect` in Android plugin +* Add instruction on resolving `js` dependency conflict in README + +## 3.5.2 + +* Some MiFare Classic fixes by @knthm: + * allow authentication of sector 0 (#157) + * fix data type check in `writeBlock` (#161) + +## 3.6.0-rc.6 + +This is a release candidate for 3.6.0. Please test it and report any issues. + +* Requires Dart 3.6+ and Flutter 3.24+ +* Remove annoying dependency on `js` library, replace with `dart:js_interop` +* Remove dependency on `dart:io` +* Contributions on Android plugin from @knthm: + * Dedicated handler thread for IO operations (#167) + * More elegant exception handling (#169) +* Bump tool versions & dependencies of Android plugin and example app: + * 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 +* Fix WebUSB interop on Web, add onDisconnect callback +* Add support for foreground polling on Android (#16, #179) diff --git a/README.md b/README.md index 1a744e3..eceb9c5 100644 --- a/README.md +++ b/README.md @@ -11,24 +11,37 @@ This plugin's functionalities include: * ISO 14443 Type A & Type B (NFC-A / NFC-B / MIFARE Classic / MIFARE Plus / MIFARE Ultralight / MIFARE DESFire) * ISO 18092 (NFC-F / FeliCa) * ISO 15963 (NFC-V) -* transceive commands with tags / cards complying with: +* R/W block / page / sector level data of tags complying with: + * MIFARE Classic / Ultralight (Android only) + * ISO 15693 (iOS only) +* transceive raw commands with tags / cards complying with: * ISO 7816 Smart Cards (layer 4, in APDUs) * other device-supported technologies (layer 3, in raw commands, see documentation for platform-specific supportability) -Note that due to API limitations not all operations are supported on both platforms. +Note that due to API limitations, not all operations are supported on all platforms. +**You are welcome to submit PRs to add support for any standard-specific operations.** This library uses [ndef](https://pub.dev/packages/ndef) for NDEF record encoding & decoding. ## Setup -Thank [nfc_manager](https://pub.dev/packages/nfc_manager) plugin for these instructions. - ### Android +We have the following minimum version requirements for Android plugin: + +* Java 17 +* Gradle 8.9 +* Android SDK 26 (you must set corresponding `jvmTarget` in you app's `build.gradle`) +* Android Gradle Plugin 8.7 + +To use this plugin on Android, you also need to: + * Add [android.permission.NFC](https://developer.android.com/reference/android/Manifest.permission.html#NFC) to your `AndroidManifest.xml`. ### iOS +This plugin now supports Swift package manager, and requires iOS 13+. + * Add [Near Field Communication Tag Reader Session Formats Entitlements](https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_developer_nfc_readersession_formats) to your entitlements. * Add [NFCReaderUsageDescription](https://developer.apple.com/documentation/bundleresources/information_property_list/nfcreaderusagedescription) to your `Info.plist`. * Add [com.apple.developer.nfc.readersession.felica.systemcodes](https://developer.apple.com/documentation/bundleresources/information_property_list/systemcodes) and [com.apple.developer.nfc.readersession.iso7816.select-identifiers](https://developer.apple.com/documentation/bundleresources/information_property_list/select-identifiers) to your `Info.plist` as needed. WARNING: for iOS 14.5 and earlier versions, you **MUST** add them before invoking `poll` with `readIso18092` or `readIso15693` enabled, or your NFC **WILL BE TOTALLY UNAVAILABLE BEFORE REBOOT** due to a [CoreNFC bug](https://github.com/nfcim/flutter_nfc_kit/issues/23). @@ -43,64 +56,14 @@ Make sure you understand the statement above and the protocol before using this ## Usage -Simple example: - -```dart -import 'package:flutter_nfc_kit/flutter_nfc_kit.dart'; -import 'package:ndef/ndef.dart' as ndef; - -var availability = await FlutterNfcKit.nfcAvailability; -if (availability != NFCAvailability.available) { - // oh-no -} - -// timeout only works on Android, while the latter two messages are only for iOS -var tag = await FlutterNfcKit.poll(timeout: Duration(seconds: 10), - iosMultipleTagMessage: "Multiple tags found!", iosAlertMessage: "Scan your tag"); - -print(jsonEncode(tag)); -if (tag.type == NFCTagType.iso7816) { - var result = await FlutterNfcKit.transceive("00B0950000", Duration(seconds: 5)); // timeout is still Android-only, persist until next change - print(result); -} -// iOS only: set alert message on-the-fly -// this will persist until finish() -await FlutterNfcKit.setIosAlertMessage("hi there!"); - -// read NDEF records if available -if (tag.ndefAvailable){ - /// decoded NDEF records (see [ndef.NDEFRecord] for details) - /// `UriRecord: id=(empty) typeNameFormat=TypeNameFormat.nfcWellKnown type=U uri=https://github.com/nfcim/ndef` - for (var record in await FlutterNfcKit.readNDEFRecords(cached: false)) { - print(record.toString()); - } - /// raw NDEF records (data in hex string) - /// `{identifier: "", payload: "00010203", type: "0001", typeNameFormat: "nfcWellKnown"}` - for (var record in await FlutterNfcKit.readNDEFRawRecords(cached: false)) { - print(jsonEncode(record).toString()); - } -} - -// write NDEF records if applicable -if (tag.ndefWritable) { - // decoded NDEF records - await FlutterNfcKit.writeNDEFRecords([new ndef.UriRecord.fromUriString("https://github.com/nfcim/flutter_nfc_kit")]); - // raw NDEF records - await FlutterNfcKit.writeNDEFRawRecords([new NDEFRawRecord("00", "0001", "0002", "0003", ndef.TypeNameFormat.unknown)]); -} - -// Call finish() only once -await FlutterNfcKit.finish(); -// iOS only: show alert/error message on finish -await FlutterNfcKit.finish(iosAlertMessage: "Success"); -// or -await FlutterNfcKit.finish(iosErrorMessage: "Failed"); -``` - -A more complicated example can be seen in `example` dir. +We provide [simple code example](example/example.md) and a [example application](example/lib). Refer to the [documentation](https://pub.dev/documentation/flutter_nfc_kit/) for more information. ### 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/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..24c3773 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,5 @@ +include: package:lints/recommended.yaml + +linter: + rules: + constant_identifier_names: false diff --git a/android/build.gradle b/android/build.gradle index 4a23601..374d28d 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -15,20 +15,26 @@ group 'im.nfc.flutter_nfc_kit' android { - namespace 'im.nfc.flutter_nfc_kit' + if (project.android.hasProperty("namespace")) { + namespace 'im.nfc.flutter_nfc_kit' + } - compileSdkVersion 33 + compileSdk 35 compileOptions { - sourceCompatibility JavaVersion.VERSION_11 - targetCompatibility JavaVersion.VERSION_11 + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 } + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8 + } + sourceSets { main.java.srcDirs += 'src/main/kotlin' } defaultConfig { - minSdkVersion 19 + minSdkVersion 26 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } lintOptions { @@ -37,7 +43,4 @@ android { } dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$KotlinVersion" - implementation "org.jetbrains.kotlin:kotlin-reflect:$KotlinVersion" - implementation 'androidx.core:core-ktx:1.10.1' } diff --git a/android/gradle.properties b/android/gradle.properties index 39a8970..48dd80c 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,5 +1,4 @@ org.gradle.jvmargs=-Xmx1536M -android.useAndroidX=true android.enableJetifier=true -AGPVersion=7.0.4 -KotlinVersion=1.9.10 \ No newline at end of file +AGPVersion=8.7.3 +KotlinVersion=2.1.0 diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 4a16027..543c293 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-7.6.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-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 e93ae78..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,8 +6,10 @@ 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 import android.os.Looper import im.nfc.flutter_nfc_kit.ByteUtils.canonicalizeData import im.nfc.flutter_nfc_kit.ByteUtils.hexToBytes @@ -23,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 @@ -30,7 +35,6 @@ import java.lang.ref.WeakReference import java.lang.reflect.InvocationTargetException import java.util.* import kotlin.concurrent.schedule -import kotlin.concurrent.thread class FlutterNfcKitPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { @@ -43,6 +47,19 @@ class FlutterNfcKitPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { private var ndefTechnology: Ndef? = null private var mifareInfo: MifareInfo? = null + 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) { try { @@ -53,11 +70,202 @@ class FlutterNfcKitPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { val transceiveMethod = this.javaClass.getMethod("transceive", ByteArray::class.java) return transceiveMethod.invoke(this, data) as ByteArray } + + private fun runOnNfcThread(result: Result, desc: String, fn: () -> Unit) { + val handledFn = Runnable { + try { + fn() + } catch (ex: Exception) { + Log.e(TAG, "$desc error", ex) + val excMessage = ex.localizedMessage + when (ex) { + is IOException -> result?.error("500", "Communication error", excMessage) + is SecurityException -> result?.error("503", "Tag already removed", excMessage) + is FormatException -> result?.error("400", "NDEF format error", excMessage) + is InvocationTargetException -> result?.error("500", "Communication error", excMessage) + is IllegalArgumentException -> result?.error("400", "Command format error", excMessage) + is NoSuchMethodException -> result?.error("405", "Transceive not supported for this type of card", excMessage) + else -> result?.error("500", "Unhandled error", excMessage) + } + } + } + if (!nfcHandler.post(handledFn)) { + 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) { - val channel = MethodChannel(flutterPluginBinding.binaryMessenger, "flutter_nfc_kit") - channel.setMethodCallHandler(this) + nfcHandlerThread = HandlerThread("NfcHandlerThread") + nfcHandlerThread.start() + nfcHandler = Handler(nfcHandlerThread.looper) + + 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() } override fun onMethodCall(call: MethodCall, result: Result) { @@ -113,27 +321,21 @@ class FlutterNfcKitPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { val timeout = call.argument("timeout")!! // technology and option bits are set in Dart code val technologies = call.argument("technologies")!! - thread { + runOnNfcThread(result, "Poll") { pollTag(nfcAdapter, result, timeout, technologies) } } "finish" -> { pollingTimeoutTask?.cancel() - thread { - try { - val tagTech = tagTechnology - if (tagTech != null && tagTech.isConnected) { - tagTech.close() - } - val ndefTech = ndefTechnology - if (ndefTech != null && ndefTech.isConnected) { - ndefTech.close() - } - } catch (ex: SecurityException) { - Log.e(TAG, "Tag already removed", ex) - } catch (ex: IOException) { - Log.e(TAG, "Close tag error", ex) + runOnNfcThread(result, "Close tag") { + val tagTech = tagTechnology + if (tagTech != null && tagTech.isConnected) { + tagTech.close() + } + val ndefTech = ndefTechnology + if (ndefTech != null && ndefTech.isConnected) { + ndefTech.close() } if (activity.get() != null) { nfcAdapter.disableReaderMode(activity.get()) @@ -155,30 +357,13 @@ class FlutterNfcKitPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { } val (sendingBytes, sendingHex) = canonicalizeData(data) - thread { - try { - switchTechnology(tagTech, ndefTechnology) - val timeout = call.argument("timeout") - val resp = tagTech.transceive(sendingBytes, timeout) - when (data) { - is String -> result.success(resp.toHexString()) - else -> result.success(resp) - } - } catch (ex: SecurityException) { - Log.e(TAG, "Transceive Error: $sendingHex", ex) - result.error("503", "Tag already removed", ex.localizedMessage) - } catch (ex: IOException) { - Log.e(TAG, "Transceive Error: $sendingHex", ex) - result.error("500", "Communication error", ex.localizedMessage) - } catch (ex: InvocationTargetException) { - Log.e(TAG, "Transceive Error: $sendingHex", ex.cause ?: ex) - result.error("500", "Communication error", ex.cause?.localizedMessage) - } catch (ex: IllegalArgumentException) { - Log.e(TAG, "Command Error: $sendingHex", ex) - result.error("400", "Command format error", ex.localizedMessage) - } catch (ex: NoSuchMethodException) { - Log.e(TAG, "Transceive not supported: $sendingHex", ex) - result.error("405", "Transceive not supported for this type of card", null) + runOnNfcThread(result, "Transceive: $sendingHex") { + switchTechnology(tagTech, ndefTechnology) + val timeout = call.argument("timeout") + val resp = tagTech.transceive(sendingBytes, timeout) + when (data) { + is String -> result.success(resp.toHexString()) + else -> result.success(resp) } } } @@ -187,45 +372,34 @@ class FlutterNfcKitPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { "readNDEF" -> { if (!ensureNDEF()) return val ndef = ndefTechnology!! - thread { - try { - switchTechnology(ndef, tagTechnology) - // read NDEF message - val message: NdefMessage? = if (call.argument("cached")!!) { - ndef.cachedNdefMessage - } else { - ndef.ndefMessage - } - val parsedMessages = mutableListOf>() - if (message != null) { - for (record in message.records) { - parsedMessages.add(mapOf( - "identifier" to record.id.toHexString(), - "payload" to record.payload.toHexString(), - "type" to record.type.toHexString(), - "typeNameFormat" to when (record.tnf) { - NdefRecord.TNF_ABSOLUTE_URI -> "absoluteURI" - NdefRecord.TNF_EMPTY -> "empty" - NdefRecord.TNF_EXTERNAL_TYPE -> "nfcExternal" - NdefRecord.TNF_WELL_KNOWN -> "nfcWellKnown" - NdefRecord.TNF_MIME_MEDIA -> "media" - NdefRecord.TNF_UNCHANGED -> "unchanged" - else -> "unknown" - } - )) - } + runOnNfcThread(result, "Read NDEF") { + switchTechnology(ndef, tagTechnology) + // read NDEF message + val message: NdefMessage? = if (call.argument("cached")!!) { + ndef.cachedNdefMessage + } else { + ndef.ndefMessage + } + val parsedMessages = mutableListOf>() + if (message != null) { + for (record in message.records) { + parsedMessages.add(mapOf( + "identifier" to record.id.toHexString(), + "payload" to record.payload.toHexString(), + "type" to record.type.toHexString(), + "typeNameFormat" to when (record.tnf) { + NdefRecord.TNF_ABSOLUTE_URI -> "absoluteURI" + NdefRecord.TNF_EMPTY -> "empty" + NdefRecord.TNF_EXTERNAL_TYPE -> "nfcExternal" + NdefRecord.TNF_WELL_KNOWN -> "nfcWellKnown" + NdefRecord.TNF_MIME_MEDIA -> "media" + NdefRecord.TNF_UNCHANGED -> "unchanged" + else -> "unknown" + } + )) } - result.success(JSONArray(parsedMessages).toString()) - } catch (ex: SecurityException) { - Log.e(TAG, "Read NDEF Error", ex) - result.error("503", "Tag already removed", ex.localizedMessage) - } catch (ex: IOException) { - Log.e(TAG, "Read NDEF Error", ex) - result.error("500", "Communication error", ex.localizedMessage) - } catch (ex: FormatException) { - Log.e(TAG, "NDEF Format Error", ex) - result.error("400", "NDEF format error", ex.localizedMessage) } + result.success(JSONArray(parsedMessages).toString()) } } @@ -236,43 +410,32 @@ class FlutterNfcKitPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { result.error("405", "Tag not writable", null) return } - thread { - try { - switchTechnology(ndef, tagTechnology) - // generate NDEF message - val jsonString = call.argument("data")!! - val recordData = JSONArray(jsonString) - val records = Array(recordData.length(), init = { i: Int -> - val record: JSONObject = recordData.get(i) as JSONObject - NdefRecord( - when (record.getString("typeNameFormat")) { - "absoluteURI" -> NdefRecord.TNF_ABSOLUTE_URI - "empty" -> NdefRecord.TNF_EMPTY - "nfcExternal" -> NdefRecord.TNF_EXTERNAL_TYPE - "nfcWellKnown" -> NdefRecord.TNF_WELL_KNOWN - "media" -> NdefRecord.TNF_MIME_MEDIA - "unchanged" -> NdefRecord.TNF_UNCHANGED - else -> NdefRecord.TNF_UNKNOWN - }, - record.getString("type").hexToBytes(), - record.getString("identifier").hexToBytes(), - record.getString("payload").hexToBytes() - ) - }) - // write NDEF message - val message = NdefMessage(records) - ndef.writeNdefMessage(message) - result.success("") - } catch (ex: SecurityException) { - Log.e(TAG, "Write NDEF Error", ex) - result.error("503", "Tag already removed", ex.localizedMessage) - } catch (ex: IOException) { - Log.e(TAG, "Write NDEF Error", ex) - result.error("500", "Communication error", ex.localizedMessage) - } catch (ex: FormatException) { - Log.e(TAG, "NDEF Format Error", ex) - result.error("400", "NDEF format error", ex.localizedMessage) - } + runOnNfcThread(result, "Write NDEF") { + switchTechnology(ndef, tagTechnology) + // generate NDEF message + val jsonString = call.argument("data")!! + val recordData = JSONArray(jsonString) + val records = Array(recordData.length(), init = { i: Int -> + val record: JSONObject = recordData.get(i) as JSONObject + NdefRecord( + when (record.getString("typeNameFormat")) { + "absoluteURI" -> NdefRecord.TNF_ABSOLUTE_URI + "empty" -> NdefRecord.TNF_EMPTY + "nfcExternal" -> NdefRecord.TNF_EXTERNAL_TYPE + "nfcWellKnown" -> NdefRecord.TNF_WELL_KNOWN + "media" -> NdefRecord.TNF_MIME_MEDIA + "unchanged" -> NdefRecord.TNF_UNCHANGED + else -> NdefRecord.TNF_UNKNOWN + }, + record.getString("type").hexToBytes(), + record.getString("identifier").hexToBytes(), + record.getString("payload").hexToBytes() + ) + }) + // write NDEF message + val message = NdefMessage(records) + ndef.writeNdefMessage(message) + result.success("") } } @@ -283,20 +446,12 @@ class FlutterNfcKitPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { result.error("405", "Tag not writable", null) return } - thread { - try { - switchTechnology(ndef, tagTechnology) - if (ndef.makeReadOnly()) { - result.success("") - } else { - result.error("500", "Failed to lock NDEF tag", null) - } - } catch (ex: SecurityException) { - Log.e(TAG, "Lock NDEF Error", ex) - result.error("503", "Tag already removed", ex.localizedMessage) - } catch (ex: IOException) { - Log.e(TAG, "Lock NDEF Error", ex) - result.error("500", "Communication error", ex.localizedMessage) + runOnNfcThread(result, "Lock NDEF") { + switchTechnology(ndef, tagTechnology) + if (ndef.makeReadOnly()) { + result.success("") + } else { + result.error("500", "Failed to lock NDEF tag", null) } } } @@ -309,30 +464,27 @@ class FlutterNfcKitPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { return } val index = call.argument("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 } val keyA = call.argument("keyA") val keyB = call.argument("keyB") - thread { - try { - val tag = tagTech as MifareClassic - // key A takes precedence if present - val success = if (keyA != null) { - val (key, _) = canonicalizeData(keyA) - tag.authenticateSectorWithKeyA(index, key) - } else if (keyB != null) { - val (key, _) = canonicalizeData(keyB) - tag.authenticateSectorWithKeyB(index, key) - } else { - result.error("400", "No keys provided", null) - return@thread - } - result.success(success) - } catch (ex: IOException) { - Log.e(TAG, "Authenticate block error", ex) - result.error("500", "Authentication error", ex.localizedMessage) + runOnNfcThread(result, "Authenticate sector") { + val tag = tagTech as MifareClassic + switchTechnology(tagTech, ndefTechnology) + // key A takes precedence if present + if (keyA != null) { + val (key, _) = canonicalizeData(keyA) + val authStatus = tag.authenticateSectorWithKeyA(index, key) + result.success(authStatus) + } else if (keyB != null) { + val (key, _) = canonicalizeData(keyB) + val authStatus = tag.authenticateSectorWithKeyB(index, key) + result.success(authStatus) + } else { + result.error("400", "No keys provided", null) } } } @@ -349,14 +501,9 @@ class FlutterNfcKitPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { result.error("400", "Invalid block/page index $index, should be in (0, $maxBlock)", null) return } - thread { - try { - switchTechnology(tagTech, ndefTechnology) - tagTech.readBlock(index, result) - } catch (ex: IOException) { - Log.e(TAG, "Read block error", ex) - result.error("500", "Communication error", ex.localizedMessage) - } + runOnNfcThread(result, "Read block") { + switchTechnology(tagTech, ndefTechnology) + tagTech.readBlock(index, result) } } @@ -372,14 +519,10 @@ class FlutterNfcKitPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { result.error("400", "Invalid sector index $index, should be in (0, $maxSector)", null) return } - thread { - try { - val tag = tagTech as MifareClassic - switchTechnology(tagTech, ndefTechnology) - result.success(tag.readSector(index)) - } catch (ex: IOException) { - Log.e(TAG, "Read sector error", ex) - result.error("500", "Communication error", ex.localizedMessage) } + runOnNfcThread(result, "Read sector") { + val tag = tagTech as MifareClassic + switchTechnology(tagTech, ndefTechnology) + result.success(tag.readSector(index)) } } @@ -405,23 +548,17 @@ class FlutterNfcKitPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { result.error("400", "Invalid data size ${bytes.size}, should be ${mifareInfo!!.blockSize}", null) return } - thread { - try { - switchTechnology(tagTech, ndefTechnology) - tagTech.writeBlock(index, bytes, result) - } catch (ex: IOException) { - Log.e(TAG, "Read block error", ex) - result.error("500", "Communication error", ex.localizedMessage) - } + runOnNfcThread(result, "Write block") { + switchTechnology(tagTech, ndefTechnology) + tagTech.writeBlock(index, bytes, result) } } + // do nothing, just for compatibility + "setIosAlertMessage" -> { result.success("") } } } - - override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {} - override fun onAttachedToActivity(binding: ActivityPluginBinding) { activity = WeakReference(binding.activity) } @@ -441,159 +578,25 @@ class FlutterNfcKitPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { private fun pollTag(nfcAdapter: NfcAdapter, result: Result, timeout: Int, technologies: Int) { pollingTimeoutTask = Timer().schedule(timeout.toLong()) { - if (activity.get() != null) { - nfcAdapter.disableReaderMode(activity.get()) + try { + if (activity.get() != null) { + + nfcAdapter.disableReaderMode(activity.get()) + } + } catch (ex: Exception) { + Log.w(TAG, "Cannot disable reader mode", ex) } result.error("408", "Polling tag timeout", null) } - nfcAdapter.enableReaderMode(activity.get(), { tag -> + 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", 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) + } - }, technologies, null) + nfcAdapter.enableReaderMode(activity.get(), pollHandler, technologies, null) } private class MethodResultWrapper(result: Result) : Result { 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/.pubignore b/example/.pubignore index 1d085ca..09f451b 100644 --- a/example/.pubignore +++ b/example/.pubignore @@ -1 +1,3 @@ -** +android/ +ios/ +web/ diff --git a/example/README.md b/example/README.md deleted file mode 100644 index 06036fc..0000000 --- a/example/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Example of flutter_nfc_kit - -We provide both Android and iOS examples in this project. diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 9933fee..35f4d98 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -1,7 +1,10 @@ plugins { - id 'com.android.application' + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" } + def localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { @@ -10,11 +13,6 @@ if (localPropertiesFile.exists()) { } } -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { flutterVersionCode = '1' @@ -25,17 +23,19 @@ if (flutterVersionName == null) { flutterVersionName = '1.0' } -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - android { namespace 'im.nfc.flutter_nfc_kit.example' - compileSdkVersion 33 + compileSdk 35 compileOptions { - sourceCompatibility JavaVersion.VERSION_11 - targetCompatibility JavaVersion.VERSION_11 + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8 } sourceSets { @@ -45,9 +45,8 @@ android { defaultConfig { applicationId "im.nfc.flutter_nfc_kit_example" - minSdkVersion 19 - targetSdkVersion 33 - compileSdkVersion 33 + minSdkVersion 26 + targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -69,5 +68,4 @@ flutter { } dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$KotlinVersion" } 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"> properties.load(reader) } +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "${AGPVersion}" apply false + id "org.jetbrains.kotlin.android" version "${KotlinVersion}" apply false +} -def flutterSdkPath = properties.getProperty("flutter.sdk") -assert flutterSdkPath != null, "flutter.sdk not set in local.properties" -apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" +include ":app" diff --git a/example/example.md b/example/example.md new file mode 100644 index 0000000..59415bc --- /dev/null +++ b/example/example.md @@ -0,0 +1,142 @@ +# Example of flutter_nfc_kit + +## Polling + +This is the default operation mode and is supported on all platforms. +We recommend using this method to read NFC tags to ensure the consistency of cross-platform interactions. + + +```dart +import 'package:flutter_nfc_kit/flutter_nfc_kit.dart'; +import 'package:ndef/ndef.dart' as ndef; + +var availability = await FlutterNfcKit.nfcAvailability; +if (availability != NFCAvailability.available) { + // oh-no +} + +// timeout only works on Android, while the latter two messages are only for iOS +var tag = await FlutterNfcKit.poll(timeout: Duration(seconds: 10), + iosMultipleTagMessage: "Multiple tags found!", iosAlertMessage: "Scan your tag"); + +print(jsonEncode(tag)); +if (tag.type == NFCTagType.iso7816) { + var result = await FlutterNfcKit.transceive("00B0950000", Duration(seconds: 5)); // timeout is still Android-only, persist until next change + print(result); +} +// iOS only: set alert message on-the-fly +// this will persist until finish() +await FlutterNfcKit.setIosAlertMessage("hi there!"); + +// read NDEF records if available +if (tag.ndefAvailable) { + /// decoded NDEF records (see [ndef.NDEFRecord] for details) + /// `UriRecord: id=(empty) typeNameFormat=TypeNameFormat.nfcWellKnown type=U uri=https://github.com/nfcim/ndef` + for (var record in await FlutterNfcKit.readNDEFRecords(cached: false)) { + print(record.toString()); + } + /// raw NDEF records (data in hex string) + /// `{identifier: "", payload: "00010203", type: "0001", typeNameFormat: "nfcWellKnown"}` + for (var record in await FlutterNfcKit.readNDEFRawRecords(cached: false)) { + print(jsonEncode(record).toString()); + } +} + +// write NDEF records if applicable +if (tag.ndefWritable) { + // decoded NDEF records + await FlutterNfcKit.writeNDEFRecords([new ndef.UriRecord.fromUriString("https://github.com/nfcim/flutter_nfc_kit")]); + // raw NDEF records + await FlutterNfcKit.writeNDEFRawRecords([new NDEFRawRecord("00", "0001", "0002", "0003", ndef.TypeNameFormat.unknown)]); +} + +// read / write block / page / sector level data +// see documentation for platform-specific supportability +if (tag.type == NFCTagType.iso15693) { + await await FlutterNfcKit.writeBlock( + 1, // index + [0xde, 0xad, 0xbe, 0xff], // data + iso15693RequestFlag: Iso15693RequestFlag(), // optional flags for ISO 15693 + iso15693ExtendedMode: false // use extended mode for ISO 15693 + ); +} + +if (tag.type == NFCType.mifare_classic) { + await FlutterNfcKit.authenticateSector(0, keyA: "FFFFFFFFFFFF"); + var data = await FlutterNfcKit.readSector(0); // read one sector, or + var data = await FlutterNfcKit.readBlock(0); // read one block +} + +// Call finish() only once +await FlutterNfcKit.finish(); +// iOS only: show alert/error message on finish +await FlutterNfcKit.finish(iosAlertMessage: "Success"); +// or +await FlutterNfcKit.finish(iosErrorMessage: "Failed"); +``` + +## Event Streaming + +This is only supported on Android now. To receive NFC tag events even when your app is in the foreground, you can set up tag event stream support by: + +1. Create a custom Activity that extends `FlutterActivity` in your Android project: + +```kotlin +package your.package.name + +import android.app.PendingIntent +import android.content.Intent +import android.nfc.NfcAdapter +import android.nfc.Tag +import io.flutter.embedding.android.FlutterActivity +import im.nfc.flutter_nfc_kit.FlutterNfcKitPlugin + +class MainActivity : FlutterActivity() { + override fun onResume() { + super.onResume() + val adapter: NfcAdapter? = NfcAdapter.getDefaultAdapter(this) + val pendingIntent: PendingIntent = PendingIntent.getActivity( + this, 0, Intent(this, javaClass).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), PendingIntent.FLAG_MUTABLE + ) + // See https://developer.android.com/reference/android/nfc/NfcAdapter#enableForegroundDispatch(android.app.Activity,%20android.app.PendingIntent,%20android.content.IntentFilter[],%20java.lang.String[][]) for details + adapter?.enableForegroundDispatch(this, pendingIntent, null, null) + } + + override fun onPause() { + super.onPause() + val adapter: NfcAdapter? = NfcAdapter.getDefaultAdapter(this) + adapter?.disableForegroundDispatch(this) + } + + override fun onNewIntent(intent: Intent) { + val tag: Tag? = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG) + tag?.apply(FlutterNfcKitPlugin::handleTag) + } +} +``` + +You may also invoke `enableForegroundDispatch` and `disableForegroundDispatch` in other places as needed. + +2. Update your `AndroidManifest.xml` to use it as the main activity instead of the default Flutter activity. + +3. In Flutter code, listen to the tag event stream and process events: + +```dart +@override +void initState() { + super.initState(); + // listen to NFC tag events + FlutterNfcKit.tagStream.listen((tag) { + print('Tag detected: ${tag.id}'); + // process the tag as in polling mode + FlutterNfcKit.transceive("xxx", ...); + // DO NOT call `FlutterNfcKit.finish` in this mode! + }); +} +``` + +This will allow your app to receive NFC tag events through a stream, which is useful for scenarios where you need continuous tag reading or want to handle tags even when your app is in the foreground but not actively polling. + +## GUI Application + +See `lib/main.dart` for a GUI application on Android / iOS / web. Skeleton code for specific platforms are not uploaded to . Please refer to the [GitHub repository](https://github.com/nfcim/flutter_nfc_kit). diff --git a/example/ios/.gitignore b/example/ios/.gitignore index e96ef60..7a7f987 100644 --- a/example/ios/.gitignore +++ b/example/ios/.gitignore @@ -1,3 +1,4 @@ +**/dgph *.mode1v3 *.mode2v3 *.moved-aside @@ -18,6 +19,7 @@ Flutter/App.framework Flutter/Flutter.framework Flutter/Flutter.podspec Flutter/Generated.xcconfig +Flutter/ephemeral/ Flutter/app.flx Flutter/app.zip Flutter/flutter_assets/ diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist index f2872cf..8c6e561 100644 --- a/example/ios/Flutter/AppFrameworkInfo.plist +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 9.0 + 12.0 diff --git a/example/ios/Flutter/Debug.xcconfig b/example/ios/Flutter/Debug.xcconfig index e8efba1..ec97fc6 100644 --- a/example/ios/Flutter/Debug.xcconfig +++ b/example/ios/Flutter/Debug.xcconfig @@ -1,2 +1,2 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/example/ios/Flutter/Release.xcconfig b/example/ios/Flutter/Release.xcconfig index 399e934..c4855bf 100644 --- a/example/ios/Flutter/Release.xcconfig +++ b/example/ios/Flutter/Release.xcconfig @@ -1,2 +1,2 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/example/ios/Podfile b/example/ios/Podfile index 1e8c3c9..279576f 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '9.0' +# platform :ios, '12.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index ed954c0..287be12 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1,22 +1,16 @@ PODS: - Flutter (1.0.0) - - flutter_nfc_kit (2.0.0): - - Flutter DEPENDENCIES: - Flutter (from `Flutter`) - - flutter_nfc_kit (from `.symlinks/plugins/flutter_nfc_kit/ios`) EXTERNAL SOURCES: Flutter: :path: Flutter - flutter_nfc_kit: - :path: ".symlinks/plugins/flutter_nfc_kit/ios" SPEC CHECKSUMS: - Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a - flutter_nfc_kit: 965c98c3fa68f5609f1cc89abb968fe1b8ffdbaa + Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 -PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c +PODFILE CHECKSUM: c4c93c5f6502fe2754f48404d3594bf779584011 -COCOAPODS: 1.11.2 +COCOAPODS: 1.16.2 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 54887d1..8d97318 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,17 +3,18 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - CD60AA827172B2D4F903B633 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DED07A612A308E6E445087C4 /* Pods_Runner.framework */; }; + E58C8BA72341A6FDA9D194F8 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C0443C20940C009428DBFC8 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -32,8 +33,11 @@ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 21B301EF0FC0C3405BEEB5B5 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 3CA721E826034CE6B499F060 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 52D42AC623D2C3710063AB8B /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; + 5C0443C20940C009428DBFC8 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; @@ -44,10 +48,7 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - AA5CBE5C422B789023911F09 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - AFC6A796EE0F18853DAA2A26 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; - C9DF0F4A4A5ED2E1DCAA30C7 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; - DED07A612A308E6E445087C4 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + CC17078EABDEF174C9DEB078 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -55,7 +56,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - CD60AA827172B2D4F903B633 /* Pods_Runner.framework in Frameworks */, + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, + E58C8BA72341A6FDA9D194F8 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -79,8 +81,8 @@ 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, - DE806EA20D44ECD93EB31DD8 /* Pods */, - B58754D6A3081C84B524BE59 /* Frameworks */, + F893C2497E8F1D360BEBEB7F /* Pods */, + AA8E6B8C7207A8A364AEEA84 /* Frameworks */, ); sourceTree = ""; }; @@ -116,21 +118,22 @@ name = "Supporting Files"; sourceTree = ""; }; - B58754D6A3081C84B524BE59 /* Frameworks */ = { + AA8E6B8C7207A8A364AEEA84 /* Frameworks */ = { isa = PBXGroup; children = ( - DED07A612A308E6E445087C4 /* Pods_Runner.framework */, + 5C0443C20940C009428DBFC8 /* Pods_Runner.framework */, ); name = Frameworks; sourceTree = ""; }; - DE806EA20D44ECD93EB31DD8 /* Pods */ = { + F893C2497E8F1D360BEBEB7F /* Pods */ = { isa = PBXGroup; children = ( - C9DF0F4A4A5ED2E1DCAA30C7 /* Pods-Runner.debug.xcconfig */, - AA5CBE5C422B789023911F09 /* Pods-Runner.release.xcconfig */, - AFC6A796EE0F18853DAA2A26 /* Pods-Runner.profile.xcconfig */, + 3CA721E826034CE6B499F060 /* Pods-Runner.debug.xcconfig */, + CC17078EABDEF174C9DEB078 /* Pods-Runner.release.xcconfig */, + 21B301EF0FC0C3405BEEB5B5 /* Pods-Runner.profile.xcconfig */, ); + name = Pods; path = Pods; sourceTree = ""; }; @@ -141,20 +144,22 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - 98052AEEBC7DC6F251FCC9E4 /* [CP] Check Pods Manifest.lock */, + F90AEBACCBF7EA4B435EAF97 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 28F73B917398D107876B80A2 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); dependencies = ( ); name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; @@ -165,12 +170,11 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = "The Chromium Authors"; TargetAttributes = { 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; - DevelopmentTeam = LWANP9659G; LastSwiftMigration = 1100; }; }; @@ -184,6 +188,9 @@ Base, ); mainGroup = 97C146E51CF9000F007C117D; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, + ); productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; projectRoot = ""; @@ -208,30 +215,14 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 28F73B917398D107876B80A2 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", - "${BUILT_PRODUCTS_DIR}/flutter_nfc_kit/flutter_nfc_kit.framework", - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_nfc_kit.framework", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( @@ -242,6 +233,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -254,7 +246,7 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; - 98052AEEBC7DC6F251FCC9E4 /* [CP] Check Pods Manifest.lock */ = { + F90AEBACCBF7EA4B435EAF97 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -351,7 +343,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -368,7 +360,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = LWANP9659G; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -376,7 +368,10 @@ ); INFOPLIST_FILE = Runner/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", @@ -436,7 +431,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -485,7 +480,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -503,7 +498,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = LWANP9659G; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -511,7 +506,10 @@ ); INFOPLIST_FILE = Runner/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", @@ -533,7 +531,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = LWANP9659G; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -541,7 +539,10 @@ ); INFOPLIST_FILE = Runner/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", @@ -578,6 +579,20 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; } diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 3db53b6..7120d2e 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,10 +1,28 @@ + + + + + + + + + + A00000000386980701 + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + diff --git a/example/lib/main.dart b/example/lib/main.dart index 2064f80..4c8156a 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -7,10 +7,11 @@ import 'package:flutter/services.dart'; import 'package:flutter_nfc_kit/flutter_nfc_kit.dart'; import 'package:logging/logging.dart'; import 'package:ndef/ndef.dart' as ndef; +import 'package:ndef/utilities.dart'; -import 'record-setting/raw_record_setting.dart'; -import 'record-setting/text_record_setting.dart'; -import 'record-setting/uri_record_setting.dart'; +import 'ndef_record/raw_record_setting.dart'; +import 'ndef_record/text_record_setting.dart'; +import 'ndef_record/uri_record_setting.dart'; void main() { Logger.root.level = Level.ALL; // defaults to Level.INFO @@ -22,7 +23,7 @@ void main() { class MyApp extends StatefulWidget { @override - _MyAppState createState() => _MyAppState(); + State createState() => _MyAppState(); } class _MyAppState extends State with SingleTickerProviderStateMixin { @@ -42,14 +43,21 @@ class _MyAppState extends State with SingleTickerProviderStateMixin { @override void initState() { super.initState(); - if (!kIsWeb) + if (!kIsWeb) { _platformVersion = '${Platform.operatingSystem} ${Platform.operatingSystemVersion}'; - else + } else { _platformVersion = 'Web'; + } initPlatformState(); - _tabController = new TabController(length: 2, vsync: this); + _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. @@ -85,7 +93,7 @@ class _MyAppState extends State with SingleTickerProviderStateMixin { ], controller: _tabController, )), - body: new TabBarView(controller: _tabController, children: [ + body: TabBarView(controller: _tabController, children: [ Scrollbar( child: SingleChildScrollView( child: Center( @@ -140,7 +148,7 @@ class _MyAppState extends State with SingleTickerProviderStateMixin { } // Pretend that we are working - if (!kIsWeb) sleep(new Duration(seconds: 1)); + if (!kIsWeb) sleep(Duration(seconds: 1)); await FlutterNfcKit.finish(iosAlertMessage: "Finished!"); }, child: Text('Start polling'), @@ -163,7 +171,7 @@ class _MyAppState extends State with SingleTickerProviderStateMixin { children: [ ElevatedButton( onPressed: () async { - if (_records!.length != 0) { + if (_records!.isNotEmpty) { try { NFCTag tag = await FlutterNfcKit.poll(); setState(() { @@ -213,7 +221,7 @@ class _MyAppState extends State with SingleTickerProviderStateMixin { final result = await Navigator.push( context, MaterialPageRoute( builder: (context) { - return TextRecordSetting(); + return NDEFTextRecordSetting(); })); if (result != null) { if (result is ndef.TextRecord) { @@ -231,7 +239,7 @@ class _MyAppState extends State with SingleTickerProviderStateMixin { final result = await Navigator.push( context, MaterialPageRoute( builder: (context) { - return UriRecordSetting(); + return NDEFUriRecordSetting(); })); if (result != null) { if (result is ndef.UriRecord) { diff --git a/example/lib/record-setting/raw_record_setting.dart b/example/lib/ndef_record/raw_record_setting.dart similarity index 90% rename from example/lib/record-setting/raw_record_setting.dart rename to example/lib/ndef_record/raw_record_setting.dart index 04bb879..e511d6e 100644 --- a/example/lib/record-setting/raw_record_setting.dart +++ b/example/lib/ndef_record/raw_record_setting.dart @@ -1,18 +1,18 @@ import 'package:flutter/material.dart'; import 'package:ndef/ndef.dart' as ndef; +import 'package:ndef/utilities.dart'; class NDEFRecordSetting extends StatefulWidget { final ndef.NDEFRecord record; - NDEFRecordSetting({Key? key, ndef.NDEFRecord? record}) - : record = record ?? ndef.NDEFRecord(), - super(key: key); + NDEFRecordSetting({super.key, ndef.NDEFRecord? record}) + : record = record ?? ndef.NDEFRecord(); @override - _NDEFRecordSetting createState() => _NDEFRecordSetting(); + State createState() => _NDEFRecordSetting(); } class _NDEFRecordSetting extends State { - GlobalKey _formKey = new GlobalKey(); + final GlobalKey _formKey = GlobalKey(); late TextEditingController _identifierController; late TextEditingController _payloadController; late TextEditingController _typeController; @@ -24,25 +24,25 @@ class _NDEFRecordSetting extends State { if (widget.record.id == null) { _identifierController = - new TextEditingController.fromValue(TextEditingValue(text: "")); + TextEditingController.fromValue(TextEditingValue(text: "")); } else { - _identifierController = new TextEditingController.fromValue( + _identifierController = TextEditingController.fromValue( TextEditingValue(text: widget.record.id!.toHexString())); } if (widget.record.payload == null) { _payloadController = - new TextEditingController.fromValue(TextEditingValue(text: "")); + TextEditingController.fromValue(TextEditingValue(text: "")); } else { - _payloadController = new TextEditingController.fromValue( + _payloadController = TextEditingController.fromValue( TextEditingValue(text: widget.record.payload!.toHexString())); } if (widget.record.encodedType == null && widget.record.decodedType == null) { // bug in ndef package (fixed in newest version) _typeController = - new TextEditingController.fromValue(TextEditingValue(text: "")); + TextEditingController.fromValue(TextEditingValue(text: "")); } else { - _typeController = new TextEditingController.fromValue( + _typeController = TextEditingController.fromValue( TextEditingValue(text: widget.record.type!.toHexString())); } _dropButtonValue = ndef.TypeNameFormat.values.indexOf(widget.record.tnf); diff --git a/example/lib/record-setting/text_record_setting.dart b/example/lib/ndef_record/text_record_setting.dart similarity index 88% rename from example/lib/record-setting/text_record_setting.dart rename to example/lib/ndef_record/text_record_setting.dart index 1e468a5..fb9ca7f 100644 --- a/example/lib/record-setting/text_record_setting.dart +++ b/example/lib/ndef_record/text_record_setting.dart @@ -2,17 +2,16 @@ import 'package:flutter/material.dart'; import 'package:ndef/ndef.dart' as ndef; -class TextRecordSetting extends StatefulWidget { +class NDEFTextRecordSetting extends StatefulWidget { final ndef.TextRecord record; - TextRecordSetting({Key? key, ndef.TextRecord? record}) - : record = record ?? ndef.TextRecord(language: 'en', text: ''), - super(key: key); + NDEFTextRecordSetting({super.key, ndef.TextRecord? record}) + : record = record ?? ndef.TextRecord(language: 'en', text: ''); @override - _TextRecordSetting createState() => _TextRecordSetting(); + State createState() => _NDEFTextRecordSetting(); } -class _TextRecordSetting extends State { - GlobalKey _formKey = new GlobalKey(); +class _NDEFTextRecordSetting extends State { + final GlobalKey _formKey = GlobalKey(); late TextEditingController _languageController; late TextEditingController _textController; late int _dropButtonValue; @@ -21,9 +20,9 @@ class _TextRecordSetting extends State { initState() { super.initState(); - _languageController = new TextEditingController.fromValue( + _languageController = TextEditingController.fromValue( TextEditingValue(text: widget.record.language!)); - _textController = new TextEditingController.fromValue( + _textController = TextEditingController.fromValue( TextEditingValue(text: widget.record.text!)); _dropButtonValue = ndef.TextEncoding.values.indexOf(widget.record.encoding); } diff --git a/example/lib/record-setting/uri_record_setting.dart b/example/lib/ndef_record/uri_record_setting.dart similarity index 85% rename from example/lib/record-setting/uri_record_setting.dart rename to example/lib/ndef_record/uri_record_setting.dart index 8171a91..ff04612 100644 --- a/example/lib/record-setting/uri_record_setting.dart +++ b/example/lib/ndef_record/uri_record_setting.dart @@ -2,17 +2,16 @@ import 'package:flutter/material.dart'; import 'package:ndef/ndef.dart' as ndef; -class UriRecordSetting extends StatefulWidget { +class NDEFUriRecordSetting extends StatefulWidget { final ndef.UriRecord record; - UriRecordSetting({Key? key, ndef.UriRecord? record}) - : record = record ?? ndef.UriRecord(prefix: '', content: ''), - super(key: key); + NDEFUriRecordSetting({super.key, ndef.UriRecord? record}) + : record = record ?? ndef.UriRecord(prefix: '', content: ''); @override - _UriRecordSetting createState() => _UriRecordSetting(); + State createState() => _NDEFUriRecordSetting(); } -class _UriRecordSetting extends State { - GlobalKey _formKey = new GlobalKey(); +class _NDEFUriRecordSetting extends State { + final GlobalKey _formKey = GlobalKey(); late TextEditingController _contentController; String? _dropButtonValue; @@ -20,7 +19,7 @@ class _UriRecordSetting extends State { initState() { super.initState(); - _contentController = new TextEditingController.fromValue( + _contentController = TextEditingController.fromValue( TextEditingValue(text: widget.record.content!)); _dropButtonValue = widget.record.prefix; } @@ -49,7 +48,7 @@ class _UriRecordSetting extends State { }).toList(), onChanged: (value) { setState(() { - _dropButtonValue = value as String?; + _dropButtonValue = value; }); }, ), diff --git a/example/pubspec.lock b/example/pubspec.lock index 4c6276d..ae26e42 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -37,34 +37,34 @@ packages: dependency: transitive description: name: collection - sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.dev" source: hosted - version: "1.17.2" + version: "1.18.0" convert: dependency: transitive description: name: convert - sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" crypto: dependency: transitive description: name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.6" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 url: "https://pub.dev" source: hosted - version: "1.0.6" + version: "1.0.8" fake_async: dependency: transitive description: @@ -73,18 +73,26 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" flutter_nfc_kit: - dependency: "direct dev" + dependency: "direct main" description: path: ".." relative: true source: path - version: "3.3.3" + version: "3.6.0-rc.6" flutter_test: dependency: "direct dev" description: flutter @@ -95,70 +103,86 @@ packages: description: flutter source: sdk version: "0.0.0" - js: + json_annotation: dependency: transitive description: - name: js - sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" url: "https://pub.dev" source: hosted - version: "0.6.7" - json_annotation: + version: "4.9.0" + leak_tracker: dependency: transitive description: - name: json_annotation - sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + name: leak_tracker + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" url: "https://pub.dev" source: hosted - version: "4.8.1" - logging: + version: "10.0.5" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + url: "https://pub.dev" + source: hosted + version: "3.0.5" + leak_tracker_testing: dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + logging: + dependency: "direct main" description: name: logging - sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" matcher: dependency: transitive description: name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.16" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.15.0" ndef: - dependency: transitive + dependency: "direct main" description: name: ndef - sha256: e40ece11d1cac52cba2b7d0211228c1b5c278032cce3f5bf3e2eefe3762fde6b + sha256: "5083507cff4bb823b2a198a27ea2c70c4d6bc27a97b66097d966a250e1615d54" url: "https://pub.dev" source: hosted - version: "0.3.1" + version: "0.3.4" path: dependency: transitive description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.0" sky_engine: dependency: transitive description: flutter @@ -172,22 +196,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" stack_trace: dependency: transitive description: name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.11.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" string_scanner: dependency: transitive description: @@ -208,26 +240,26 @@ packages: dependency: transitive description: name: test_api - sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "0.7.2" typed_data: dependency: transitive description: name: typed_data - sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.4.0" uuid: dependency: transitive description: name: uuid - sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff url: "https://pub.dev" source: hosted - version: "3.0.7" + version: "4.5.1" vector_math: dependency: transitive description: @@ -236,14 +268,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" - web: + vm_service: dependency: transitive description: - name: web - sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + name: vm_service + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "0.1.4-beta" + version: "14.2.5" sdks: - dart: ">=3.1.0-185.0.dev <4.0.0" - flutter: ">=2.0.0" + dart: ">=3.5.0 <4.0.0" + flutter: ">=3.24.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index bf7d3f5..5000b03 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -3,61 +3,20 @@ description: Demonstrates how to use the flutter_nfc_kit plugin. publish_to: 'none' environment: - sdk: ">=2.12.0" + sdk: ">=3.2.0" dependencies: flutter: sdk: flutter - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.3 + flutter_nfc_kit: + path: ../ + logging: ^1.3.0 + ndef: ^0.3.3 + cupertino_icons: ^1.0.8 dev_dependencies: flutter_test: sdk: flutter - flutter_nfc_kit: - path: ../ - -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter. flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. uses-material-design: true - - # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware. - - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/assets-and-images/#from-packages - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/custom-fonts/#from-packages diff --git a/ios/Assets/.gitkeep b/ios/Assets/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/ios/Classes/FlutterNfcKitPlugin.h b/ios/Classes/FlutterNfcKitPlugin.h deleted file mode 100644 index df2ee8c..0000000 --- a/ios/Classes/FlutterNfcKitPlugin.h +++ /dev/null @@ -1,4 +0,0 @@ -#import - -@interface FlutterNfcKitPlugin : NSObject -@end diff --git a/ios/Classes/FlutterNfcKitPlugin.m b/ios/Classes/FlutterNfcKitPlugin.m deleted file mode 100644 index bbaa9fc..0000000 --- a/ios/Classes/FlutterNfcKitPlugin.m +++ /dev/null @@ -1,15 +0,0 @@ -#import "FlutterNfcKitPlugin.h" -#if __has_include() -#import -#else -// Support project import fallback if the generated compatibility header -// is not copied when this plugin is created as a library. -// https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816 -#import "flutter_nfc_kit-Swift.h" -#endif - -@implementation FlutterNfcKitPlugin -+ (void)registerWithRegistrar:(NSObject*)registrar { - [SwiftFlutterNfcKitPlugin registerWithRegistrar:registrar]; -} -@end diff --git a/ios/flutter_nfc_kit.podspec b/ios/flutter_nfc_kit.podspec index b906fc2..3985fd1 100644 --- a/ios/flutter_nfc_kit.podspec +++ b/ios/flutter_nfc_kit.podspec @@ -4,7 +4,7 @@ # Pod::Spec.new do |s| s.name = 'flutter_nfc_kit' - s.version = '2.0.0' + s.version = '3.6.0' s.summary = 'NFC support plugin of Flutter.' s.description = <<-DESC Flutter plugin to provide NFC functionality on Android and iOS, including reading metadata, read & write NDEF records, and transceive layer 3 & 4 data with NFC tags / cards. @@ -13,7 +13,7 @@ Pod::Spec.new do |s| s.license = { :file => '../LICENSE' } s.author = { 'nfc.im' => 'nfsee@nfc.im' } s.source = { :path => '.' } - s.source_files = 'Classes/**/*' + s.source_files = 'flutter_nfc_kit/Sources/flutter_nfc_kit/**/*.swift' s.dependency 'Flutter' s.weak_frameworks = ['CoreNFC'] s.platform = :ios, '13.0' diff --git a/ios/flutter_nfc_kit/.gitignore b/ios/flutter_nfc_kit/.gitignore new file mode 100644 index 0000000..ea0f7b6 --- /dev/null +++ b/ios/flutter_nfc_kit/.gitignore @@ -0,0 +1 @@ +.swiftpm/ diff --git a/ios/flutter_nfc_kit/Package.swift b/ios/flutter_nfc_kit/Package.swift new file mode 100644 index 0000000..f42cd07 --- /dev/null +++ b/ios/flutter_nfc_kit/Package.swift @@ -0,0 +1,22 @@ +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "flutter_nfc_kit", + platforms: [ + .iOS("13.0"), + ], + products: [ + .library(name: "flutter-nfc-kit", targets: ["flutter_nfc_kit"]) + ], + dependencies: [], + targets: [ + .target( + name: "flutter_nfc_kit", + dependencies: [], + resources: [] + ) + ] +) \ No newline at end of file diff --git a/ios/Classes/SwiftFlutterNfcKitPlugin.swift b/ios/flutter_nfc_kit/Sources/flutter_nfc_kit/FlutterNfcKitPlugin.swift similarity index 68% rename from ios/Classes/SwiftFlutterNfcKitPlugin.swift rename to ios/flutter_nfc_kit/Sources/flutter_nfc_kit/FlutterNfcKitPlugin.swift index 532dc7e..3ef6b34 100644 --- a/ios/Classes/SwiftFlutterNfcKitPlugin.swift +++ b/ios/flutter_nfc_kit/Sources/flutter_nfc_kit/FlutterNfcKitPlugin.swift @@ -8,7 +8,7 @@ extension Data { let rawValue: Int static let upperCase = HexEncodingOptions(rawValue: 1 << 0) } - + func hexEncodedString(options: HexEncodingOptions = [.upperCase]) -> String { let format = options.contains(.upperCase) ? "%02hhX" : "%02hhx" return map { String(format: format, $0) }.joined() @@ -30,18 +30,18 @@ func dataWithHexString(hex: String) -> Data { return data } -public class SwiftFlutterNfcKitPlugin: NSObject, FlutterPlugin, NFCTagReaderSessionDelegate { +public class FlutterNfcKitPlugin: NSObject, FlutterPlugin, NFCTagReaderSessionDelegate { var session: NFCTagReaderSession? var result: FlutterResult? var tag: NFCTag? var multipleTagMessage: String? - + public static func register(with registrar: FlutterPluginRegistrar) { - let channel = FlutterMethodChannel(name: "flutter_nfc_kit", binaryMessenger: registrar.messenger()) - let instance = SwiftFlutterNfcKitPlugin() + let channel = FlutterMethodChannel(name: "flutter_nfc_kit/method", binaryMessenger: registrar.messenger()) + let instance = FlutterNfcKitPlugin() registrar.addMethodCallDelegate(instance, channel: channel) } - + // from FlutterPlugin public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { if call.method == "getNFCAvailability" { @@ -50,6 +50,13 @@ public class SwiftFlutterNfcKitPlugin: NSObject, FlutterPlugin, NFCTagReaderSess } else { result("not_supported") } + } else if call.method == "restartPolling" { + if let session = session { + self.result = result + session.restartPolling() + } else { + result(FlutterError(code: "404", message: "No active session", details: nil)) + } } else if call.method == "poll" { if session != nil { result(FlutterError(code: "406", message: "Cannot invoke poll in a active session", details: nil)) @@ -81,74 +88,99 @@ public class SwiftFlutterNfcKitPlugin: NSObject, FlutterPlugin, NFCTagReaderSess if tag != nil { let req = (call.arguments as? [String: Any?])?["data"] if req != nil, req is String || req is FlutterStandardTypedData { - var data: Data? + var data: Data switch req { case let hexReq as String: data = dataWithHexString(hex: hexReq) case let binReq as FlutterStandardTypedData: data = binReq.data default: - data = nil + result(FlutterError(code: "400", message: "No data specified", details: nil)) + return } - + + if data.count == 0 { + result(FlutterError(code: "400", message: "Empty data specified", details: nil)) + return + } + switch tag { case let .iso7816(tag): - var apdu: NFCISO7816APDU? - if data != nil { - apdu = NFCISO7816APDU(data: data!) + let apdu: NFCISO7816APDU? = NFCISO7816APDU(data: data) + if apdu == nil { + result(FlutterError(code: "400", message: "Command format error", details: nil)) + return } - - if apdu != nil { - tag.sendCommand(apdu: apdu!, completionHandler: { (response: Data, sw1: UInt8, sw2: UInt8, error: Error?) in - if let error = error { - result(FlutterError(code: "500", message: "Communication error", details: error.localizedDescription)) + tag.sendCommand(apdu: apdu!) { (response: Data, sw1: UInt8, sw2: UInt8, error: Error?) in + if let error = error { + result(FlutterError(code: "500", message: "Communication error", details: error.localizedDescription)) + } else { + var response = response + response.append(contentsOf: [sw1, sw2]) + if req is String { + result(response.hexEncodedString()) } else { - var response = response - response.append(contentsOf: [sw1, sw2]) - if req is String { - result(response.hexEncodedString()) - } else { - result(response) - } + result(response) } - }) - } else { - result(FlutterError(code: "400", message: "Command format error", details: nil)) + } } + case let .feliCa(tag): - if data != nil { - // the first byte in data is length - // and iOS will add it for us - // so skip it - tag.sendFeliCaCommand(commandPacket: data!.advanced(by: 1), completionHandler: { (response: Data, error: Error?) in - if let error = error { - result(FlutterError(code: "500", message: "Communication error", details: error.localizedDescription)) + if data.count < 2 { + result(FlutterError(code: "400", message: "feliCa command format error", details: nil)) + return + } + // the first byte in data is length, and iOS will add it for us, so skip it + tag.sendFeliCaCommand(commandPacket: data.advanced(by: 1)) { (response: Data, error: Error?) in + if let error = error { + result(FlutterError(code: "500", message: "Communication error", details: error.localizedDescription)) + } else { + if req is String { + result(response.hexEncodedString()) } else { - if req is String { - result(response.hexEncodedString()) - } else { - result(response) - } + result(response) } - }) - } else { - result(FlutterError(code: "400", message: "No felica command specified", details: nil)) + } } case let .miFare(tag): - if data != nil { - tag.sendMiFareCommand(commandPacket: data!, completionHandler: { (response: Data, error: Error?) in - if let error = error { - result(FlutterError(code: "500", message: "Communication error", details: error.localizedDescription)) + tag.sendMiFareCommand(commandPacket: data) { (response: Data, error: Error?) in + if let error = error { + result(FlutterError(code: "500", message: "Communication error", details: error.localizedDescription)) + } else { + if req is String { + result(response.hexEncodedString()) } else { + result(response) + } + } + } + case let .iso15693(tag): + if data.count < 2 { + result(FlutterError(code: "400", message: "iso15693 command format error", details: nil)) + return + } + if #available(iOS 14, *) { + // format: flag, command, [parameter, data] + tag.sendRequest(requestFlags: Int(data[0]), commandCode: Int(data[1]), data: data.advanced(by: 2)) { (res: Result<(NFCISO15693ResponseFlag, Data?), Error>) in + switch (res) { + case let .failure(err): + result(FlutterError(code: "500", message: "Communication error", details: err.localizedDescription)) + case let .success((flags, data)): + var response = Data() + response.append(flags.rawValue) + if data != nil { + response.append(data!) + } if req is String { result(response.hexEncodedString()) } else { result(response) } } - }) + } } else { - result(FlutterError(code: "400", message: "No mifare command specified", details: nil)) + result(FlutterError(code: "405", message: "Transceive for iso15693 not supported on iOS < 14.0", details: nil)) + return } default: result(FlutterError(code: "405", message: "Transceive not supported on this type of card", details: nil)) @@ -159,6 +191,51 @@ public class SwiftFlutterNfcKitPlugin: NSObject, FlutterPlugin, NFCTagReaderSess } else { result(FlutterError(code: "406", message: "No tag polled", details: nil)) } + } else if call.method == "readBlock" { + let arguments = call.arguments as! [String : Any?] + if case let .iso15693(tag) = tag { + let rawFlags = (arguments["iso15693Flags"] as? UInt8) ?? 0 + let extendedMode = (arguments["iso15693ExtendedMode"] as? Bool) ?? false + let handler = { (dataBlock: Data, error: Error?) in + if let error = error { + result(FlutterError(code: "500", message: "Communication error", details: error.localizedDescription)) + } else { + result(dataBlock) + } + } + if !extendedMode { + let blockNumber = arguments["index"] as! UInt8 + tag.readSingleBlock(requestFlags: RequestFlag(rawValue: rawFlags), blockNumber: blockNumber, completionHandler: handler) + } else { + let blockNumber = arguments["index"] as! Int + tag.extendedReadSingleBlock(requestFlags: RequestFlag(rawValue: rawFlags), blockNumber: blockNumber, completionHandler: handler) + } + } else { + result(FlutterError(code: "405", message: "readBlock not supported on this type of card", details: nil)) + } + } else if call.method == "writeBlock" { + let arguments = call.arguments as! [String : Any?] + let data = (arguments["data"] as! FlutterStandardTypedData).data + if case let .iso15693(tag) = tag { + let rawFlags = (arguments["iso15693Flags"] as? UInt8) ?? 0 + let extendedMode = (arguments["iso15693ExtendedMode"] as? Bool) ?? false + let handler = { (error: Error?) in + if let error = error { + result(FlutterError(code: "500", message: "Communication error", details: error.localizedDescription)) + } else { + result(nil) + } + } + if !extendedMode { + let blockNumber = arguments["index"] as! UInt8 + tag.writeSingleBlock(requestFlags: RequestFlag(rawValue: rawFlags), blockNumber: blockNumber, dataBlock: data, completionHandler: handler) + } else { + 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 call.method == "readNDEF" { if tag != nil { var ndefTag: NFCNDEFTag? @@ -175,7 +252,7 @@ public class SwiftFlutterNfcKitPlugin: NSObject, FlutterPlugin, NFCTagReaderSess ndefTag = nil } if ndefTag != nil { - ndefTag!.readNDEF(completionHandler: { (msg: NFCNDEFMessage?, error: Error?) in + ndefTag!.readNDEF() { (msg: NFCNDEFMessage?, error: Error?) in if let nfcError = error as? NFCReaderError, nfcError.errorCode == 403 { // NDEF tag does not contain any NDEF message result("[]") @@ -183,10 +260,10 @@ public class SwiftFlutterNfcKitPlugin: NSObject, FlutterPlugin, NFCTagReaderSess result(FlutterError(code: "500", message: "Read NDEF error", details: error.localizedDescription)) } else if let msg = msg { var records: [[String: Any]] = [] - + for record in msg.records { var entry: [String: Any] = [:] - + entry["identifier"] = record.identifier.hexEncodedString() entry["payload"] = record.payload.hexEncodedString() entry["type"] = record.type.hexEncodedString() @@ -206,17 +283,17 @@ public class SwiftFlutterNfcKitPlugin: NSObject, FlutterPlugin, NFCTagReaderSess default: entry["typeNameFormat"] = "unknown" } - + records.append(entry) } - + let jsonData = try! JSONSerialization.data(withJSONObject: records) let jsonString = String(data: jsonData, encoding: .utf8) result(jsonString) } else { result(FlutterError(code: "500", message: "Impossible branch reached", details: nil)) } - }) + } } else { result(FlutterError(code: "405", message: "NDEF not supported on this type of card", details: nil)) } @@ -269,14 +346,14 @@ public class SwiftFlutterNfcKitPlugin: NSObject, FlutterPlugin, NFCTagReaderSess payload: dataWithHexString(hex: record["payload"] as! String) )) } - - ndefTag!.writeNDEF(NFCNDEFMessage(records: records), completionHandler: { (error: Error?) in + + ndefTag!.writeNDEF(NFCNDEFMessage(records: records)) { (error: Error?) in if let error = error { result(FlutterError(code: "500", message: "Write NDEF error", details: error.localizedDescription)) } else { result(nil) } - }) + } } else { result(FlutterError(code: "400", message: "Bad argument", details: nil)) } @@ -289,12 +366,12 @@ public class SwiftFlutterNfcKitPlugin: NSObject, FlutterPlugin, NFCTagReaderSess } else if call.method == "finish" { self.result?(FlutterError(code: "406", message: "Session not active", details: nil)) self.result = nil - + if let session = session { let arguments = call.arguments as! [String: Any?] let alertMessage = arguments["iosAlertMessage"] as? String let errorMessage = arguments["iosErrorMessage"] as? String - + if let errorMessage = errorMessage { session.invalidate(errorMessage: errorMessage) } else { @@ -305,7 +382,7 @@ public class SwiftFlutterNfcKitPlugin: NSObject, FlutterPlugin, NFCTagReaderSess } self.session = nil } - + tag = nil result(nil) } else if call.method == "setIosAlertMessage" { @@ -333,13 +410,13 @@ public class SwiftFlutterNfcKitPlugin: NSObject, FlutterPlugin, NFCTagReaderSess ndefTag = nil } if ndefTag != nil { - ndefTag!.writeLock(completionHandler: { (error: Error?) in + ndefTag!.writeLock() { (error: Error?) in if let error = error { result(FlutterError(code: "500", message: "Lock NDEF error", details: error.localizedDescription)) } else { result(nil) } - }) + } } else { result(FlutterError(code: "405", message: "NDEF not supported on this type of card", details: nil)) } @@ -351,10 +428,10 @@ public class SwiftFlutterNfcKitPlugin: NSObject, FlutterPlugin, NFCTagReaderSess result(FlutterMethodNotImplemented) } } - + // from NFCTagReaderSessionDelegate public func tagReaderSessionDidBecomeActive(_: NFCTagReaderSession) {} - + // from NFCTagReaderSessionDelegate public func tagReaderSession(_: NFCTagReaderSession, didInvalidateWithError error: Error) { guard result != nil else { return; } @@ -378,7 +455,7 @@ public class SwiftFlutterNfcKitPlugin: NSObject, FlutterPlugin, NFCTagReaderSess session = nil tag = nil } - + // from NFCTagReaderSessionDelegate public func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) { if tags.count > 1 { @@ -392,9 +469,9 @@ public class SwiftFlutterNfcKitPlugin: NSObject, FlutterPlugin, NFCTagReaderSess } return } - + let firstTag = tags.first! - + var result: [String: Any] = [:] // default NDEF status result["ndefAvailable"] = false @@ -403,7 +480,7 @@ public class SwiftFlutterNfcKitPlugin: NSObject, FlutterPlugin, NFCTagReaderSess // fake NDEF results result["ndefType"] = "" result["ndefCanMakeReadOnly"] = false - + switch firstTag { case let .iso7816(tag): result["type"] = "iso7816" @@ -438,8 +515,8 @@ public class SwiftFlutterNfcKitPlugin: NSObject, FlutterPlugin, NFCTagReaderSess case let .feliCa(tag): result["type"] = "iso18092" result["standard"] = "ISO 18092 (FeliCa)" + result["id"] = tag.currentIDm.hexEncodedString() result["systemCode"] = tag.currentSystemCode.hexEncodedString() - result["manufacturer"] = tag.currentIDm.hexEncodedString() case let .iso15693(tag): result["type"] = "iso15693" result["standard"] = "ISO 15693" @@ -448,16 +525,17 @@ public class SwiftFlutterNfcKitPlugin: NSObject, FlutterPlugin, NFCTagReaderSess default: result["type"] = "unknown" result["standard"] = "unknown" + result["id"] = "unknown" } - - session.connect(to: firstTag, completionHandler: { (error: Error?) in + + session.connect(to: firstTag) { (error: Error?) in if let error = error { self.result?(FlutterError(code: "500", message: "Error connecting to card", details: error.localizedDescription)) self.result = nil return } self.tag = firstTag - + var ndefTag: NFCNDEFTag? switch self.tag { case let .iso7816(tag): @@ -471,9 +549,9 @@ public class SwiftFlutterNfcKitPlugin: NSObject, FlutterPlugin, NFCTagReaderSess default: ndefTag = nil } - + if ndefTag != nil { - ndefTag!.queryNDEFStatus(completionHandler: { (status: NFCNDEFStatus, capacity: Int, error: Error?) in + ndefTag!.queryNDEFStatus() { (status: NFCNDEFStatus, capacity: Int, error: Error?) in if error == nil { if status != NFCNDEFStatus.notSupported { result["ndefAvailable"] = true @@ -485,17 +563,34 @@ public class SwiftFlutterNfcKitPlugin: NSObject, FlutterPlugin, NFCTagReaderSess result["ndefCapacity"] = capacity } // ignore error, just return with ndef disabled - let jsonData = try! JSONSerialization.data(withJSONObject: result) - let jsonString = String(data: jsonData, encoding: .utf8) - self.result?(jsonString) - self.result = nil - }) + switch self.tag { + case let .feliCa(tag): + tag.polling(systemCode: tag.currentSystemCode, requestCode: .noRequest, timeSlot: .max16) { (pmm: Data, _: Data, error: Error?) in + if let error = error { + self.result?(FlutterError(code: "500", message: "Communication error on connect", details: error.localizedDescription)) + self.result = nil + } else { + result["manufacturer"] = pmm.hexEncodedString() + + let jsonData = try! JSONSerialization.data(withJSONObject: result) + let jsonString = String(data: jsonData, encoding: .utf8) + self.result?(jsonString) + self.result = nil + } + } + default: + let jsonData = try! JSONSerialization.data(withJSONObject: result) + let jsonString = String(data: jsonData, encoding: .utf8) + self.result?(jsonString) + self.result = nil + } + } } else { let jsonData = try! JSONSerialization.data(withJSONObject: result) let jsonString = String(data: jsonData, encoding: .utf8) self.result?(jsonString) self.result = nil } - }) + } } } diff --git a/lib/flutter_nfc_kit.dart b/lib/flutter_nfc_kit.dart index 50b1373..c051837 100644 --- a/lib/flutter_nfc_kit.dart +++ b/lib/flutter_nfc_kit.dart @@ -1,12 +1,11 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:io' show Platform; -import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:ndef/ndef.dart' as ndef; import 'package:ndef/ndef.dart' show TypeNameFormat; // for generated file +import 'package:ndef/utilities.dart'; import 'package:json_annotation/json_annotation.dart'; part 'flutter_nfc_kit.g.dart'; @@ -69,7 +68,7 @@ class NFCTag { /// The standard that the tag complies with (can be `unknown`) final String standard; - /// Tag ID + /// Tag ID (can be `unknown`) final String id; /// ATQA (Type A only, Android only) @@ -160,7 +159,7 @@ class NDEFRawRecord { final String type; /// type name format (see [ndef](https://pub.dev/packages/ndef) package for detail) - final ndef.TypeNameFormat typeNameFormat; + final TypeNameFormat typeNameFormat; NDEFRawRecord(this.identifier, this.payload, this.type, this.typeNameFormat); @@ -174,7 +173,7 @@ extension NDEFRecordConvert on ndef.NDEFRecord { /// Convert an [ndef.NDEFRecord] to encoded [NDEFRawRecord] NDEFRawRecord toRaw() { return NDEFRawRecord(id?.toHexString() ?? '', payload?.toHexString() ?? '', - type?.toHexString() ?? '', this.tnf); + type?.toHexString() ?? '', tnf); } /// Convert an [NDEFRawRecord] to decoded [ndef.NDEFRecord]. @@ -186,15 +185,111 @@ extension NDEFRecordConvert on ndef.NDEFRecord { } } +/// Request flag for ISO 15693 Tags +class Iso15693RequestFlags { + /// bit 1 + bool dualSubCarriers; + + /// bit 2 + bool highDataRate; + + /// bit 3 + bool inventory; + + /// bit 4 + bool protocolExtension; + + /// bit 5 + bool select; + + /// bit 6 + bool address; + + /// bit 7 + bool option; + + /// bit 8 + bool commandSpecificBit8; + + /// encode bits to one byte as specified in ISO15693-3 + int encode() { + var result = 0; + if (dualSubCarriers) { + result |= 0x01; + } + if (highDataRate) { + result |= 0x02; + } + if (inventory) { + result |= 0x04; + } + if (protocolExtension) { + result |= 0x08; + } + if (select) { + result |= 0x10; + } + if (address) { + result |= 0x20; + } + if (option) { + result |= 0x40; + } + if (commandSpecificBit8) { + result |= 0x80; + } + 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}); + + /// 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); + return f; + } +} + /// Main class of NFC Kit class FlutterNfcKit { /// Default timeout for [transceive] (in milliseconds) static const int TRANSCEIVE_TIMEOUT = 5 * 1000; /// Default timeout for [poll] (in milliseconds) - static const int POLL_TIIMEOUT = 20 * 1000; + static const int POLL_TIMEOUT = 20 * 1000; + + static const MethodChannel _channel = MethodChannel('flutter_nfc_kit/method'); + + static const EventChannel _tagEventChannel = + EventChannel('flutter_nfc_kit/event'); - static const MethodChannel _channel = const MethodChannel('flutter_nfc_kit'); + /// 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 { @@ -216,7 +311,7 @@ class FlutterNfcKit { /// On Android, set [androidPlatformSound] to control whether to play sound when a tag is polled, /// and set [androidCheckNDEF] to control whether check NDEF records on the tag. /// - /// The four boolean flags [readIso14443A], [readIso14443B], [readIso18092], [readIso15693] controls the NFC technology that would be tried. + /// The four boolean flags [readIso14443A], [readIso14443B], [readIso18092], [readIso15693] control the NFC technology that would be tried. /// On iOS, setting any of [readIso14443A] and [readIso14443B] will enable `iso14443` in `pollingOption`. /// /// On Web, all parameters are ignored except [timeout] and [probeWebUSBMagic]. @@ -224,10 +319,6 @@ 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. - /// - /// Caution: due to [bug in iOS CoreNFC](https://github.com/nfcim/flutter_nfc_kit/issues/23), [readIso18092] is disabled by default from 2.2.1. - /// If enabled, please ensure that `com.apple.developer.nfc.readersession.felica.systemcodes` is set in `Info.plist`, - /// or your NFC **WILL BE TOTALLY UNAVAILABLE BEFORE REBOOT**. static Future poll({ Duration? timeout, bool androidPlatformSound = true, @@ -252,7 +343,7 @@ class FlutterNfcKit { if (!androidCheckNDEF) technologies |= 0x80; if (!androidPlatformSound) technologies |= 0x100; final String data = await _channel.invokeMethod('poll', { - 'timeout': timeout?.inMilliseconds ?? POLL_TIIMEOUT, + 'timeout': timeout?.inMilliseconds ?? POLL_TIMEOUT, 'iosAlertMessage': iosAlertMessage, 'iosMultipleTagMessage': iosMultipleTagMessage, 'technologies': technologies, @@ -261,6 +352,14 @@ 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. + static Future iosRestartPolling() async => + await _channel.invokeMethod("restartPolling"); + /// Transceive data with the card / tag in the format of APDU (iso7816) or raw commands (other technologies). /// The [capdu] can be either of type Uint8List or hex string. /// Return value will be in the same type of [capdu]. @@ -325,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. /// @@ -344,11 +443,11 @@ 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 setIosAlertMessage(String message) async { - if (!kIsWeb && Platform.isIOS) { + if (!kIsWeb) { return await _channel.invokeMethod('setIosAlertMessage', message); } } @@ -360,59 +459,68 @@ class FlutterNfcKit { return await _channel.invokeMethod('makeNdefReadOnly'); } - /// Authenticate against a sector of MIFARE Classic tag. - /// + /// Authenticate against a sector of MIFARE Classic tag (Android only). + /// /// 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(T is String || T 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 block (16 bytes) from tag - /// + /// Read one unit of data (specified below) from: + /// * MIFARE Classic / Ultralight tag: one 16B block / page (Android only) + /// * ISO 15693 tag: one 4B block (iOS only) + /// /// 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. /// For MIFARE Ultralight tags, four consecutive pages will be read. /// Returns data in [Uint8List]. - static Future readBlock(int index) async { + static Future readBlock(int index, + {Iso15693RequestFlags? iso15693Flags, + bool iso15693ExtendedMode = false}) async { + var flags = iso15693Flags ?? Iso15693RequestFlags(); return await _channel.invokeMethod('readBlock', { - 'index': index - }); + 'index': index, + 'iso15693Flags': flags.encode(), + 'iso15693ExtendedMode': iso15693ExtendedMode, + }); } - /// Write one block (16B) / page (4B) to MIFARE Classic / Ultralight tag - /// + /// Write one unit of data (specified below) to: + /// * MIFARE Classic tag: one 16B block (Android only) + /// * MIFARE Ultralight tag: one 4B page (Android only) + /// * ISO 15693 tag: one 4B block (iOS only) + /// /// 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) async { - assert(T is String || T is Uint8List); + 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', { 'index': index, 'data': data, + 'iso15693Flags': flags.encode(), + 'iso15693ExtendedMode': iso15693ExtendedMode, }); } - /// Read one sector from MIFARE Classic tag - /// + /// Read one sector from MIFARE Classic tag (Android Only) + /// /// There must be a valid session when invoking. /// [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 readSector(int index) async { - return await _channel.invokeMethod('readSector', { - 'index': index - }); + return await _channel.invokeMethod('readSector', {'index': index}); } - } diff --git a/lib/flutter_nfc_kit_web.dart b/lib/flutter_nfc_kit_web.dart index af50c20..1fbc876 100644 --- a/lib/flutter_nfc_kit_web.dart +++ b/lib/flutter_nfc_kit_web.dart @@ -5,7 +5,6 @@ import 'dart:async'; // ignore: avoid_web_libraries_in_flutter import 'dart:html' as html show window; import 'dart:js_util'; -import 'dart:typed_data'; import 'package:convert/convert.dart'; import 'package:flutter/services.dart'; @@ -21,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, ); @@ -36,10 +35,11 @@ class FlutterNfcKitWeb { Future handleMethodCall(MethodCall call) async { switch (call.method) { case 'getNFCAvailability': - if (hasProperty(html.window.navigator, 'usb')) + if (hasProperty(html.window.navigator, 'usb')) { return 'available'; - else + } else { return 'not_supported'; + } case 'poll': int timeout = call.arguments["timeout"]; diff --git a/lib/webusb_interop.dart b/lib/webusb_interop.dart index 6fe94d1..a0a750d 100644 --- a/lib/webusb_interop.dart +++ b/lib/webusb_interop.dart @@ -3,16 +3,16 @@ /// Library that inter-ops with JavaScript on WebUSB APIs. /// /// Note: you should **NEVER use this library directly**, but instead use the [FlutterNfcKit] class in your project. -library webusb_interop; +library; import 'dart:convert'; import 'dart:js_util'; import 'dart:async'; import 'dart:typed_data'; +import 'dart:js_interop'; import 'package:convert/convert.dart'; import 'package:flutter/services.dart'; -import 'package:js/js.dart'; import 'package:logging/logging.dart'; final log = Logger('FlutterNFCKit:WebUSB'); @@ -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, @@ -56,26 +56,21 @@ class _USBControlTransferParameters { class WebUSB { static dynamic _device; static String customProbeData = ""; + static Function? onDisconnect; static bool _deviceAvailable() { return _device != null && getProperty(_device, 'opened'); } - static void _onDisconnect(event) { - _device = null; - log.info('device is disconnected from WebUSB API'); - } - static const USB_PROBE_MAGIC = '_NFC_IM_'; /// Try to poll a WebUSB device according to our protocol. static Future poll(int timeout, bool probeMagic) async { // request WebUSB device with custom classcode if (!_deviceAvailable()) { - var devicePromise = _USB.requestDevice(new _USBDeviceRequestOptions( - filters: [ - new _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())) @@ -83,7 +78,10 @@ class WebUSB { promiseToFuture(callMethod(device, 'claimInterface', [1]))) .timeout(Duration(milliseconds: timeout)); _device = device; - _USB.ondisconnect = allowInterop(_onDisconnect); + _USB.ondisconnect = () { + _device = null; + onDisconnect?.call(); + }.toJS; log.info("WebUSB device opened", _device); } on TimeoutException catch (_) { log.severe("Polling tag timeout"); @@ -98,7 +96,7 @@ class WebUSB { try { // PROBE request var promise = callMethod(_device, 'controlTransferIn', [ - new _USBControlTransferParameters( + _USBControlTransferParameters( requestType: 'vendor', recipient: 'interface', request: 0xff, @@ -147,7 +145,7 @@ class WebUSB { static Future _doTransceive(Uint8List capdu) async { // send a command (CMD) var promise = callMethod(_device, 'controlTransferOut', [ - new _USBControlTransferParameters( + _USBControlTransferParameters( requestType: 'vendor', recipient: 'interface', request: 0, @@ -159,7 +157,7 @@ class WebUSB { // wait for execution to finish (STAT) while (true) { promise = callMethod(_device, 'controlTransferIn', [ - new _USBControlTransferParameters( + _USBControlTransferParameters( requestType: 'vendor', recipient: 'interface', request: 2, @@ -184,7 +182,7 @@ class WebUSB { } // get the response (RESP) promise = callMethod(_device, 'controlTransferIn', [ - new _USBControlTransferParameters( + _USBControlTransferParameters( requestType: 'vendor', recipient: 'interface', request: 1, @@ -221,7 +219,7 @@ class WebUSB { throw PlatformException(code: "408", message: "Transceive timeout"); } on PlatformException catch (e) { log.severe("Transceive error", e); - throw e; + rethrow; } on Exception catch (e) { log.severe("Transceive error", e); throw PlatformException( diff --git a/pubspec.yaml b/pubspec.yaml index aa09310..12e21f4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,27 +1,27 @@ 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.3.3 +version: 3.6.0-rc.6 homepage: "https://github.com/nfcim/flutter_nfc_kit" environment: - sdk: ">=2.12.0 <4.0.0" - flutter: ">=2.0.0" + sdk: ">=3.5.0 <4.0.0" + flutter: ">=3.24.0" dependencies: flutter: sdk: flutter flutter_web_plugins: sdk: flutter - json_annotation: ^4.0.1 - ndef: ^0.3.1 - convert: ^3.0.1 - logging: ^1.0.2 - js: ^0.6.3 + json_annotation: ^4.8.1 + ndef: ^0.3.3 + convert: ^3.1.1 + logging: ^1.2.0 dev_dependencies: flutter_test: sdk: flutter - build_runner: ^2.1.0 + lints: ^5.0.0 + build_runner: ^2.4.9 json_serializable: ^6.7.1 flutter: