├── .github
└── workflows
│ ├── example-app.yml
│ └── publish.yaml
├── .gitignore
├── .metadata
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── WebUSB.md
├── analysis_options.yaml
├── android
├── .classpath
├── .gitignore
├── build.gradle
├── gradle.properties
├── gradle
│ └── wrapper
│ │ └── gradle-wrapper.properties
├── settings.gradle
└── src
│ └── main
│ ├── AndroidManifest.xml
│ └── kotlin
│ └── im
│ └── nfc
│ └── flutter_nfc_kit
│ ├── ByteUtils.kt
│ ├── FlutterNfcKitPlugin.kt
│ └── MifareUtils.kt
├── example
├── .gitignore
├── .metadata
├── .pubignore
├── android
│ ├── .gitignore
│ ├── app
│ │ ├── .classpath
│ │ ├── build.gradle
│ │ └── src
│ │ │ ├── debug
│ │ │ └── AndroidManifest.xml
│ │ │ ├── main
│ │ │ ├── AndroidManifest.xml
│ │ │ ├── kotlin
│ │ │ │ └── im
│ │ │ │ │ └── nfc
│ │ │ │ │ └── flutter_nfc_kit
│ │ │ │ │ └── example
│ │ │ │ │ └── MainActivity.kt
│ │ │ └── res
│ │ │ │ ├── drawable-v21
│ │ │ │ └── launch_background.xml
│ │ │ │ ├── drawable
│ │ │ │ └── launch_background.xml
│ │ │ │ ├── mipmap-hdpi
│ │ │ │ └── ic_launcher.png
│ │ │ │ ├── mipmap-mdpi
│ │ │ │ └── ic_launcher.png
│ │ │ │ ├── mipmap-xhdpi
│ │ │ │ └── ic_launcher.png
│ │ │ │ ├── mipmap-xxhdpi
│ │ │ │ └── ic_launcher.png
│ │ │ │ ├── mipmap-xxxhdpi
│ │ │ │ └── ic_launcher.png
│ │ │ │ ├── values-night
│ │ │ │ └── styles.xml
│ │ │ │ └── values
│ │ │ │ └── styles.xml
│ │ │ └── profile
│ │ │ └── AndroidManifest.xml
│ ├── build.gradle
│ ├── gradle.properties
│ ├── gradle
│ │ └── wrapper
│ │ │ └── gradle-wrapper.properties
│ ├── settings.gradle
│ └── settings_aar.gradle
├── example.md
├── ios
│ ├── .gitignore
│ ├── Flutter
│ │ ├── AppFrameworkInfo.plist
│ │ ├── Debug.xcconfig
│ │ └── Release.xcconfig
│ ├── Podfile
│ ├── Podfile.lock
│ ├── Runner.xcodeproj
│ │ ├── project.pbxproj
│ │ ├── project.xcworkspace
│ │ │ └── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ └── xcschemes
│ │ │ └── Runner.xcscheme
│ ├── Runner.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ └── Runner
│ │ ├── AppDelegate.swift
│ │ ├── Assets.xcassets
│ │ ├── AppIcon.appiconset
│ │ │ ├── Contents.json
│ │ │ ├── Icon-App-1024x1024@1x.png
│ │ │ ├── Icon-App-20x20@1x.png
│ │ │ ├── Icon-App-20x20@2x.png
│ │ │ ├── Icon-App-20x20@3x.png
│ │ │ ├── Icon-App-29x29@1x.png
│ │ │ ├── Icon-App-29x29@2x.png
│ │ │ ├── Icon-App-29x29@3x.png
│ │ │ ├── Icon-App-40x40@1x.png
│ │ │ ├── Icon-App-40x40@2x.png
│ │ │ ├── Icon-App-40x40@3x.png
│ │ │ ├── Icon-App-60x60@2x.png
│ │ │ ├── Icon-App-60x60@3x.png
│ │ │ ├── Icon-App-76x76@1x.png
│ │ │ ├── Icon-App-76x76@2x.png
│ │ │ └── Icon-App-83.5x83.5@2x.png
│ │ └── LaunchImage.imageset
│ │ │ ├── Contents.json
│ │ │ ├── LaunchImage.png
│ │ │ ├── LaunchImage@2x.png
│ │ │ ├── LaunchImage@3x.png
│ │ │ └── README.md
│ │ ├── Base.lproj
│ │ ├── LaunchScreen.storyboard
│ │ └── Main.storyboard
│ │ ├── Info.plist
│ │ ├── Runner-Bridging-Header.h
│ │ └── Runner.entitlements
├── lib
│ ├── main.dart
│ └── ndef_record
│ │ ├── raw_record_setting.dart
│ │ ├── text_record_setting.dart
│ │ └── uri_record_setting.dart
├── pubspec.lock
├── pubspec.yaml
└── web
│ ├── favicon.png
│ ├── icons
│ ├── Icon-192.png
│ └── Icon-512.png
│ ├── index.html
│ └── manifest.json
├── ios
├── .gitignore
├── flutter_nfc_kit.podspec
└── flutter_nfc_kit
│ ├── .gitignore
│ ├── Package.swift
│ └── Sources
│ └── flutter_nfc_kit
│ └── FlutterNfcKitPlugin.swift
├── lib
├── flutter_nfc_kit.dart
├── flutter_nfc_kit.g.dart
├── flutter_nfc_kit_web.dart
└── webusb_interop.dart
└── pubspec.yaml
/.github/workflows/example-app.yml:
--------------------------------------------------------------------------------
1 | name: Build Example App
2 |
3 | on:
4 | push:
5 | branches: [master, develop]
6 | pull_request:
7 | branches: [master, develop]
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 | if: ${{ !contains(github.event.head_commit.message, 'ci skip') }}
13 |
14 | strategy:
15 | fail-fast: false
16 | matrix:
17 | variant: [debug, release]
18 |
19 | steps:
20 | - uses: actions/checkout@v4
21 | - uses: actions/setup-java@v4
22 | with:
23 | distribution: 'temurin'
24 | java-version: '21'
25 | - uses: subosito/flutter-action@v2
26 | with:
27 | channel: 'stable'
28 | cache: true
29 | - run: dart pub get
30 | - run: dart format --output=none --set-exit-if-changed .
31 | - run: dart analyze
32 | - run: flutter pub get
33 | working-directory: example/
34 | #- run: flutter test
35 | - run: flutter build apk --${{ matrix.variant }} --verbose
36 | working-directory: example/
37 | - uses: actions/upload-artifact@v4
38 | with:
39 | name: example-apk-${{ matrix.variant }}
40 | path: |
41 | example/build/app/outputs/flutter-apk/app-${{ matrix.variant }}.apk
42 | example/build/reports/*
43 |
44 | build-ios:
45 | runs-on: macos-latest
46 | strategy:
47 | fail-fast: false
48 | matrix:
49 | variant: [debug, release]
50 |
51 | steps:
52 | - uses: actions/checkout@v4
53 | - uses: subosito/flutter-action@v2
54 | with:
55 | channel: 'stable'
56 | cache: true
57 | - run: flutter pub get
58 | working-directory: example/
59 | - run: flutter build ios --${{ matrix.variant }} --verbose --no-codesign
60 | working-directory: example/
61 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yaml:
--------------------------------------------------------------------------------
1 | name: Publish to pub.dev
2 |
3 | env:
4 | FLUTTER_VERSION: 3.24.5
5 |
6 | on:
7 | push:
8 | tags:
9 | - 'v[0-9]+.[0-9]+.[0-9]+*'
10 |
11 | jobs:
12 | publish:
13 | name: 'Publish to pub.dev'
14 | permissions:
15 | id-token: write
16 | runs-on: ubuntu-latest
17 | steps:
18 | - uses: actions/checkout@v4
19 | - uses: dart-lang/setup-dart@v1
20 | - name: Setup Flutter SDK
21 | uses: flutter-actions/setup-flutter@v3
22 | with:
23 | channel: stable
24 | version: ${{ env.FLUTTER_VERSION }}
25 | cache: true
26 | - name: Install dependencies
27 | run: flutter pub get
28 | - name: Publish - dry run
29 | run: flutter pub publish --dry-run
30 | - name: Publish to pub.dev
31 | run: flutter pub publish -f
32 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .dart_tool/
3 |
4 | .packages
5 | .pub/
6 |
7 | build/
8 |
9 | .idea
10 | .project
11 | .settings
12 |
13 | *.iml
14 |
15 | doc
16 | pubspec.lock
17 | !example/pubspec.lock
18 |
19 |
--------------------------------------------------------------------------------
/.metadata:
--------------------------------------------------------------------------------
1 | # This file tracks properties of this Flutter project.
2 | # Used by Flutter tool to assess capabilities and perform upgrades etc.
3 | #
4 | # This file should be version controlled and should not be manually edited.
5 |
6 | version:
7 | revision: 27321ebbad34b0a3fafe99fac037102196d655ff
8 | channel: stable
9 |
10 | project_type: plugin
11 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## 0.0.1
2 |
3 | * Initial release
4 | * Support reading metadata of cards of standard ISO 14443-4 Type A & Type B (NFC-A / NFC-B / Mifare Classic / Mifare Ultralight)
5 | * Support transceiving APDU with cards of standard ISO 7816
6 |
7 | ## 0.0.2
8 |
9 | * Support reading metadata of cards of standard ISO 18092 / JIS 6319 (NFC-F / Felica)
10 | * Support reading metadata of cards of standard ISO 15963 (NFC-V)
11 | * Support reading GUID of China ID Card (which is non-standard)
12 | * Add more documentation
13 | * Release with MIT License
14 |
15 | ## 0.0.3
16 |
17 | * Fix compilation errors of iOS plugin
18 |
19 | ## 0.0.4
20 |
21 | * Fix IsoDep unintentionally closing of the Android plugin
22 | * Fix incorrect standards description
23 |
24 | ## 0.0.5
25 |
26 | * Fix finish method blocking error
27 |
28 | ## 0.0.6
29 |
30 | * Avoid returning redundant result if user finish the read operation prematurely
31 |
32 | ## 0.0.7
33 |
34 | * Allow data transceive on lower layer (Android only)
35 |
36 | ## 1.0.0
37 |
38 | * Remove China ID Card support due to support of lower layer transceiving
39 | * Fix some racing problem on iOS
40 | * We are out-of beta and releasing on time!
41 |
42 | ## 1.0.1
43 |
44 | * Fix IllegalStateException & Add MethodResultWrapper (Thanks to @smlu)
45 |
46 | ## 1.0.2
47 |
48 | * Remove redundant code in Android plugin
49 | * Format dart code
50 |
51 | ## 1.1.0
52 |
53 | * Add NFC session alert/error message on iOS (Thanks to @smlu)
54 | * Support execution timeout as optional parameter on Android for `poll` and `transceive` (Thanks to @smlu)
55 | * Accept command in the type of hex string / `UInt8List` in `transceive` and return in the same type (Thanks to @smlu)
56 |
57 | ## 1.2.0
58 |
59 | * Add support for NFC 18902 on iOS
60 | * Add initial NDEF support (read only, no decoding)
61 | * Allow disabling platform sound on Android when polling card
62 |
63 | ## 2.0.0
64 |
65 | * Switch to [ndef](https://pub.dev/packages/ndef) for NDEF record encoding & decoding (breaking API change)
66 | * Support writing NDEF records
67 | * Add NDEF writing in example app
68 |
69 | ## 2.0.1
70 |
71 | * Fix compiling problem on iOS
72 |
73 | ## 2.0.2
74 |
75 | * Fix format of CHANGELOG.md
76 | * Format source code to pass static analysis
77 |
78 | ## 2.1.0
79 |
80 | * Update to latest version of `ndef` package
81 | * Update example app on writing NDEF records
82 |
83 | ## 2.2.0
84 |
85 | * Allow specifying needed NFC technologies in `poll` (fix [#15](https://github.com/nfcim/flutter_nfc_kit/issues/15))
86 |
87 | ## 2.2.1
88 |
89 | * Disable ISO 18092 in `poll` by default due to iOS CoreNFC bug (see [#23](https://github.com/nfcim/flutter_nfc_kit/issues/23))
90 | * Bump dependencies & fix some deprecation warnings
91 |
92 | ## 3.0.0
93 |
94 | * Upgrade to Flutter 2.0, add null-safety support for APIs
95 | * Bump dependencies (`ndef` to 0.3.1)
96 |
97 | ## 3.0.1
98 |
99 | * Remove errorous non-null assertion in `ndef.NDEFRecord.toRaw()` extension method & fix example app (#38)
100 |
101 | ## 3.0.2
102 |
103 | * Fix incorrect flags passed by `poll` method to Android NFC API (#42)
104 |
105 | ## 3.1.0
106 |
107 | * Fix inappropriate non-null type in fields of `NFCTag` (#43)
108 |
109 | ## 3.2.0
110 |
111 | * Add `makeNdefReadOnly` (#53, thanks to @timnew)
112 | * Avoid NFC API calls to block the main thread on Android (#54, thanks to @cyberbobs)
113 | * Bump dependencies of Android plugin and example App (esp. Kotlin 1.6.0)
114 | * Exclude example app in published version to reduce package size
115 |
116 | ## 3.3.0
117 |
118 | * Add Web support using own WebUSB protocol (see documentation for detail)
119 | * Bump dependencies (esp. Kotlin 1.6.21 and SDK 31) of Android plugin and example App to fix build error (#55)
120 | * Distinguish session canceled and session timeout (#58, #59, thanks to @timnew)
121 | * Minor error fixes (#61, #63, #64, #65, #71, #72, many of them come from @timnew)
122 |
123 | ## 3.3.1
124 |
125 | * Downgrade dependency `js` to 0.6.3 to maintain backward compatibility with Flutter 2 (#74)
126 |
127 | ## 3.3.2
128 |
129 | * Upgrade to Flutter 3, bump dependencies, change target SDK of Android to 33
130 | * Fix multiple issues (#88, #90, #102)
131 |
132 | ## 3.3.3
133 |
134 | * Fix build script of Android plugin and remove AGP version requirement (#110)
135 |
136 | ## 3.4.0
137 |
138 | * Add support for reading / write MIFARE Classic / Ultralight tags on Android (merged #82, partially fixes #82)
139 | * Add support for reading / write ISO 15693 tags on iOS (merged #117, partially fixes #68)
140 | * Fix compiling issues (#123)
141 | * Other minor fixes (#114, #115)
142 |
143 | ## 3.4.1
144 |
145 | **This version is *deprecated* due to a bug in Mifare tag handling. Please upgrade to 3.4.2.**
146 |
147 | * Fix & split examples to example/ dir
148 | * Publish examples to pub.dev
149 | * Support transceiving of raw ISO15693 commands on iOS
150 |
151 | ## 3.4.2
152 |
153 | * Fix polling error on Mifare tags (#126, #128, #129, #133)
154 |
155 | ## 3.5.0
156 |
157 | * Some FeliCa improvements by @shiwano:
158 | * Fix missing `id` field in FeliCa card reading on iOS (#140)
159 | * Set the IDm to the `id` and the PMm to the `manufacturer` on iOS (#140)
160 | * Add `iosRestartPolling` method by @rostopira (#151)
161 | * Fix type assertion in `authenticateSector` (fix #148)
162 | * Refine exception handling in Android plugin (fix #91 and #149)
163 | * Bump multiple dependencies:
164 | * Android plugin / example app: Java 17, AGP 7.4.2, Kotlin 1.9.23, minSdkVersion 26 (fix #127, #144, #145)
165 | * `js` library: 0.7.1
166 |
167 | ## 3.5.1
168 |
169 | * Fix multiple issues related to `authenticateSector` (#159):
170 | * Fix type checking assertions of arguments
171 | * Add missing call to `connect` in Android plugin
172 | * Add instruction on resolving `js` dependency conflict in README
173 |
174 | ## 3.5.2
175 |
176 | * Some MiFare Classic fixes by @knthm:
177 | * allow authentication of sector 0 (#157)
178 | * fix data type check in `writeBlock` (#161)
179 |
180 | ## 3.6.0
181 |
182 | * Requires Dart 3.6+ and Flutter 3.24+
183 | * Remove annoying dependency on `js` library, replace with `dart:js_interop`
184 | * Remove dependency on `dart:io`
185 | * Contributions on Android plugin from @knthm:
186 | * Dedicated handler thread for IO operations (#167)
187 | * More elegant exception handling (#169)
188 | * Bump tool versions & dependencies of Android plugin and example app:
189 | * Related issues / PRs: #179 #184, #186, #187
190 | * Now requiring Java 17, Gradle 8.9, MinSDKVer 26, AGP 8.7, Kotlin 2.1.0
191 | * Add Swift package manager support for iOS plugin, bump dependencies
192 | * Fix WebUSB interop on Web, add onDisconnect callback
193 | * Add support for foreground polling on Android (#16, #179)
194 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Thanks for your interest in contributing to `flutter_nfc_kit`!
4 |
5 | ## GitHub Workflow
6 |
7 | We have two main branches:
8 |
9 | * `develop` branch (default) is for actively development and RC releases.
10 | * `master` branch is for stable releases.
11 |
12 | Both branches are protected. Pull requests are needed for any changes.
13 |
14 | Please always make your changes on `develop` branch, and submit your PR against it.
15 | If there are any merge conflicts, please resolve them by rebasing your commits.
16 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 nfc.im
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Flutter NFC Kit
2 |
3 | [](https://pub.dev/packages/flutter_nfc_kit)
4 | 
5 |
6 | Yet another plugin to provide NFC functionality on Android, iOS and browsers (by WebUSB, see below).
7 |
8 | This plugin's functionalities include:
9 |
10 | * read metadata and read & write NDEF records of tags / cards complying with:
11 | * ISO 14443 Type A & Type B (NFC-A / NFC-B / MIFARE Classic / MIFARE Plus / MIFARE Ultralight / MIFARE DESFire)
12 | * ISO 18092 (NFC-F / FeliCa)
13 | * ISO 15963 (NFC-V)
14 | * R/W block / page / sector level data of tags complying with:
15 | * MIFARE Classic / Ultralight (Android only)
16 | * ISO 15693 (iOS only)
17 | * transceive raw commands with tags / cards complying with:
18 | * ISO 7816 Smart Cards (layer 4, in APDUs)
19 | * other device-supported technologies (layer 3, in raw commands, see documentation for platform-specific supportability)
20 |
21 | Note that due to API limitations, not all operations are supported on all platforms.
22 | **You are welcome to submit PRs to add support for any standard-specific operations.**
23 |
24 | This library uses [ndef](https://pub.dev/packages/ndef) for NDEF record encoding & decoding.
25 |
26 | ## Contributing
27 |
28 | Please refer to [CONTRIBUTING.md](CONTRIBUTING.md) for more information.
29 |
30 | ## Setup
31 |
32 | ### Android
33 |
34 | We have the following minimum version requirements for Android plugin:
35 |
36 | * Java 17
37 | * Gradle 8.9
38 | * Android SDK 26 (you must set corresponding `jvmTarget` in you app's `build.gradle`)
39 | * Android Gradle Plugin 8.7
40 |
41 | To use this plugin on Android, you also need to:
42 |
43 | * Add [android.permission.NFC](https://developer.android.com/reference/android/Manifest.permission.html#NFC) to your `AndroidManifest.xml`.
44 |
45 | ### iOS
46 |
47 | This plugin now supports Swift package manager, and requires iOS 13+.
48 |
49 | * 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.
50 | * Add [NFCReaderUsageDescription](https://developer.apple.com/documentation/bundleresources/information_property_list/nfcreaderusagedescription) to your `Info.plist`.
51 | * 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).
52 | * Open Runner.xcworkspace with Xcode and navigate to project settings then the tab _Signing & Capabilities._
53 | * Select the Runner in targets in left sidebar then press the "+ Capability" in the left upper corner and choose _Near Field Communication Tag Reading._
54 |
55 | ### Web
56 |
57 | The web version of this plugin **does not actually support NFC** in browsers, but uses a specific [WebUSB protocol](https://github.com/nfcim/flutter_nfc_kit/blob/master/WebUSB.md), so that Flutter programs can communicate with dual-interface (NFC / USB) devices in a platform-independent way.
58 |
59 | Make sure you understand the statement above and the protocol before using this plugin.
60 |
61 | ## Usage
62 |
63 | We provide [simple code example](example/example.md) and a [example application](example/lib).
64 |
65 | Refer to the [documentation](https://pub.dev/documentation/flutter_nfc_kit/) for more information.
66 |
67 | ### Error codes
68 |
69 | 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.
70 |
71 | ### Operation Mode
72 |
73 | 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.
74 |
--------------------------------------------------------------------------------
/WebUSB.md:
--------------------------------------------------------------------------------
1 | # WebUSB Protocol
2 |
3 | ## Overview
4 |
5 | Since NFC is inaccessible on the web, applications targeted on dual interfaces (NFC and USB)
6 | may use the following protocol to communicate with the WebUSB on Chromium-based web browsers.
7 |
8 | The communication is based on the Control Transfer. The interface with index 1 is used.
9 |
10 | Note: you **need to implement this protocol on your own USB device** before communicating with it using the web version of FlutterNfcKit.
11 |
12 | Currently, the following devices adopt this protocol:
13 |
14 | * [CanoKey](https://www.canokeys.org/)
15 |
16 | ## Messages
17 |
18 | Basically, the messages on the WebUSB interface are APDU commands.
19 | To transceive a pair of APDU commands, two phases are required:
20 |
21 | 1. Send a command APDU
22 | 2. Get the response APDU
23 |
24 | Each type of message is a vendor-specific request, defined as:
25 |
26 | | bRequest | Value |
27 | | -------- | ----- |
28 | | CMD | 00h |
29 | | RESP | 01h |
30 | | STAT | 02h |
31 | | PROBE | FFh |
32 |
33 | 1. Probe device
34 |
35 | The following control pipe request is used to probe whether the device supports this protocol.
36 |
37 | | bmRequestType | bRequest | wValue | wIndex | wLength | Data |
38 | | ------------- | -------- | ------ | ------ | ------- | ---- |
39 | | 11000001B | PROBE | 0000h | 1 | 0 | N/A |
40 |
41 | The response data **MUST** begin with magic bytes `0x5f4e46435f494d5f` (`_NFC_IM_`) in order to be recognized.
42 | The remaining bytes can be used as custom information provided by the device.
43 |
44 | 2. Send command APDU
45 |
46 | The following control pipe request is used to send a command APDU.
47 |
48 | | bmRequestType | bRequest | wValue | wIndex | wLength | Data |
49 | | ------------- | -------- | ------ | ------ | -------------- | ----- |
50 | | 01000001B | CMD | 0000h | 1 | length of data | bytes |
51 |
52 | 3. Get execution status
53 |
54 | The following control pipe request is used to get the status of the device.
55 |
56 | | bmRequestType | bRequest | wValue | wIndex | wLength | Data |
57 | | ------------- | -------- | ------ | ------ | ------- | ---- |
58 | | 11000001B | STAT | 0000h | 1 | 0 | N/A |
59 |
60 | The response data is 1-byte long, `0x01` for in progress and `0x00` for finishing processing,
61 | and you can fetch the result using `RESP` command, and other values for invalid states.
62 |
63 | If the command is still under processing, the response will be empty.
64 |
65 | 4. Get response APDU
66 |
67 | The following control pipe request is used to get the response APDU.
68 |
69 | | bmRequestType | bRequest | wValue | wIndex | wLength | Data |
70 | | ------------- | -------- | ------ | ------ | ------- | ---- |
71 | | 11000001B | RESP | 0000h | 1 | 0 | N/A |
72 |
73 | The device will send the response no more than 1500 bytes.
74 |
--------------------------------------------------------------------------------
/analysis_options.yaml:
--------------------------------------------------------------------------------
1 | include: package:lints/recommended.yaml
2 |
3 | linter:
4 | rules:
5 | constant_identifier_names: false
6 |
--------------------------------------------------------------------------------
/android/.classpath:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/android/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/workspace.xml
5 | /.idea/libraries
6 | .DS_Store
7 | /build
8 | /captures
9 |
--------------------------------------------------------------------------------
/android/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.library'
3 | id 'org.jetbrains.kotlin.android'
4 | }
5 |
6 | allprojects {
7 | repositories {
8 | gradlePluginPortal()
9 | google()
10 | mavenCentral()
11 | }
12 | }
13 |
14 | group 'im.nfc.flutter_nfc_kit'
15 |
16 | android {
17 |
18 | if (project.android.hasProperty("namespace")) {
19 | namespace 'im.nfc.flutter_nfc_kit'
20 | }
21 |
22 | compileSdk 35
23 |
24 | compileOptions {
25 | sourceCompatibility JavaVersion.VERSION_1_8
26 | targetCompatibility JavaVersion.VERSION_1_8
27 | }
28 |
29 | kotlinOptions {
30 | jvmTarget = JavaVersion.VERSION_1_8
31 | }
32 |
33 | sourceSets {
34 | main.java.srcDirs += 'src/main/kotlin'
35 | }
36 | defaultConfig {
37 | minSdkVersion 26
38 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
39 | }
40 | lintOptions {
41 | disable 'InvalidPackage'
42 | }
43 | }
44 |
45 | dependencies {
46 | }
47 |
--------------------------------------------------------------------------------
/android/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx1536M
2 | android.enableJetifier=true
3 | AGPVersion=8.7.3
4 | KotlinVersion=2.1.0
5 |
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Fri Sep 08 22:01:14 CST 2023
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/android/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | gradlePluginPortal()
4 | google()
5 | mavenCentral()
6 | }
7 | plugins {
8 | id 'com.android.library' version "${AGPVersion}"
9 | id 'org.jetbrains.kotlin.android' version "${KotlinVersion}"
10 | }
11 | }
12 |
13 | rootProject.name = 'flutter_nfc_kit'
14 |
--------------------------------------------------------------------------------
/android/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/android/src/main/kotlin/im/nfc/flutter_nfc_kit/ByteUtils.kt:
--------------------------------------------------------------------------------
1 | package im.nfc.flutter_nfc_kit
2 |
3 | import im.nfc.flutter_nfc_kit.ByteUtils.hexToBytes
4 | import im.nfc.flutter_nfc_kit.ByteUtils.toHexString
5 |
6 |
7 | object ByteUtils {
8 | private const val HEX_CHARS = "0123456789ABCDEF"
9 | private val HEX_CHARS_ARRAY = HEX_CHARS.toCharArray()
10 |
11 | fun String.hexToBytes(): ByteArray {
12 | if (length % 2 == 1) throw IllegalArgumentException()
13 |
14 | val result = ByteArray(length / 2)
15 |
16 | val str = this.uppercase()
17 | for (i in 0 until length step 2) {
18 | val firstIndex = HEX_CHARS.indexOf(str[i])
19 | val secondIndex = HEX_CHARS.indexOf(str[i + 1])
20 | require(!(firstIndex == -1 || secondIndex == -1))
21 | val octet = (firstIndex shl 4) or secondIndex
22 | result[i shr 1] = octet.toByte()
23 | }
24 |
25 | return result
26 | }
27 |
28 | fun ByteArray.toHexString(): String {
29 | val result = StringBuffer()
30 | forEach {
31 | result.append(it.toHexString())
32 | }
33 | return result.toString()
34 | }
35 |
36 | fun Byte.toHexString(): String {
37 | val octet = this.toInt()
38 | val firstIndex = (octet and 0xF0) ushr 4
39 | val secondIndex = octet and 0x0F
40 | return "${HEX_CHARS_ARRAY[firstIndex]}${HEX_CHARS_ARRAY[secondIndex]}"
41 | }
42 |
43 | fun canonicalizeData(data: Any): Pair {
44 | val bytes = when (data) {
45 | is String -> data.hexToBytes()
46 | else -> data as ByteArray
47 | }
48 | val hex = when (data) {
49 | is ByteArray -> data.toHexString()
50 | else -> data as String
51 | }
52 | return Pair(bytes, hex)
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/android/src/main/kotlin/im/nfc/flutter_nfc_kit/FlutterNfcKitPlugin.kt:
--------------------------------------------------------------------------------
1 | package im.nfc.flutter_nfc_kit
2 |
3 | import android.app.Activity
4 | import android.nfc.FormatException
5 | import android.nfc.NdefMessage
6 | import android.nfc.NdefRecord
7 | import android.nfc.NfcAdapter
8 | import android.nfc.NfcAdapter.*
9 | import android.nfc.Tag
10 | import android.nfc.tech.*
11 | import android.os.Handler
12 | import android.os.HandlerThread
13 | import android.os.Looper
14 | import im.nfc.flutter_nfc_kit.ByteUtils.canonicalizeData
15 | import im.nfc.flutter_nfc_kit.ByteUtils.hexToBytes
16 | import im.nfc.flutter_nfc_kit.ByteUtils.toHexString
17 | import im.nfc.flutter_nfc_kit.MifareUtils.readBlock
18 | import im.nfc.flutter_nfc_kit.MifareUtils.readSector
19 | import im.nfc.flutter_nfc_kit.MifareUtils.writeBlock
20 | import io.flutter.Log
21 | import io.flutter.embedding.engine.plugins.FlutterPlugin
22 | import io.flutter.embedding.engine.plugins.activity.ActivityAware
23 | import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
24 | import io.flutter.plugin.common.MethodCall
25 | import io.flutter.plugin.common.MethodChannel
26 | import io.flutter.plugin.common.MethodChannel.MethodCallHandler
27 | import io.flutter.plugin.common.MethodChannel.Result
28 | import io.flutter.plugin.common.EventChannel
29 | import io.flutter.plugin.common.EventChannel.EventSink
30 | import io.flutter.plugin.common.EventChannel.StreamHandler
31 | import org.json.JSONArray
32 | import org.json.JSONObject
33 | import java.io.IOException
34 | import java.lang.ref.WeakReference
35 | import java.lang.reflect.InvocationTargetException
36 | import java.util.*
37 | import kotlin.concurrent.schedule
38 |
39 |
40 | class FlutterNfcKitPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
41 |
42 | companion object {
43 | private val TAG = FlutterNfcKitPlugin::class.java.name
44 | private var activity: WeakReference = WeakReference(null)
45 | private var pollingTimeoutTask: TimerTask? = null
46 | private var tagTechnology: TagTechnology? = null
47 | private var ndefTechnology: Ndef? = null
48 | private var mifareInfo: MifareInfo? = null
49 |
50 | private lateinit var nfcHandlerThread: HandlerThread
51 | private lateinit var nfcHandler: Handler
52 | private lateinit var methodChannel: MethodChannel
53 | private lateinit var eventChannel: EventChannel
54 | private var eventSink: EventSink? = null
55 |
56 | public fun handleTag(tag: Tag) {
57 | val result = parseTag(tag)
58 | Handler(Looper.getMainLooper()).post {
59 | eventSink?.success(result)
60 | }
61 | }
62 |
63 | private fun TagTechnology.transceive(data: ByteArray, timeout: Int?): ByteArray {
64 | if (timeout != null) {
65 | try {
66 | val timeoutMethod = this.javaClass.getMethod("setTimeout", Int::class.java)
67 | timeoutMethod.invoke(this, timeout)
68 | } catch (_: Throwable) {}
69 | }
70 | val transceiveMethod = this.javaClass.getMethod("transceive", ByteArray::class.java)
71 | return transceiveMethod.invoke(this, data) as ByteArray
72 | }
73 |
74 | private fun runOnNfcThread(result: Result, desc: String, fn: () -> Unit) {
75 | val handledFn = Runnable {
76 | try {
77 | fn()
78 | } catch (ex: Exception) {
79 | Log.e(TAG, "$desc error", ex)
80 | val excMessage = ex.localizedMessage
81 | when (ex) {
82 | is IOException -> result?.error("500", "Communication error", excMessage)
83 | is SecurityException -> result?.error("503", "Tag already removed", excMessage)
84 | is FormatException -> result?.error("400", "NDEF format error", excMessage)
85 | is InvocationTargetException -> result?.error("500", "Communication error", excMessage)
86 | is IllegalArgumentException -> result?.error("400", "Command format error", excMessage)
87 | is NoSuchMethodException -> result?.error("405", "Transceive not supported for this type of card", excMessage)
88 | else -> result?.error("500", "Unhandled error", excMessage)
89 | }
90 | }
91 | }
92 | if (!nfcHandler.post(handledFn)) {
93 | result.error("500", "Failed to post job to NFC Handler thread.", null)
94 | }
95 | }
96 |
97 | private fun parseTag(tag: Tag): String {
98 | // common fields
99 | val type: String
100 | val id = tag.id.toHexString()
101 | val standard: String
102 | // ISO 14443 Type A
103 | var atqa = ""
104 | var sak = ""
105 | // ISO 14443 Type B
106 | var protocolInfo = ""
107 | var applicationData = ""
108 | // ISO 7816
109 | var historicalBytes = ""
110 | var hiLayerResponse = ""
111 | // NFC-F / Felica
112 | var manufacturer = ""
113 | var systemCode = ""
114 | // NFC-V
115 | var dsfId = ""
116 | // NDEF
117 | var ndefAvailable = false
118 | var ndefWritable = false
119 | var ndefCanMakeReadOnly = false
120 | var ndefCapacity = 0
121 | var ndefType = ""
122 |
123 | if (tag.techList.contains(NfcA::class.java.name)) {
124 | val aTag = NfcA.get(tag)
125 | atqa = aTag.atqa.toHexString()
126 | sak = byteArrayOf(aTag.sak.toByte()).toHexString()
127 | tagTechnology = aTag
128 | when {
129 | tag.techList.contains(IsoDep::class.java.name) -> {
130 | standard = "ISO 14443-4 (Type A)"
131 | type = "iso7816"
132 | val isoDep = IsoDep.get(tag)
133 | tagTechnology = isoDep
134 | historicalBytes = isoDep.historicalBytes.toHexString()
135 | }
136 | tag.techList.contains(MifareClassic::class.java.name) -> {
137 | standard = "ISO 14443-3 (Type A)"
138 | type = "mifare_classic"
139 | with(MifareClassic.get(tag)) {
140 | tagTechnology = this
141 | mifareInfo = MifareInfo(
142 | this.type,
143 | size,
144 | MifareClassic.BLOCK_SIZE,
145 | blockCount,
146 | sectorCount
147 | )
148 | }
149 | }
150 | tag.techList.contains(MifareUltralight::class.java.name) -> {
151 | standard = "ISO 14443-3 (Type A)"
152 | type = "mifare_ultralight"
153 | with(MifareUltralight.get(tag)) {
154 | tagTechnology = this
155 | mifareInfo = MifareInfo.fromUltralight(this.type)
156 | }
157 | }
158 | else -> {
159 | standard = "ISO 14443-3 (Type A)"
160 | type = "unknown"
161 | }
162 | }
163 | } else if (tag.techList.contains(NfcB::class.java.name)) {
164 | val bTag = NfcB.get(tag)
165 | protocolInfo = bTag.protocolInfo.toHexString()
166 | applicationData = bTag.applicationData.toHexString()
167 | if (tag.techList.contains(IsoDep::class.java.name)) {
168 | type = "iso7816"
169 | standard = "ISO 14443-4 (Type B)"
170 | val isoDep = IsoDep.get(tag)
171 | tagTechnology = isoDep
172 | hiLayerResponse = isoDep.hiLayerResponse.toHexString()
173 | } else {
174 | type = "unknown"
175 | standard = "ISO 14443-3 (Type B)"
176 | tagTechnology = bTag
177 | }
178 | } else if (tag.techList.contains(NfcF::class.java.name)) {
179 | standard = "ISO 18092 (FeliCa)"
180 | type = "iso18092"
181 | val fTag = NfcF.get(tag)
182 | manufacturer = fTag.manufacturer.toHexString()
183 | systemCode = fTag.systemCode.toHexString()
184 | tagTechnology = fTag
185 | } else if (tag.techList.contains(NfcV::class.java.name)) {
186 | standard = "ISO 15693"
187 | type = "iso15693"
188 | val vTag = NfcV.get(tag)
189 | dsfId = vTag.dsfId.toHexString()
190 | tagTechnology = vTag
191 | } else {
192 | type = "unknown"
193 | standard = "unknown"
194 | }
195 |
196 | // detect ndef
197 | if (tag.techList.contains(Ndef::class.java.name)) {
198 | val ndefTag = Ndef.get(tag)
199 | ndefTechnology = ndefTag
200 | ndefAvailable = true
201 | ndefType = ndefTag.type
202 | ndefWritable = ndefTag.isWritable
203 | ndefCanMakeReadOnly = ndefTag.canMakeReadOnly()
204 | ndefCapacity = ndefTag.maxSize
205 | }
206 |
207 | val jsonResult = JSONObject(mapOf(
208 | "type" to type,
209 | "id" to id,
210 | "standard" to standard,
211 | "atqa" to atqa,
212 | "sak" to sak,
213 | "historicalBytes" to historicalBytes,
214 | "protocolInfo" to protocolInfo,
215 | "applicationData" to applicationData,
216 | "hiLayerResponse" to hiLayerResponse,
217 | "manufacturer" to manufacturer,
218 | "systemCode" to systemCode,
219 | "dsfId" to dsfId,
220 | "ndefAvailable" to ndefAvailable,
221 | "ndefType" to ndefType,
222 | "ndefWritable" to ndefWritable,
223 | "ndefCanMakeReadOnly" to ndefCanMakeReadOnly,
224 | "ndefCapacity" to ndefCapacity,
225 | ))
226 |
227 | if (mifareInfo != null) {
228 | with(mifareInfo!!) {
229 | jsonResult.put("mifareInfo", JSONObject(mapOf(
230 | "type" to typeStr,
231 | "size" to size,
232 | "blockSize" to blockSize,
233 | "blockCount" to blockCount,
234 | "sectorCount" to sectorCount
235 | )))
236 | }
237 | }
238 |
239 | return jsonResult.toString()
240 | }
241 | }
242 |
243 | override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
244 | nfcHandlerThread = HandlerThread("NfcHandlerThread")
245 | nfcHandlerThread.start()
246 | nfcHandler = Handler(nfcHandlerThread.looper)
247 |
248 | methodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "flutter_nfc_kit/method")
249 | methodChannel.setMethodCallHandler(this)
250 |
251 | eventChannel = EventChannel(flutterPluginBinding.binaryMessenger, "flutter_nfc_kit/event")
252 | eventChannel.setStreamHandler(object : EventChannel.StreamHandler {
253 | override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
254 | if (events != null) {
255 | eventSink = events
256 | }
257 | }
258 |
259 | override fun onCancel(arguments: Any?) {
260 | // No need to do anything here
261 | }
262 | })
263 | }
264 |
265 | override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
266 | methodChannel.setMethodCallHandler(null)
267 | eventChannel.setStreamHandler(null)
268 | nfcHandlerThread.quitSafely()
269 | }
270 |
271 | override fun onMethodCall(call: MethodCall, result: Result) {
272 | handleMethodCall(call, MethodResultWrapper(result))
273 | }
274 |
275 | private fun handleMethodCall(call: MethodCall, result: Result) {
276 |
277 | if (activity.get() == null) {
278 | result.error("500", "Cannot call method when not attached to activity", null)
279 | return
280 | }
281 |
282 | val nfcAdapter = getDefaultAdapter(activity.get())
283 |
284 | if (nfcAdapter?.isEnabled != true && call.method != "getNFCAvailability") {
285 | result.error("404", "NFC not available", null)
286 | return
287 | }
288 |
289 | val ensureNDEF = {
290 | if (ndefTechnology == null) {
291 | if (tagTechnology == null) {
292 | result.error("406", "No tag polled", null)
293 | } else {
294 | result.error("405", "NDEF not supported on current tag", null)
295 | }
296 | false
297 | } else true
298 | }
299 |
300 | val switchTechnology = { target: TagTechnology, other: TagTechnology? ->
301 | if (!target.isConnected) {
302 | // close previously connected technology
303 | if (other !== null && other.isConnected) {
304 | other.close()
305 | }
306 | target.connect()
307 | }
308 | }
309 |
310 | when (call.method) {
311 |
312 | "getNFCAvailability" -> {
313 | when {
314 | nfcAdapter == null -> result.success("not_supported")
315 | nfcAdapter.isEnabled -> result.success("available")
316 | else -> result.success("disabled")
317 | }
318 | }
319 |
320 | "poll" -> {
321 | val timeout = call.argument("timeout")!!
322 | // technology and option bits are set in Dart code
323 | val technologies = call.argument("technologies")!!
324 | runOnNfcThread(result, "Poll") {
325 | pollTag(nfcAdapter, result, timeout, technologies)
326 | }
327 | }
328 |
329 | "finish" -> {
330 | pollingTimeoutTask?.cancel()
331 | runOnNfcThread(result, "Close tag") {
332 | val tagTech = tagTechnology
333 | if (tagTech != null && tagTech.isConnected) {
334 | tagTech.close()
335 | }
336 | val ndefTech = ndefTechnology
337 | if (ndefTech != null && ndefTech.isConnected) {
338 | ndefTech.close()
339 | }
340 | if (activity.get() != null) {
341 | nfcAdapter.disableReaderMode(activity.get())
342 | }
343 | result.success("")
344 | }
345 | }
346 |
347 | "transceive" -> {
348 | val tagTech = tagTechnology
349 | val data = call.argument("data")
350 | if (data == null || (data !is String && data !is ByteArray)) {
351 | result.error("400", "Bad argument", null)
352 | return
353 | }
354 | if (tagTech == null) {
355 | result.error("406", "No tag polled", null)
356 | return
357 | }
358 | val (sendingBytes, sendingHex) = canonicalizeData(data)
359 |
360 | runOnNfcThread(result, "Transceive: $sendingHex") {
361 | switchTechnology(tagTech, ndefTechnology)
362 | val timeout = call.argument("timeout")
363 | val resp = tagTech.transceive(sendingBytes, timeout)
364 | when (data) {
365 | is String -> result.success(resp.toHexString())
366 | else -> result.success(resp)
367 | }
368 | }
369 | }
370 |
371 | /// NDEF-related methods below
372 | "readNDEF" -> {
373 | if (!ensureNDEF()) return
374 | val ndef = ndefTechnology!!
375 | runOnNfcThread(result, "Read NDEF") {
376 | switchTechnology(ndef, tagTechnology)
377 | // read NDEF message
378 | val message: NdefMessage? = if (call.argument("cached")!!) {
379 | ndef.cachedNdefMessage
380 | } else {
381 | ndef.ndefMessage
382 | }
383 | val parsedMessages = mutableListOf