diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..d3ccf41
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,107 @@
+# Suppress: EC112
+# top-most EditorConfig file
+root = true
+
+# Don't use tabs for indentation.
+[*]
+indent_style = space
+# (Please don't specify an indent_size here; that has too many unintended consequences.)
+
+# Code files
+[*.{cs,csx,vb,vbx}]
+indent_size = 4
+
+# Xml project files
+[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}]
+indent_size = 2
+
+# Xml config files
+[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}]
+indent_size = 2
+
+# JSON files
+[*.json]
+indent_size = 2
+
+# Dotnet code style settings:
+[*.{cs,vb}]
+# Sort using and Import directives with System.* appearing first
+dotnet_sort_system_directives_first = false
+# Avoid "this." and "Me." if not necessary
+dotnet_style_qualification_for_field = false:suggestion
+dotnet_style_qualification_for_property = false:suggestion
+dotnet_style_qualification_for_method = false:suggestion
+dotnet_style_qualification_for_event = false:suggestion
+
+# Use language keywords instead of framework type names for type references
+dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
+dotnet_style_predefined_type_for_member_access = true:suggestion
+
+# Suggest more modern language features when available
+dotnet_style_object_initializer = true:suggestion
+dotnet_style_collection_initializer = true:suggestion
+dotnet_style_coalesce_expression = true:suggestion
+dotnet_style_null_propagation = true:suggestion
+dotnet_style_explicit_tuple_names = true:suggestion
+
+# Naming Conventions:
+# Pascal Casing
+dotnet_naming_symbols.method_and_property_symbols.applicable_kinds = method,property,enum
+dotnet_naming_symbols.method_and_property_symbols.applicable_accessibilities = *
+dotnet_naming_style.pascal_case_style.capitalization = pascal_case
+
+dotnet_naming_rule.methods_and_properties_must_be_pascal_case.severity = warning
+dotnet_naming_rule.methods_and_properties_must_be_pascal_case.symbols = method_and_property_symbols
+dotnet_naming_rule.methods_and_properties_must_be_pascal_case.style = pascal_case_style
+
+# Non-public members must be lower-case
+#dotnet_naming_symbols.non_public_symbols.applicable_kinds = property,event,delegate
+#dotnet_naming_symbols.non_public_symbols.applicable_accessibilities = private
+#dotnet_naming_style.all_lower_case_style.capitalization = camel_case
+
+#dotnet_naming_rule.non_public_members_must_be_lower_case.severity = warning
+#dotnet_naming_rule.non_public_members_must_be_lower_case.symbols = non_public_symbols
+#dotnet_naming_rule.non_public_members_must_be_lower_case.style = all_lower_case_style
+
+# Private fields must start with an underscore
+dotnet_naming_symbols.private_fields.applicable_kinds = field
+dotnet_naming_symbols.private_fields.applicable_accessibilities = private
+dotnet_naming_style.prefix_underscore.capitalization = camel_case
+dotnet_naming_style.prefix_underscore.required_prefix = _
+
+dotnet_naming_rule.private_members_with_underscore.severity = warning
+dotnet_naming_rule.private_members_with_underscore.symbols = private_fields
+dotnet_naming_rule.private_members_with_underscore.style = prefix_underscore
+
+# CSharp code style settings:
+[*.cs]
+# Do not prefer "var" everywhere
+csharp_style_var_for_built_in_types = true:suggestion
+csharp_style_var_when_type_is_apparent = true:suggestion
+csharp_style_var_elsewhere = true:suggestion
+
+# Prefer method-like constructs to have a block body
+csharp_style_expression_bodied_methods = true:suggestion
+csharp_style_expression_bodied_constructors = true:none
+csharp_style_expression_bodied_operators = true:none
+
+# Prefer property-like constructs to have an expression-body
+csharp_style_expression_bodied_properties = true:suggestion
+csharp_style_expression_bodied_indexers = true:none
+csharp_style_expression_bodied_accessors = true:none
+
+# Suggest more modern language features when available
+csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
+csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
+csharp_style_inlined_variable_declaration = true:suggestion
+csharp_style_throw_expression = true:suggestion
+csharp_style_conditional_delegate_call = true:suggestion
+
+# Newline settings
+csharp_new_line_before_open_brace = all
+csharp_new_line_before_else = true
+csharp_new_line_before_catch = true
+csharp_new_line_before_finally = true
+csharp_new_line_before_members_in_object_initializers = true
+csharp_new_line_before_members_in_anonymous_types = true
+
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e57e422
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,341 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
+
+# User-specific files
+*.rsuser
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+
+# Visual Studio 2015/2017 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUNIT
+*.VisualState.xml
+TestResult.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# Benchmark Results
+BenchmarkDotNet.Artifacts/
+
+# .NET Core
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_h.h
+*.ilk
+*.meta
+*.obj
+*.iobj
+*.pch
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*_wpftmp.csproj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# Visual Studio Trace Files
+*.e2e
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# JustCode is a .NET coding add-in
+.JustCode
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# Note: Comment the next line if you want to checkin your web deploy settings,
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# The packages folder can be ignored because of Package Restore
+**/[Pp]ackages/*
+# except build/, which is used as an MSBuild target.
+!**/[Pp]ackages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/[Pp]ackages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+*.appx
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!?*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+# ASP.NET Core default setup: bower directory is configured as wwwroot/lib/ and bower restore is true
+**/wwwroot/lib/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+ServiceFabricBackup/
+*.rptproj.bak
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+*.rptproj.rsuser
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# JetBrains Rider
+.idea/
+*.sln.iml
+
+# CodeRush personal settings
+.cr/personal
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Tabs Studio
+*.tss
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
+
+# OpenCover UI analysis results
+OpenCover/
+
+# Azure Stream Analytics local run output
+ASALocalRun/
+
+# MSBuild Binary and Structured Log
+*.binlog
+
+# NVidia Nsight GPU debugger configuration file
+*.nvuser
+
+# MFractors (Xamarin productivity tool) working folder
+.mfractor/
+
+# Local History for Visual Studio
+.localhistory/
+
+# BeatPulse healthcheck temp database
+healthchecksdb
diff --git a/src/Plugin.NFC/Android/CrossNFC.android.cs b/src/Plugin.NFC/Android/CrossNFC.android.cs
new file mode 100644
index 0000000..44a932b
--- /dev/null
+++ b/src/Plugin.NFC/Android/CrossNFC.android.cs
@@ -0,0 +1,90 @@
+using Android.App;
+using Android.Content;
+using Android.OS;
+using System;
+
+namespace Plugin.NFC
+{
+ ///
+ /// Cross NFC (Android specific)
+ ///
+ public static partial class CrossNFC
+ {
+ internal static ActivityLifecycleContextListener lifecycleListener;
+
+ ///
+ /// Initialization
+ ///
+ /// Android
+ public static void Init(Application application)
+ {
+ if (lifecycleListener != null) return;
+ lifecycleListener = new ActivityLifecycleContextListener();
+ application.RegisterActivityLifecycleCallbacks(lifecycleListener);
+ }
+
+ ///
+ /// Initialization
+ ///
+ /// Android
+ public static void Init(Activity activity)
+ {
+ Init(activity.Application);
+ lifecycleListener.Activity = activity;
+ }
+
+ ///
+ /// Overrides Activity.OnNewIntent()
+ ///
+ /// Android
+ public static void OnNewIntent(Intent intent) => ((NFCImplementation)Current).HandleNewIntent(intent);
+
+ ///
+ /// Overrides Activity.OnResume()
+ ///
+ public static void OnResume() => ((NFCImplementation)Current).HandleOnResume();
+
+ ///
+ /// Returns the current Android
+ ///
+ internal static Context AppContext => Application.Context;
+
+ ///
+ /// Returns the current Android
+ ///
+ ///
+ ///
+ internal static Activity GetCurrentActivity(bool throwError)
+ {
+ var activity = lifecycleListener?.Activity;
+ if (throwError && activity == null)
+ throw new NullReferenceException("The current Activity can not be detected. Ensure that you have called Init in your Activity or Application class.");
+ return activity;
+ }
+ }
+
+ ///
+ /// James Montemagno's ActivityLifecycleContextListener from CurrentActivityPlugin
+ ///
+ ///
+ class ActivityLifecycleContextListener : Java.Lang.Object, Application.IActivityLifecycleCallbacks
+ {
+ WeakReference _currentActivity = new WeakReference(null);
+
+ internal Context Context => Activity ?? Application.Context;
+
+ internal Activity Activity
+ {
+ get => _currentActivity.TryGetTarget(out var a) ? a : null;
+ set => _currentActivity.SetTarget(value);
+ }
+
+ void Application.IActivityLifecycleCallbacks.OnActivityCreated(Activity activity, Bundle savedInstanceState) => Activity = activity;
+ void Application.IActivityLifecycleCallbacks.OnActivityDestroyed(Activity activity) { }
+ void Application.IActivityLifecycleCallbacks.OnActivityPaused(Activity activity) => Activity = activity;
+ void Application.IActivityLifecycleCallbacks.OnActivityResumed(Activity activity) => Activity = activity;
+ void Application.IActivityLifecycleCallbacks.OnActivitySaveInstanceState(Activity activity, Bundle outState) { }
+ void Application.IActivityLifecycleCallbacks.OnActivityStarted(Activity activity) { }
+ void Application.IActivityLifecycleCallbacks.OnActivityStopped(Activity activity) { }
+ }
+}
diff --git a/src/Plugin.NFC/Android/NFC.android.cs b/src/Plugin.NFC/Android/NFC.android.cs
new file mode 100644
index 0000000..da90162
--- /dev/null
+++ b/src/Plugin.NFC/Android/NFC.android.cs
@@ -0,0 +1,570 @@
+using Android;
+using Android.App;
+using Android.Content;
+using Android.Nfc;
+using Android.Nfc.Tech;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Plugin.NFC
+{
+ ///
+ /// Android implementation of
+ ///
+ public class NFCImplementation : INFC
+ {
+ public event EventHandler OnTagConnected;
+ public event EventHandler OnTagDisconnected;
+ public event NdefMessageReceivedEventHandler OnMessageReceived;
+ public event NdefMessagePublishedEventHandler OnMessagePublished;
+ public event TagDiscoveredEventHandler OnTagDiscovered;
+ public event EventHandler OniOSReadingSessionCancelled;
+ public event TagListeningStatusChangedEventHandler OnTagListeningStatusChanged;
+
+ readonly NfcAdapter _nfcAdapter;
+
+ bool _isListening;
+ bool _isWriting;
+ bool _isFormatting;
+ Tag _currentTag;
+
+ ///
+ /// Current Android
+ ///
+ Context CurrentContext => CrossNFC.AppContext;
+
+ ///
+ /// Current Android
+ ///
+ Activity CurrentActivity => CrossNFC.GetCurrentActivity(true);
+
+ ///
+ /// Checks if NFC Feature is available
+ ///
+ public bool IsAvailable
+ {
+ get
+ {
+ if (CurrentContext.CheckCallingOrSelfPermission(Manifest.Permission.Nfc) != Android.Content.PM.Permission.Granted)
+ return false;
+ return _nfcAdapter != null;
+ }
+ }
+
+ ///
+ /// Checks if NFC Feature is enabled
+ ///
+ public bool IsEnabled => IsAvailable && _nfcAdapter.IsEnabled;
+
+ ///
+ /// Checks if writing mode is supported
+ ///
+ public bool IsWritingTagSupported => NFCUtils.IsWritingSupported();
+
+ ///
+ /// NFC configuration
+ ///
+ public NfcConfiguration Configuration { get; private set; }
+
+ ///
+ /// Default constructor
+ ///
+ public NFCImplementation()
+ {
+ _nfcAdapter = NfcAdapter.GetDefaultAdapter(CurrentContext);
+ Configuration = NfcConfiguration.GetDefaultConfiguration();
+ }
+
+ ///
+ /// Update NFC configuration
+ ///
+ ///
+ public void SetConfiguration(NfcConfiguration configuration) => Configuration.Update(configuration);
+
+ ///
+ /// Starts tags detection
+ ///
+ public void StartListening()
+ {
+ if (_nfcAdapter == null)
+ return;
+
+ var intent = new Intent(CurrentActivity, CurrentActivity.GetType()).AddFlags(ActivityFlags.SingleTop);
+
+ // We don't use MonoAndroid12.0 as targetframework for easier backward compatibility:
+ // MonoAndroid12.0 needs JDK 11.
+ PendingIntentFlags pendingIntentFlags = 0;
+
+#if NET6_0_OR_GREATER
+ if (Android.OS.Build.VERSION.SdkInt >= Android.OS.BuildVersionCodes.S)
+ pendingIntentFlags = PendingIntentFlags.Mutable;
+#else
+ if ((int)Android.OS.Build.VERSION.SdkInt >= 31) //Android.OS.BuildVersionCodes.S
+ pendingIntentFlags = (PendingIntentFlags)33554432; //PendingIntentFlags.Mutable
+#endif
+
+ var pendingIntent = PendingIntent.GetActivity(CurrentActivity, 0, intent, pendingIntentFlags);
+
+ var ndefFilter = new IntentFilter(NfcAdapter.ActionNdefDiscovered);
+ ndefFilter.AddDataType("*/*");
+
+ var tagFilter = new IntentFilter(NfcAdapter.ActionTagDiscovered);
+ tagFilter.AddCategory(Intent.CategoryDefault);
+
+ var filters = new IntentFilter[] { ndefFilter, tagFilter };
+
+ _nfcAdapter.EnableForegroundDispatch(CurrentActivity, pendingIntent, filters, null);
+
+ _isListening = true;
+ OnTagListeningStatusChanged?.Invoke(_isListening);
+ }
+
+ ///
+ /// Stops tags detection
+ ///
+ public void StopListening()
+ {
+ DisablePublishing();
+ if (_nfcAdapter != null)
+ _nfcAdapter.DisableForegroundDispatch(CurrentActivity);
+
+ _isListening = false;
+ OnTagListeningStatusChanged?.Invoke(_isListening);
+ }
+
+ ///
+ /// Starts tag publishing (writing or formatting)
+ ///
+ /// Format tag
+ public void StartPublishing(bool clearMessage = false)
+ {
+ if (!IsWritingTagSupported)
+ return;
+
+ _isWriting = true;
+ _isFormatting = clearMessage;
+ }
+
+ ///
+ /// Stops tag publishing
+ ///
+ public void StopPublishing() => DisablePublishing();
+
+ ///
+ /// Publish or write a message on a tag
+ ///
+ /// see
+ /// make tag read-only
+ public void PublishMessage(ITagInfo tagInfo, bool makeReadOnly = false) => WriteOrClearMessage(tagInfo, false, makeReadOnly);
+
+ ///
+ /// Format tag
+ ///
+ /// see
+ public void ClearMessage(ITagInfo tagInfo) => WriteOrClearMessage(tagInfo, true);
+
+ ///
+ /// Write or Clear a NDEF message
+ ///
+ ///
+ /// Clear Message
+ /// Make tag read-only
+ internal void WriteOrClearMessage(ITagInfo tagInfo, bool clearMessage = false, bool makeReadOnly = false)
+ {
+ try
+ {
+ if (_currentTag == null)
+ throw new Exception(Configuration.Messages.NFCErrorMissingTag);
+
+ if (tagInfo == null)
+ throw new Exception(Configuration.Messages.NFCErrorMissingTagInfo);
+
+ var ndef = Ndef.Get(_currentTag);
+ if (ndef != null)
+ {
+ try
+ {
+ if (!ndef.IsWritable)
+ throw new Exception(Configuration.Messages.NFCErrorReadOnlyTag);
+
+ if (ndef.MaxSize < NFCUtils.GetSize(tagInfo.Records))
+ throw new Exception(Configuration.Messages.NFCErrorCapacityTag);
+
+ ndef.Connect();
+ OnTagConnected?.Invoke(null, EventArgs.Empty);
+
+ NdefMessage message = null;
+ if (clearMessage)
+ {
+ message = GetEmptyNdefMessage();
+ }
+ else
+ {
+ var records = new List();
+ for (var i = 0; i < tagInfo.Records.Length; i++)
+ {
+ var record = tagInfo.Records[i];
+ if (GetAndroidNdefRecord(record) is NdefRecord ndefRecord)
+ records.Add(ndefRecord);
+ }
+
+ if (records.Any())
+ message = new NdefMessage(records.ToArray());
+ }
+
+ if (message != null)
+ {
+ ndef.WriteNdefMessage(message);
+
+ if (!clearMessage && makeReadOnly)
+ {
+ if (!MakeReadOnly(ndef))
+ Console.WriteLine("Cannot lock tag");
+ }
+
+ var nTag = GetTagInfo(_currentTag, ndef.NdefMessage);
+ OnMessagePublished?.Invoke(nTag);
+ }
+ else
+ throw new Exception(Configuration.Messages.NFCErrorWrite);
+ }
+ catch (Android.Nfc.TagLostException tlex)
+ {
+ throw new Exception("Tag Lost Error: " + tlex.Message);
+ }
+ catch (Java.IO.IOException ioex)
+ {
+ throw new Exception("Tag IO Error: " + ioex.Message);
+ }
+ catch (Android.Nfc.FormatException fe)
+ {
+ throw new Exception("Tag Format Error: " + fe.Message);
+ }
+ catch (Exception ex)
+ {
+ throw new Exception("Tag Error:" + ex.Message);
+ }
+ finally
+ {
+ if (ndef.IsConnected)
+ ndef.Close();
+
+ _currentTag = null;
+ OnTagDisconnected?.Invoke(null, EventArgs.Empty);
+ }
+ }
+ else
+ throw new Exception(Configuration.Messages.NFCErrorNotCompliantTag);
+ }
+ catch (Exception ex)
+ {
+ StopPublishingAndThrowError(ex.Message);
+ }
+ }
+
+ ///
+ /// Handle Android OnNewIntent
+ ///
+ /// Android
+ internal void HandleNewIntent(Intent intent)
+ {
+ if (intent == null)
+ return;
+
+ if (intent.Action == NfcAdapter.ActionTagDiscovered || intent.Action == NfcAdapter.ActionNdefDiscovered)
+ {
+ _currentTag = intent.GetParcelableExtra(NfcAdapter.ExtraTag) as Tag;
+ if (_currentTag != null)
+ {
+ var nTag = GetTagInfo(_currentTag);
+ if (_isWriting)
+ {
+ // Write mode
+ OnTagDiscovered?.Invoke(nTag, _isFormatting);
+ }
+ else
+ {
+ // Read mode
+ OnMessageReceived?.Invoke(nTag);
+ }
+ }
+ }
+ }
+
+ ///
+ /// Handle Android OnResume
+ ///
+ internal void HandleOnResume()
+ {
+ // Android 10 fix:
+ // If listening mode is already enable, we restart listening when activity is resumed
+ if (_isListening)
+ StartListening();
+ }
+
+ #region Private
+
+ ///
+ /// Stops publishing and throws error
+ ///
+ /// message
+ void StopPublishingAndThrowError(string message)
+ {
+ StopPublishing();
+ throw new Exception(message);
+ }
+
+ ///
+ /// Deactivate publishing
+ ///
+ void DisablePublishing()
+ {
+ _isWriting = false;
+ _isFormatting = false;
+ }
+
+ ///
+ /// Transforms an array of into an array of
+ ///
+ /// Array of
+ /// Array of
+ NFCNdefRecord[] GetRecords(NdefRecord[] records)
+ {
+ var results = new NFCNdefRecord[records.Length];
+ for (var i = 0; i < records.Length; i++)
+ {
+ var ndefRecord = new NFCNdefRecord
+ {
+ TypeFormat = (NFCNdefTypeFormat)records[i].Tnf,
+ Uri = records[i].ToUri()?.ToString(),
+ MimeType = records[i].ToMimeType(),
+ Payload = records[i].GetPayload()
+ };
+ results.SetValue(ndefRecord, i);
+ }
+ return results;
+ }
+
+ ///
+ /// Returns informations contains in NFC Tag
+ ///
+ /// Android
+ /// Android
+ ///
+ ITagInfo GetTagInfo(Tag tag, NdefMessage ndefMessage = null)
+ {
+ if (tag == null)
+ return null;
+
+ var ndef = Ndef.Get(tag);
+ var nTag = new TagInfo(tag.GetId(), ndef != null);
+
+ if (ndef != null)
+ {
+ nTag.Capacity = ndef.MaxSize;
+ nTag.IsWritable = ndef.IsWritable;
+
+ if (ndefMessage == null)
+ ndefMessage = ndef.CachedNdefMessage;
+
+ if (ndefMessage != null)
+ {
+ var records = ndefMessage.GetRecords();
+ nTag.Records = GetRecords(records);
+ }
+ }
+
+ return nTag;
+ }
+
+ ///
+ /// Transforms a into an Android
+ ///
+ /// Object
+ /// Android
+ NdefRecord GetAndroidNdefRecord(NFCNdefRecord record)
+ {
+ if (record == null)
+ return null;
+
+ NdefRecord ndefRecord = null;
+ switch (record.TypeFormat)
+ {
+ case NFCNdefTypeFormat.WellKnown:
+ var languageCode = record.LanguageCode;
+ if (string.IsNullOrWhiteSpace(languageCode)) languageCode = Configuration.DefaultLanguageCode;
+ ndefRecord = NdefRecord.CreateTextRecord(languageCode.Substring(0, 2), Encoding.UTF8.GetString(record.Payload));
+ break;
+ case NFCNdefTypeFormat.Mime:
+ ndefRecord = NdefRecord.CreateMime(record.MimeType, record.Payload);
+ break;
+ case NFCNdefTypeFormat.Uri:
+ ndefRecord = NdefRecord.CreateUri(Encoding.UTF8.GetString(record.Payload));
+ break;
+ case NFCNdefTypeFormat.External:
+ ndefRecord = NdefRecord.CreateExternal(record.ExternalDomain, record.ExternalType, record.Payload);
+ break;
+ case NFCNdefTypeFormat.Empty:
+ ndefRecord = GetEmptyNdefRecord();
+ break;
+ case NFCNdefTypeFormat.Unknown:
+ case NFCNdefTypeFormat.Unchanged:
+ case NFCNdefTypeFormat.Reserved:
+ default:
+ break;
+
+ }
+ return ndefRecord;
+ }
+
+ ///
+ /// Returns an empty Android
+ ///
+ /// Android
+ NdefRecord GetEmptyNdefRecord()
+ {
+ var empty = Array.Empty();
+ return new NdefRecord(NdefRecord.TnfEmpty, empty, empty, empty);
+ }
+
+ ///
+ /// Returns an empty Android
+ ///
+ /// Android
+ NdefMessage GetEmptyNdefMessage()
+ {
+ var records = new NdefRecord[1];
+ records[0] = GetEmptyNdefRecord();
+ return new NdefMessage(records);
+ }
+
+ ///
+ /// Make a tag read-only
+ /// WARNING: This operation is permanent
+ ///
+ ///
+ /// boolean
+ bool MakeReadOnly(Ndef ndef)
+ {
+ if (ndef == null)
+ return false;
+
+ var result = false;
+ var newConnection = false;
+
+ if (!ndef.IsConnected)
+ {
+ newConnection = true;
+ ndef.Connect();
+ }
+
+ if (ndef.CanMakeReadOnly())
+ result = ndef.MakeReadOnly();
+
+ if (newConnection && ndef.IsConnected)
+ ndef.Close();
+
+ return result;
+ }
+
+ #endregion
+
+ #region NFC Status Event Listener
+
+ NfcBroadcastReceiver _nfcBroadcastReceiver;
+
+ event OnNfcStatusChangedEventHandler _onNfcStatusChangedInternal;
+
+ public event OnNfcStatusChangedEventHandler OnNfcStatusChanged
+ {
+ add
+ {
+ var wasRunning = _onNfcStatusChangedInternal != null;
+ _onNfcStatusChangedInternal += value;
+ if (!wasRunning && _onNfcStatusChangedInternal != null)
+ {
+ RegisterListener();
+ }
+ }
+ remove
+ {
+ var wasRunning = _onNfcStatusChangedInternal != null;
+ _onNfcStatusChangedInternal -= value;
+ if (wasRunning && _onNfcStatusChangedInternal == null)
+ {
+ UnRegisterListener();
+ }
+ }
+ }
+
+ ///
+ /// Register NFC Broadcast Receiver
+ ///
+ void RegisterListener()
+ {
+ _nfcBroadcastReceiver = new NfcBroadcastReceiver(OnNfcStatusChange);
+ CurrentContext?.RegisterReceiver(_nfcBroadcastReceiver, new IntentFilter(NfcAdapter.ActionAdapterStateChanged));
+ }
+
+ ///
+ /// Unregister NFC Broadcast Receiver
+ ///
+ void UnRegisterListener()
+ {
+ if (_nfcBroadcastReceiver == null)
+ return;
+
+ try
+ {
+ CurrentContext?.UnregisterReceiver(_nfcBroadcastReceiver);
+ }
+ catch (Java.Lang.IllegalArgumentException ex)
+ {
+ throw new Exception("NFC Broadcast Receiver Error: " + ex.Message);
+ }
+
+ _nfcBroadcastReceiver.Dispose();
+ _nfcBroadcastReceiver = null;
+ }
+
+ ///
+ /// Called when NFC status has changed
+ ///
+ void OnNfcStatusChange() => _onNfcStatusChangedInternal?.Invoke(IsEnabled);
+
+ ///
+ /// Broadcast Receiver to check NFC feature availability
+ ///
+ [BroadcastReceiver(Enabled = true, Exported = false, Label = "NFC Status Broadcast Receiver")]
+ class NfcBroadcastReceiver : BroadcastReceiver
+ {
+ Action _onChanged;
+
+ public NfcBroadcastReceiver() { }
+ public NfcBroadcastReceiver(Action onChanged)
+ {
+ _onChanged = onChanged;
+ }
+
+ public override async void OnReceive(Context context, Intent intent)
+ {
+ if (intent.Action == NfcAdapter.ActionAdapterStateChanged)
+ {
+ var state = intent.GetIntExtra(NfcAdapter.ExtraAdapterState, default);
+ if (state == NfcAdapter.StateOff || state == NfcAdapter.StateOn)
+ {
+ // await 1500ms to ensure that the status updates
+ await Task.Delay(1500);
+ _onChanged?.Invoke();
+ }
+ }
+ }
+ }
+
+ #endregion
+
+ }
+}
diff --git a/src/Plugin.NFC/Plugin.NFC.csproj b/src/Plugin.NFC/Plugin.NFC.csproj
new file mode 100644
index 0000000..e1f631b
--- /dev/null
+++ b/src/Plugin.NFC/Plugin.NFC.csproj
@@ -0,0 +1,107 @@
+
+
+
+
+ netstandard1.0;netstandard2.0;Xamarin.iOS10;MonoAndroid10.0;net8.0;net8.0-android;net8.0-ios
+
+ Plugin.NFC
+ Plugin.NFC
+ Plugin.NFC
+
+ $(AssemblyName) ($(TargetFramework))
+ 1.0.0.0
+ 1.0.0.0
+ 1.0.0.0
+ 1.0.0.0
+ true
+ en
+ $(DefineConstants);
+ CS0067
+
+ false
+ false
+ latest
+ portable
+
+ MIT
+ https://github.com/franckbour/Plugin.NFC
+ https://github.com/franckbour/Plugin.NFC
+ git
+ https://github.com/franckbour/Plugin.NFC/blob/master/CHANGELOG.md
+ https://github.com/franckbour/Plugin.NFC/raw/master/art/nfc128.png
+ icon.png
+ maui, xamarin, ios, android, xamarin.forms, plugin, NFC
+
+ NFC Plugin for Xamarin
+ A Cross-Platform plugin to easily read and write NFC tags.
+ Cross-Platform NFC (Near Field Communication) plugin to easily read and write NFC tags in your application.
+
+ Franck Bour
+ Franck Bour
+ Copyright 2022
+
+
+ false
+
+
+
+
+ true
+ false
+
+
+
+ true
+ true
+
+
+ true
+
+ true
+
+ $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb
+
+
+
+
+ <_NET8 Condition=" $(TargetFramework.Contains('net8')) ">true
+ <_DROID Condition=" $(TargetFramework.ToLowerInvariant().Contains('droid')) ">true
+ <_IOS Condition=" $(TargetFramework.ToLowerInvariant().Contains('ios')) ">true
+ <_MOBILE Condition=" '$(_DROID)' == 'true' OR '$(_IOS)' == 'true' ">true
+
+ $(DefineConstants);__ANDROID__;__MOBILE__;
+ $(DefineConstants);__IOS__;__MOBILE__;
+ $(DefineConstants);__NET8__;
+
+
+
+ 11.0
+ 21.0
+
+
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Plugin.NFC/Shared/CrossNFC.shared.cs b/src/Plugin.NFC/Shared/CrossNFC.shared.cs
new file mode 100644
index 0000000..056e369
--- /dev/null
+++ b/src/Plugin.NFC/Shared/CrossNFC.shared.cs
@@ -0,0 +1,73 @@
+using System;
+
+namespace Plugin.NFC
+{
+ ///
+ /// Cross NFC
+ ///
+ public static partial class CrossNFC
+ {
+ static Lazy _implementation = new Lazy(() => CreateNFC(), System.Threading.LazyThreadSafetyMode.PublicationOnly);
+
+ ///
+ /// Gets if the plugin is supported on the current platform.
+ ///
+ public static bool IsSupported => _implementation.Value != null;
+
+ ///
+ /// Legacy Mode (Supporting Mifare Classic on iOS)
+ ///
+ static bool _legacy = false;
+
+ public static bool Legacy
+ {
+ get
+ {
+ return _legacy;
+ }
+
+ set
+ {
+ _legacy = value;
+
+ _implementation = new Lazy(() => CreateNFC(), System.Threading.LazyThreadSafetyMode.PublicationOnly);
+ }
+ }
+
+ ///
+ /// Current plugin implementation to use
+ ///
+ public static INFC Current
+ {
+ get
+ {
+ INFC ret = _implementation.Value;
+ if (ret == null)
+ {
+ throw NotImplementedInReferenceAssembly();
+ }
+ return ret;
+ }
+ }
+
+ static INFC CreateNFC()
+ {
+#if NETSTANDARD || !__MOBILE__
+ return null;
+#elif __IOS__
+ ObjCRuntime.Class.ThrowOnInitFailure = false;
+ if (NFCUtils.IsWritingSupported() && !Legacy)
+ return new NFCImplementation();
+ return new NFCImplementation_Before_iOS13();
+#else
+#pragma warning disable IDE0022 // Use expression body for methods
+ return new NFCImplementation();
+#pragma warning restore IDE0022 // Use expression body for methods
+#endif
+ }
+
+ internal static Exception NotImplementedInReferenceAssembly() =>
+ new NotImplementedException("This functionality is not implemented in the portable version of this assembly. You should reference the NuGet package from your main application project in order to reference the platform-specific implementation.");
+
+ }
+}
diff --git a/src/Plugin.NFC/Shared/INFC.shared.cs b/src/Plugin.NFC/Shared/INFC.shared.cs
new file mode 100644
index 0000000..ace1e12
--- /dev/null
+++ b/src/Plugin.NFC/Shared/INFC.shared.cs
@@ -0,0 +1,118 @@
+using System;
+
+namespace Plugin.NFC
+{
+ #region Event delegates
+ public delegate void NdefMessageReceivedEventHandler(ITagInfo tagInfo);
+ public delegate void NdefMessagePublishedEventHandler(ITagInfo tagInfo);
+ public delegate void TagDiscoveredEventHandler(ITagInfo tagInfo, bool format);
+ public delegate void OnNfcStatusChangedEventHandler(bool isEnabled);
+ public delegate void TagListeningStatusChangedEventHandler(bool isListening);
+ #endregion
+
+ ///
+ /// Main interface for NFC
+ ///
+ public interface INFC
+ {
+ ///
+ /// Checks if NFC Feature is available
+ ///
+ bool IsAvailable { get; }
+
+ ///
+ /// Checks if NFC Feature is enabled
+ ///
+ bool IsEnabled { get; }
+
+ ///
+ /// Checks if writing mode is supported
+ ///
+ bool IsWritingTagSupported { get; }
+
+ ///
+ /// NFC Configuration
+ ///
+ NfcConfiguration Configuration { get; }
+
+ ///
+ /// Set Nfc configuration
+ ///
+ ///
+ void SetConfiguration(NfcConfiguration configuration);
+
+ ///
+ /// Starts tags detection
+ ///
+ void StartListening();
+
+ ///
+ /// Stops tags detection
+ ///
+ void StopListening();
+
+ ///
+ /// Starts tag publishing (writing or formatting)
+ ///
+ /// Format tag
+ void StartPublishing(bool clearMessage = false);
+
+ ///
+ /// Stops tag publishing
+ ///
+ void StopPublishing();
+
+ ///
+ /// Publish or write a message on a tag
+ ///
+ /// see
+ /// make tag read-only
+ void PublishMessage(ITagInfo tagInfo, bool makeReadOnly = false);
+
+ ///
+ /// Format tag
+ ///
+ /// see
+ void ClearMessage(ITagInfo tagInfo);
+
+ ///
+ /// Event raised when tag is connected
+ ///
+ event EventHandler OnTagConnected;
+
+ ///
+ /// Event raised when tag is disconnected
+ ///
+ event EventHandler OnTagDisconnected;
+
+ ///
+ /// Event raised when ndef message is received
+ ///
+ event NdefMessageReceivedEventHandler OnMessageReceived;
+
+ ///
+ /// Event raised when a tag is discovered (Editing)
+ ///
+ event TagDiscoveredEventHandler OnTagDiscovered;
+
+ ///
+ /// Event raised when ndef message has been published
+ ///
+ event NdefMessagePublishedEventHandler OnMessagePublished;
+
+ ///
+ /// Event raised when iOS NFC reading session is cancelled
+ ///
+ event EventHandler OniOSReadingSessionCancelled;
+
+ ///
+ /// Event raised when NFC status changes
+ ///
+ event OnNfcStatusChangedEventHandler OnNfcStatusChanged;
+
+ ///
+ /// Event raised when NFC listener status changes
+ ///
+ event TagListeningStatusChangedEventHandler OnTagListeningStatusChanged;
+ }
+}
diff --git a/src/Plugin.NFC/Shared/ITagInfo.shared.cs b/src/Plugin.NFC/Shared/ITagInfo.shared.cs
new file mode 100644
index 0000000..ba4e226
--- /dev/null
+++ b/src/Plugin.NFC/Shared/ITagInfo.shared.cs
@@ -0,0 +1,104 @@
+namespace Plugin.NFC
+{
+ ///
+ /// Interface for ITagInfo
+ ///
+ public interface ITagInfo
+ {
+ ///
+ /// Tag Raw Identifier
+ ///
+ byte[] Identifier { get; }
+
+ ///
+ /// Tag Serial Number
+ ///
+ string SerialNumber { get; }
+
+ ///
+ /// Writable tag
+ ///
+ bool IsWritable { get; set; }
+
+ ///
+ /// Empty tag
+ ///
+ bool IsEmpty { get; }
+
+ ///
+ /// Supported tag
+ ///
+ bool IsSupported { get; }
+
+ ///
+ /// Capacity of tag in bytes
+ ///
+ int Capacity { get; set; }
+
+ ///
+ /// Array of of tag
+ ///
+ NFCNdefRecord[] Records { get; set; }
+ }
+
+ ///
+ /// Class describing the information containing within a NFC tag
+ ///
+ public class NFCNdefRecord
+ {
+ ///
+ /// NDEF Type
+ ///
+ public NFCNdefTypeFormat TypeFormat { get; set; }
+
+ ///
+ /// MimeType used for type
+ ///
+ public string MimeType { get; set; } = "text/plain";
+
+ ///
+ /// External domain used for type
+ ///
+ public string ExternalDomain { get; set; }
+
+ ///
+ /// External type used for type
+ ///
+ public string ExternalType { get; set; }
+
+ ///
+ /// Payload
+ ///
+ public byte[] Payload { get; set; }
+
+ ///
+ /// Uri
+ ///
+ public string Uri { get; set; }
+
+ ///
+ /// String formatted payload
+ ///
+ public string Message => NFCUtils.GetMessage(TypeFormat, Payload, Uri);
+
+ ///
+ /// Two letters ISO 639-1 Language Code (ex: en, fr, de...)
+ ///
+ public string LanguageCode { get; set; }
+ }
+
+ ///
+ /// Enumeration of NDEF type
+ ///
+ public enum NFCNdefTypeFormat
+ {
+ Empty = 0x00,
+ WellKnown = 0x01,
+ Mime = 0x02,
+ Uri = 0x03,
+ External = 0x04,
+ Unknown = 0x05,
+ Unchanged = 0x06,
+ Reserved = 0x07
+ }
+}
diff --git a/src/Plugin.NFC/Shared/NFCUtils.shared.cs b/src/Plugin.NFC/Shared/NFCUtils.shared.cs
new file mode 100644
index 0000000..cb0720c
--- /dev/null
+++ b/src/Plugin.NFC/Shared/NFCUtils.shared.cs
@@ -0,0 +1,113 @@
+using System.Linq;
+using System.Text;
+#if __IOS__
+using UIKit;
+#endif
+
+namespace Plugin.NFC
+{
+ ///
+ /// NFC tools
+ ///
+ public static class NFCUtils
+ {
+ ///
+ /// Returns the content size of an array of
+ ///
+ /// array of
+ /// Content size
+ internal static int GetSize(NFCNdefRecord[] records)
+ {
+ var size = 0;
+ if (records != null && records.Length > 0)
+ {
+ for (var i = 0; i < records.Length; i++)
+ {
+ if (records[i] != null)
+ size += records[i].Payload.Length;
+ }
+ }
+ return size;
+ }
+
+ ///
+ /// Returns the string formatted payload
+ ///
+ /// type of
+ /// record payload
+ /// record uri
+ /// String formatted payload
+ internal static string GetMessage(NFCNdefTypeFormat type, byte[] payload, string uri)
+ {
+ string message;
+ if (!string.IsNullOrWhiteSpace(uri))
+ message = uri;
+ else
+ {
+ if (type == NFCNdefTypeFormat.WellKnown)
+ {
+ // NDEF_WELLKNOWN Text record
+ var status = payload[0];
+ var enc = status & 0x80;
+ var languageCodeLength = status & 0x3F;
+ if (enc == 0)
+ message = Encoding.UTF8.GetString(payload, languageCodeLength + 1, payload.Length - languageCodeLength - 1);
+ else
+ message = Encoding.Unicode.GetString(payload, languageCodeLength + 1, payload.Length - languageCodeLength - 1);
+ }
+ else
+ {
+ // Other NDEF types
+ message = Encoding.UTF8.GetString(payload, 0, payload.Length);
+ }
+ }
+ return message;
+ }
+
+ ///
+ /// Transforms a string message into an array of bytes
+ ///
+ /// text message
+ /// Array of bytes
+ public static byte[] EncodeToByteArray(string text) => Encoding.UTF8.GetBytes(text);
+
+ ///
+ /// Returns the string formatted payload
+ ///
+ /// Object
+ /// String formatted payload
+ public static string GetMessage(NFCNdefRecord record)
+ {
+ if (record == null)
+ return string.Empty;
+ return GetMessage(record.TypeFormat, record.Payload, record.Uri);
+ }
+
+ ///
+ /// Convert bytes array into hexadecimal string
+ ///
+ /// Bytes Array
+ /// Separator
+ /// Hexadecimal string
+ public static string ByteArrayToHexString(byte[] bytes, string separator = null)
+ {
+ return bytes == null ? string.Empty : string.Join(separator ?? string.Empty, bytes.Select(b => b.ToString("X2")));
+ }
+
+ ///
+ /// Checks if writing tags is supported
+ ///
+ /// boolean
+ public static bool IsWritingSupported()
+ {
+#if __IOS__
+ var splitted = UIDevice.CurrentDevice.SystemVersion?.Split('.');
+ if (splitted != null && splitted.Length > 0 && int.TryParse(splitted[0], out var majorVersion))
+ return majorVersion >= 13;
+ return false;
+#else
+ return true;
+#endif
+ }
+ }
+}
diff --git a/src/Plugin.NFC/Shared/NfcConfiguration.shared.cs b/src/Plugin.NFC/Shared/NfcConfiguration.shared.cs
new file mode 100644
index 0000000..e3a356c
--- /dev/null
+++ b/src/Plugin.NFC/Shared/NfcConfiguration.shared.cs
@@ -0,0 +1,282 @@
+namespace Plugin.NFC
+{
+ ///
+ /// NFC Configuration class
+ ///
+ public class NfcConfiguration
+ {
+ ///
+ /// List of user defined messages
+ ///
+ public UserDefinedMessages Messages { get; set; }
+
+ ///
+ /// Sets ISO 639-1 Language Code for all ndef records (default is "en")
+ ///
+ public string DefaultLanguageCode { get; set; }
+
+ ///
+ /// Update Nfc Configuration with a new configuration object
+ ///
+ ///
+ public void Update(NfcConfiguration newCfg)
+ {
+ if (newCfg == null || newCfg.Messages == null)
+ return;
+ Messages = newCfg.Messages;
+ DefaultLanguageCode = newCfg.DefaultLanguageCode;
+ }
+
+ ///
+ /// Get the default Nfc configuration
+ ///
+ /// Default
+ public static NfcConfiguration GetDefaultConfiguration()
+ => new NfcConfiguration { Messages = new UserDefinedMessages(), DefaultLanguageCode = "en" };
+ }
+
+ ///
+ /// User defined UI messages
+ ///
+ public class UserDefinedMessages
+ {
+ string _nfcSessionInvalidated = "Session Invalidated";
+ string _nfcSessionInvalidatedButton = "OK";
+ string _nfcWritingNotSupported = "Writing NFC Tag is not supported on this device";
+ string _nfcDialogAlertMessage = "Please hold your phone near a NFC tag";
+ string _nfcErrorRead = "Read error. Please try again";
+ string _nfcErrorEmptyTag = "Tag is empty";
+ string _nfcErrorReadOnlyTag = "Tag is not writable";
+ string _nfcErrorCapacityTag = "Tag's capacity is too low";
+ string _nfcErrorMissingTag = "Tag is missing";
+ string _nfcErrorMissingTagInfo = "No Tag Informations: nothing to write";
+ string _nfcErrorNotSupportedTag = "Tag is not supported";
+ string _nfcErrorNotCompliantTag = "Tag is not NDEF compliant";
+ string _nfcErrorWrite = "Nothing to write";
+ string _nfcSuccessRead = "Read Operation Successful";
+ string _nfcSuccessWrite = "Write Operation Successful";
+ string _nfcSuccessClear = "Clear Operation Successful";
+ string _nfcSessionTimeout = "session timeout";
+
+ ///
+ /// Session timeout
+ ///
+ public string NFCSessionTimeout
+ {
+ get => _nfcSessionTimeout;
+ set
+ {
+ if (!string.IsNullOrWhiteSpace(value))
+ _nfcSessionTimeout = value;
+ }
+ }
+
+ ///
+ /// Session invalidated
+ ///
+ public string NFCSessionInvalidatedButton
+ {
+ get => _nfcSessionInvalidatedButton;
+ set
+ {
+ if (!string.IsNullOrWhiteSpace(value))
+ _nfcSessionInvalidatedButton = value;
+ }
+ }
+
+ ///
+ /// Session invalidated
+ ///
+ public string NFCSessionInvalidated
+ {
+ get => _nfcSessionInvalidated;
+ set
+ {
+ if (!string.IsNullOrWhiteSpace(value))
+ _nfcSessionInvalidated = value;
+ }
+ }
+
+ ///
+ /// Writing feature not supported
+ ///
+ public string NFCWritingNotSupported
+ {
+ get => _nfcWritingNotSupported;
+ set
+ {
+ if (!string.IsNullOrWhiteSpace(value))
+ _nfcWritingNotSupported = value;
+ }
+ }
+
+ ///
+ /// [iOS] NFC Scan dialog alert message
+ ///
+ public string NFCDialogAlertMessage
+ {
+ get => _nfcDialogAlertMessage;
+ set
+ {
+ if (!string.IsNullOrWhiteSpace(value))
+ _nfcDialogAlertMessage = value;
+ }
+ }
+
+ ///
+ /// Read operation error
+ ///
+ public string NFCErrorRead
+ {
+ get => _nfcErrorRead;
+ set
+ {
+ if (!string.IsNullOrWhiteSpace(value))
+ _nfcErrorRead = value;
+ }
+ }
+
+ ///
+ /// Write operation error
+ ///
+ public string NFCErrorWrite
+ {
+ get => _nfcErrorWrite;
+ set
+ {
+ if (!string.IsNullOrWhiteSpace(value))
+ _nfcErrorWrite = value;
+ }
+ }
+
+ ///
+ /// Empty tag error
+ ///
+ public string NFCErrorEmptyTag
+ {
+ get => _nfcErrorEmptyTag;
+ set
+ {
+ if (!string.IsNullOrWhiteSpace(value))
+ _nfcErrorEmptyTag = value;
+ }
+ }
+
+ ///
+ /// Read-only tag error
+ ///
+ public string NFCErrorReadOnlyTag
+ {
+ get => _nfcErrorReadOnlyTag;
+ set
+ {
+ if (!string.IsNullOrWhiteSpace(value))
+ _nfcErrorReadOnlyTag = value;
+ }
+ }
+
+ ///
+ /// Tag capacity error
+ ///
+ public string NFCErrorCapacityTag
+ {
+ get => _nfcErrorCapacityTag;
+ set
+ {
+ if (!string.IsNullOrWhiteSpace(value))
+ _nfcErrorCapacityTag = value;
+ }
+ }
+
+ ///
+ /// Missing tag error
+ ///
+ public string NFCErrorMissingTag
+ {
+ get => _nfcErrorMissingTag;
+ set
+ {
+ if (!string.IsNullOrWhiteSpace(value))
+ _nfcErrorMissingTag = value;
+ }
+ }
+
+ ///
+ /// Missing tag info error
+ ///
+ public string NFCErrorMissingTagInfo
+ {
+ get => _nfcErrorMissingTagInfo;
+ set
+ {
+ if (!string.IsNullOrWhiteSpace(value))
+ _nfcErrorMissingTagInfo = value;
+ }
+ }
+
+ ///
+ /// Not supported tag error
+ ///
+ public string NFCErrorNotSupportedTag
+ {
+ get => _nfcErrorNotSupportedTag;
+ set
+ {
+ if (!string.IsNullOrWhiteSpace(value))
+ _nfcErrorNotSupportedTag = value;
+ }
+ }
+
+ ///
+ /// Not NDEF compliant tag error
+ ///
+ public string NFCErrorNotCompliantTag
+ {
+ get => _nfcErrorNotCompliantTag;
+ set
+ {
+ if (!string.IsNullOrWhiteSpace(value))
+ _nfcErrorNotCompliantTag = value;
+ }
+ }
+
+ ///
+ /// [iOS] Successful read operation message
+ ///
+ public string NFCSuccessRead
+ {
+ get => _nfcSuccessRead;
+ set
+ {
+ if (!string.IsNullOrWhiteSpace(value))
+ _nfcSuccessRead = value;
+ }
+ }
+
+ ///
+ /// [iOS] Successful write operation message
+ ///
+ public string NFCSuccessWrite
+ {
+ get => _nfcSuccessWrite;
+ set
+ {
+ if (!string.IsNullOrWhiteSpace(value))
+ _nfcSuccessWrite = value;
+ }
+ }
+
+ ///
+ /// [iOS] Successful clear operation message
+ ///
+ public string NFCSuccessClear
+ {
+ get => _nfcSuccessClear;
+ set
+ {
+ if (!string.IsNullOrWhiteSpace(value))
+ _nfcSuccessClear = value;
+ }
+ }
+ }
+}
diff --git a/src/Plugin.NFC/Shared/TagInfo.shared.cs b/src/Plugin.NFC/Shared/TagInfo.shared.cs
new file mode 100644
index 0000000..73c310d
--- /dev/null
+++ b/src/Plugin.NFC/Shared/TagInfo.shared.cs
@@ -0,0 +1,62 @@
+namespace Plugin.NFC
+{
+ ///
+ /// Default implementation of
+ ///
+ public class TagInfo : ITagInfo
+ {
+ public byte[] Identifier { get; }
+
+ ///
+ /// Tag Serial Number
+ ///
+ public string SerialNumber { get; }
+
+ ///
+ /// Writable tag
+ ///
+ public bool IsWritable { get; set; }
+
+ ///
+ /// Capacity of tag in bytes
+ ///
+ public int Capacity { get; set; }
+
+ ///
+ /// Array of of tag
+ ///
+ public NFCNdefRecord[] Records { get; set; }
+
+ ///
+ /// Empty tag
+ ///
+ public bool IsEmpty => Records == null || Records.Length == 0 || Records[0] == null ||Records[0].TypeFormat == NFCNdefTypeFormat.Empty;
+
+ ///
+ ///
+ ///
+ public bool IsSupported { get; private set; }
+
+ ///
+ /// Default constructor
+ ///
+ public TagInfo()
+ {
+ IsSupported = true;
+ }
+
+ ///
+ /// Custom contructor
+ ///
+ /// Tag Identifier
+ /// Is Ndef tag
+ public TagInfo(byte[] identifier, bool isNdef = true)
+ {
+ Identifier = identifier;
+ SerialNumber = NFCUtils.ByteArrayToHexString(identifier);
+ IsSupported = isNdef;
+ }
+
+ public override string ToString() => $"TagInfo: identifier: {Identifier}, SerialNumber:{SerialNumber}, Capacity:{Capacity} bytes, IsSupported:{IsSupported}, IsEmpty:{IsEmpty}, IsWritable:{IsWritable}";
+ }
+}
diff --git a/src/Plugin.NFC/iOS/NFC.iOS.cs b/src/Plugin.NFC/iOS/NFC.iOS.cs
new file mode 100644
index 0000000..6aebc6d
--- /dev/null
+++ b/src/Plugin.NFC/iOS/NFC.iOS.cs
@@ -0,0 +1,1303 @@
+using CoreFoundation;
+using CoreNFC;
+using Foundation;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using UIKit;
+
+namespace Plugin.NFC
+{
+ ///
+ /// iOS 13+ implementation of
+ ///
+ public class NFCImplementation : NFCTagReaderSessionDelegate, INFC
+ {
+ public const string SessionTimeoutMessage = "session timeout";
+
+ public event EventHandler OnTagConnected;
+ public event EventHandler OnTagDisconnected;
+ public event NdefMessageReceivedEventHandler OnMessageReceived;
+ public event NdefMessagePublishedEventHandler OnMessagePublished;
+ public event TagDiscoveredEventHandler OnTagDiscovered;
+ public event EventHandler OniOSReadingSessionCancelled;
+ public event OnNfcStatusChangedEventHandler OnNfcStatusChanged;
+ public event TagListeningStatusChangedEventHandler OnTagListeningStatusChanged;
+
+ bool _isWriting;
+ bool _isFormatting;
+ bool _customInvalidation = false;
+
+ INFCTag _tag;
+
+ NFCTagReaderSession NfcSession { get; set; }
+
+ ///
+ /// Checks if NFC Feature is available
+ ///
+ public bool IsAvailable => NFCReaderSession.ReadingAvailable;
+
+ ///
+ /// Checks if NFC Feature is enabled
+ ///
+ public bool IsEnabled => IsAvailable;
+
+ ///
+ /// Checks if writing mode is supported
+ ///
+ public bool IsWritingTagSupported => true;
+
+ ///
+ /// NFC configuration
+ ///
+ public NfcConfiguration Configuration { get; private set; }
+
+ ///
+ /// Default constructor
+ ///
+ public NFCImplementation()
+ {
+ Configuration = NfcConfiguration.GetDefaultConfiguration();
+ }
+
+ ///
+ /// Update NFC configuration
+ ///
+ ///
+ public void SetConfiguration(NfcConfiguration configuration) => Configuration.Update(configuration);
+
+ ///
+ /// Starts tags detection
+ ///
+ public void StartListening()
+ {
+ _customInvalidation = false;
+ _isWriting = false;
+ _isFormatting = false;
+
+ NfcSession = new NFCTagReaderSession(NFCPollingOption.Iso14443 | NFCPollingOption.Iso15693, this, DispatchQueue.CurrentQueue)
+ {
+ AlertMessage = Configuration.Messages.NFCDialogAlertMessage
+ };
+ NfcSession?.BeginSession();
+ OnTagListeningStatusChanged?.Invoke(true);
+ }
+
+ ///
+ /// Stops tags detection
+ ///
+ public void StopListening()
+ {
+ NfcSession?.InvalidateSession();
+ }
+
+ ///
+ /// Starts tag publishing (writing or formatting)
+ ///
+ /// Format tag
+ public void StartPublishing(bool clearMessage = false)
+ {
+ if (!IsAvailable)
+ throw new InvalidOperationException(Configuration.Messages.NFCWritingNotSupported);
+
+ _customInvalidation = false;
+ _isWriting = true;
+ _isFormatting = clearMessage;
+
+ NfcSession = new NFCTagReaderSession(NFCPollingOption.Iso14443 | NFCPollingOption.Iso15693, this, DispatchQueue.CurrentQueue)
+ {
+ AlertMessage = Configuration.Messages.NFCDialogAlertMessage
+ };
+ NfcSession?.BeginSession();
+ OnTagListeningStatusChanged?.Invoke(true);
+ }
+
+ ///
+ /// Stops tag publishing
+ ///
+ public void StopPublishing()
+ {
+ _isWriting = _isFormatting = _customInvalidation = false;
+ _tag = null;
+ NfcSession?.InvalidateSession();
+ }
+
+ ///
+ /// Publish or write a message on a tag
+ ///
+ /// see
+ /// make tag read-only
+ public void PublishMessage(ITagInfo tagInfo, bool makeReadOnly = false) => WriteOrClearMessage(_tag, tagInfo, false, makeReadOnly);
+
+ ///
+ /// Format tag
+ ///
+ /// see
+ public void ClearMessage(ITagInfo tagInfo) => WriteOrClearMessage(_tag, tagInfo, true);
+
+ ///
+ /// Event raised when NFC tags are detected
+ ///
+ /// iOS
+ /// Array of iOS
+ public override void DidDetectTags(NFCTagReaderSession session, INFCTag[] tags)
+ {
+ _customInvalidation = false;
+ _tag = tags.First();
+
+ var connectionError = string.Empty;
+ session.ConnectTo(_tag, (error) =>
+ {
+ if (error != null)
+ {
+ connectionError = error.LocalizedDescription;
+ Invalidate(session, connectionError);
+ return;
+ }
+
+ var ndefTag = NfcNdefTagExtensions.GetNdefTag(_tag);
+
+ if (ndefTag == null)
+ {
+ Invalidate(session, Configuration.Messages.NFCErrorNotCompliantTag);
+ return;
+ }
+
+ ndefTag.QueryNdefStatus((status, capacity, error) =>
+ {
+ if (error != null)
+ {
+ Invalidate(session, error.LocalizedDescription);
+ return;
+ }
+
+ var isNdefSupported = status != NFCNdefStatus.NotSupported;
+
+ var identifier = NfcNdefTagExtensions.GetTagIdentifier(ndefTag);
+ var nTag = new TagInfo(identifier, isNdefSupported)
+ {
+ IsWritable = status == NFCNdefStatus.ReadWrite,
+ Capacity = Convert.ToInt32(capacity)
+ };
+
+ if (!isNdefSupported)
+ {
+ session.AlertMessage = Configuration.Messages.NFCErrorNotSupportedTag;
+
+ OnMessageReceived?.Invoke(nTag);
+ Invalidate(session);
+ return;
+ }
+
+ if (_isWriting)
+ {
+ // Write mode
+ OnTagDiscovered?.Invoke(nTag, _isFormatting);
+ }
+ else
+ {
+ // Read mode
+ ndefTag.ReadNdef((message, error) =>
+ {
+ // iOS Error: NFCReaderError.NdefReaderSessionErrorZeroLengthMessage (NDEF tag does not contain any NDEF message)
+ // NFCReaderError.NdefReaderSessionErrorZeroLengthMessage constant should be equals to 403 instead of 304
+ // see https://developer.apple.com/documentation/corenfc/nfcreadererror/code/ndefreadersessionerrorzerolengthmessage
+ if (error != null && error.Code != 403)
+ {
+ Invalidate(session, Configuration.Messages.NFCErrorRead);
+ return;
+ }
+
+ session.AlertMessage = Configuration.Messages.NFCSuccessRead;
+
+ nTag.Records = NFCNdefPayloadExtensions.GetRecords(message?.Records);
+ OnMessageReceived?.Invoke(nTag);
+ Invalidate(session);
+ });
+ }
+ });
+ });
+ }
+
+ ///
+ /// Event raised when an error happened during detection
+ ///
+ /// iOS
+ /// iOS
+ public override void DidInvalidate(NFCTagReaderSession session, NSError error)
+ {
+ OnTagListeningStatusChanged?.Invoke(false);
+
+ var readerError = (NFCReaderError)(long)error.Code;
+ if (readerError != NFCReaderError.ReaderSessionInvalidationErrorFirstNDEFTagRead && readerError != NFCReaderError.ReaderSessionInvalidationErrorUserCanceled)
+ {
+ var alertController = UIAlertController.Create(Configuration.Messages.NFCSessionInvalidated, error.LocalizedDescription.ToLower().Equals(SessionTimeoutMessage) ? Configuration.Messages.NFCSessionTimeout : error.LocalizedDescription, UIAlertControllerStyle.Alert);
+ alertController.AddAction(UIAlertAction.Create(Configuration.Messages.NFCSessionInvalidatedButton, UIAlertActionStyle.Default, null));
+ OniOSReadingSessionCancelled?.Invoke(null, EventArgs.Empty);
+ DispatchQueue.MainQueue.DispatchAsync(() =>
+ {
+ GetCurrentController().PresentViewController(alertController, true, null);
+ });
+ }
+ else if (readerError == NFCReaderError.ReaderSessionInvalidationErrorUserCanceled && !_customInvalidation)
+ OniOSReadingSessionCancelled?.Invoke(null, EventArgs.Empty);
+ }
+
+ ///
+ /// Write or Clear a NDEF message
+ ///
+ ///
+ ///
+ /// Clear Message
+ /// Make a tag read-only
+ internal void WriteOrClearMessage(INFCTag tag, ITagInfo tagInfo, bool clearMessage = false, bool makeReadOnly = false)
+ {
+ if (NfcSession == null)
+ return;
+
+ if (tag == null)
+ {
+ Invalidate(NfcSession, Configuration.Messages.NFCErrorMissingTag);
+ return;
+ }
+
+ if (tagInfo == null || (!clearMessage && tagInfo.Records.Any(record => record.Payload == null)))
+ {
+ Invalidate(NfcSession, Configuration.Messages.NFCErrorMissingTagInfo);
+ return;
+ }
+
+ var ndefTag = NfcNdefTagExtensions.GetNdefTag(tag);
+ if (ndefTag == null)
+ {
+ Invalidate(NfcSession, Configuration.Messages.NFCErrorNotCompliantTag);
+ return;
+ }
+
+ try
+ {
+ if (!ndefTag.Available)
+ {
+ NfcSession.ConnectTo(tag, (error) =>
+ {
+ if (error != null)
+ {
+ Invalidate(NfcSession, error.LocalizedDescription);
+ return;
+ }
+
+ ExecuteWriteOrClear(NfcSession, ndefTag, tagInfo, clearMessage);
+ });
+ }
+ else
+ {
+ ExecuteWriteOrClear(NfcSession, ndefTag, tagInfo, clearMessage);
+ }
+
+ if (!clearMessage && makeReadOnly)
+ {
+ MakeTagReadOnly(NfcSession, tag, ndefTag);
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine(ex.StackTrace);
+ }
+ finally
+ {
+ OnTagDisconnected?.Invoke(null, EventArgs.Empty);
+ }
+ }
+
+ #region Private
+
+ ///
+ /// Returns the current iOS controller
+ ///
+ /// Object
+ UIViewController GetCurrentController()
+ {
+ var window = UIApplication.SharedApplication.KeyWindow;
+ var vc = window.RootViewController;
+ while (vc.PresentedViewController != null)
+ vc = vc.PresentedViewController;
+ return vc;
+ }
+
+ ///
+ /// Invalidate the session
+ ///
+ ///
+ /// Message to show
+ void Invalidate(NFCTagReaderSession session, string message = null)
+ {
+ _customInvalidation = true;
+ if (string.IsNullOrWhiteSpace(message))
+ session.InvalidateSession();
+ else
+ session.InvalidateSession(message);
+ }
+
+ ///
+ /// Writes or clears a TAG
+ ///
+ ///
+ ///
+ ///
+ /// Clear message
+ void ExecuteWriteOrClear(NFCTagReaderSession session, INFCNdefTag tag, ITagInfo tagInfo, bool clearMessage = false)
+ {
+ tag.QueryNdefStatus((status, capacity, error) =>
+ {
+ if (error != null)
+ {
+ Invalidate(session, error.LocalizedDescription);
+ return;
+ }
+
+ if (status == NFCNdefStatus.ReadOnly)
+ {
+ Invalidate(session, Configuration.Messages.NFCErrorReadOnlyTag);
+ return;
+ }
+
+ if (Convert.ToInt32(capacity) < NFCUtils.GetSize(tagInfo.Records))
+ {
+ Invalidate(session, Configuration.Messages.NFCErrorCapacityTag);
+ return;
+ }
+
+ NFCNdefMessage message = null;
+ if (!clearMessage)
+ {
+ session.AlertMessage = Configuration.Messages.NFCSuccessWrite;
+
+ var records = new List();
+ for (var i = 0; i < tagInfo.Records.Length; i++)
+ {
+ var record = tagInfo.Records[i];
+ if (NFCNdefPayloadExtensions.GetiOSPayload(record, Configuration) is NFCNdefPayload ndefPayload)
+ records.Add(ndefPayload);
+ }
+
+ if (records.Any())
+ message = new NFCNdefMessage(records.ToArray());
+ }
+ else
+ {
+ session.AlertMessage = Configuration.Messages.NFCSuccessClear;
+ message = NFCNdefMessageExtensions.EmptyNdefMessage;
+ }
+
+ if (message != null)
+ {
+ tag.WriteNdef(message, (error) =>
+ {
+ if (error != null)
+ {
+ Invalidate(session, error.LocalizedDescription);
+ return;
+ }
+
+ tagInfo.Records = NFCNdefPayloadExtensions.GetRecords(message.Records);
+ OnMessagePublished?.Invoke(tagInfo);
+ Invalidate(NfcSession);
+ });
+ }
+ else
+ Invalidate(session, Configuration.Messages.NFCErrorWrite);
+ });
+ }
+
+ ///
+ /// Make a tag read-only
+ /// WARNING: This operation is permanent
+ ///
+ ///
+ ///
+ ///
+ void MakeTagReadOnly(NFCTagReaderSession session, INFCTag tag, INFCNdefTag ndefTag)
+ {
+ session.ConnectTo(tag, (error) =>
+ {
+ if (error != null)
+ {
+ Console.WriteLine(error.LocalizedDescription);
+ return;
+ }
+
+ ndefTag.WriteLock((error) =>
+ {
+ if (error != null)
+ Console.WriteLine("Error when locking a tag on iOS: " + error.LocalizedDescription);
+ else
+ Console.WriteLine("Locking Successful!");
+ });
+ });
+ }
+
+ #endregion
+ }
+
+ ///
+ /// Old iOS implementation of (iOS < 13)
+ ///
+ public class NFCImplementation_Before_iOS13 : NFCNdefReaderSessionDelegate, INFC
+ {
+ public const string SessionTimeoutMessage = "session timeout";
+
+ private bool _isWriting;
+ private bool _isFormatting;
+ private bool _customInvalidation = false;
+ private INFCNdefTag _tag;
+
+ public event EventHandler OnTagConnected;
+ public event EventHandler OnTagDisconnected;
+ public event NdefMessageReceivedEventHandler OnMessageReceived;
+ public event NdefMessagePublishedEventHandler OnMessagePublished;
+ public event TagDiscoveredEventHandler OnTagDiscovered;
+ public event EventHandler OniOSReadingSessionCancelled;
+ public event OnNfcStatusChangedEventHandler OnNfcStatusChanged;
+ public event TagListeningStatusChangedEventHandler OnTagListeningStatusChanged;
+
+ NFCNdefReaderSession NfcSession { get; set; }
+
+ ///
+ /// Checks if NFC Feature is available
+ ///
+ public bool IsAvailable => NFCNdefReaderSession.ReadingAvailable;
+
+ ///
+ /// Checks if NFC Feature is enabled
+ ///
+ public bool IsEnabled => IsAvailable;
+
+ ///
+ /// Checks if writing mode is supported
+ ///
+ public bool IsWritingTagSupported => NFCUtils.IsWritingSupported();
+
+ ///
+ /// NFC configuration
+ ///
+ public NfcConfiguration Configuration { get; private set; }
+
+ ///
+ /// Default constructor
+ ///
+ public NFCImplementation_Before_iOS13()
+ {
+ Configuration = NfcConfiguration.GetDefaultConfiguration();
+ }
+
+ ///
+ /// Update NFC configuration
+ ///
+ ///
+ public void SetConfiguration(NfcConfiguration configuration) => Configuration.Update(configuration);
+
+ ///
+ /// Starts tags detection
+ ///
+ public void StartListening()
+ {
+ _customInvalidation = false;
+ _isWriting = false;
+ _isFormatting = false;
+
+ NfcSession = new NFCNdefReaderSession(this, DispatchQueue.CurrentQueue, true)
+ {
+ AlertMessage = Configuration.Messages.NFCDialogAlertMessage
+ };
+
+ NfcSession?.BeginSession();
+ OnTagListeningStatusChanged?.Invoke(true);
+ }
+
+ ///
+ /// Stops tags detection
+ ///
+ public void StopListening()
+ {
+ NfcSession?.InvalidateSession();
+ }
+
+ ///
+ /// Starts tag publishing (writing or formatting)
+ ///
+ /// Format tag
+ public void StartPublishing(bool clearMessage = false)
+ {
+ if (!IsAvailable)
+ {
+ throw new InvalidOperationException(Configuration.Messages.NFCWritingNotSupported);
+ }
+
+ _customInvalidation = false;
+ _isWriting = true;
+ _isFormatting = clearMessage;
+
+ NfcSession = new NFCNdefReaderSession(this, DispatchQueue.CurrentQueue, true)
+ {
+ AlertMessage = Configuration.Messages.NFCDialogAlertMessage
+ };
+ NfcSession?.BeginSession();
+ OnTagListeningStatusChanged?.Invoke(true);
+ }
+
+ ///
+ /// Stops tag publishing
+ ///
+ public void StopPublishing()
+ {
+ _isWriting = _isFormatting = _customInvalidation = false;
+ _tag = null;
+ NfcSession?.InvalidateSession();
+ }
+
+ ///
+ /// Publish or write a message on a tag
+ ///
+ /// see
+ /// Make a tag read-only
+ public void PublishMessage(ITagInfo tagInfo, bool makeReadOnly = false) => WriteOrClearMessage(_tag, tagInfo, false, makeReadOnly);
+
+ ///
+ /// Format tag
+ ///
+ /// see
+ public void ClearMessage(ITagInfo tagInfo) => WriteOrClearMessage(_tag, tagInfo, true);
+
+ ///
+ /// Event raised when NDEF messages are detected
+ ///
+ /// iOS
+ /// Array of iOS
+ public override void DidDetect(NFCNdefReaderSession session, NFCNdefMessage[] messages)
+ {
+ OnTagConnected?.Invoke(null, EventArgs.Empty);
+
+ if (messages != null && messages.Length > 0)
+ {
+ var first = messages[0];
+ var tagInfo = new TagInfo
+ {
+ IsWritable = false,
+ Records = NFCNdefPayloadExtensions.GetRecords(first.Records)
+ };
+ OnMessageReceived?.Invoke(tagInfo);
+ }
+
+ OnTagDisconnected?.Invoke(null, EventArgs.Empty);
+ }
+
+ ///
+ /// Event raised when NFC tag detected
+ ///
+ /// iOS
+ /// Array of iOS
+ public override void DidDetectTags(NFCNdefReaderSession session, INFCNdefTag[] tags)
+ {
+ _customInvalidation = false;
+ _tag = tags.First();
+
+ session.ConnectToTag(_tag, error =>
+ {
+ if (error != null)
+ {
+ Invalidate(session, error.LocalizedDescription);
+ return;
+ }
+
+ if (_tag == null)
+ {
+ Invalidate(session, Configuration.Messages.NFCErrorNotCompliantTag);
+ return;
+ }
+
+ _tag.QueryNdefStatus((status, capacity, ndefError) =>
+ {
+ if (ndefError != null)
+ {
+ Invalidate(session, ndefError.LocalizedDescription);
+ return;
+ }
+
+ var isNdefSupported = status != NFCNdefStatus.NotSupported;
+
+ var identifier = NfcNdefTagExtensions.GetTagIdentifier(_tag);
+ var nTag = new TagInfo(identifier, isNdefSupported)
+ {
+ IsWritable = status == NFCNdefStatus.ReadWrite,
+ Capacity = Convert.ToInt32(capacity)
+ };
+
+ if (!isNdefSupported)
+ {
+ session.AlertMessage = Configuration.Messages.NFCErrorNotSupportedTag;
+
+ OnMessageReceived?.Invoke(nTag);
+ Invalidate(session);
+ return;
+ }
+
+ if (_isWriting)
+ {
+ // Write mode
+ OnTagDiscovered?.Invoke(nTag, _isFormatting);
+ }
+ else
+ {
+ // Read mode
+ _tag.ReadNdef((message, readError) =>
+ {
+ if (readError != null)
+ {
+ Invalidate(session, readError.Code == (int)NFCReaderError.NdefReaderSessionErrorZeroLengthMessage
+ ? Configuration.Messages.NFCErrorEmptyTag
+ : Configuration.Messages.NFCErrorRead);
+ return;
+ }
+
+ session.AlertMessage = Configuration.Messages.NFCSuccessRead;
+
+ nTag.Records = NFCNdefPayloadExtensions.GetRecords(message?.Records);
+ OnMessageReceived?.Invoke(nTag);
+ Invalidate(session);
+ });
+ }
+ });
+ });
+ }
+
+ ///
+ /// Event raised when an error happened during detection
+ ///
+ /// iOS
+ /// iOS
+ public override void DidInvalidate(NFCNdefReaderSession session, NSError error)
+ {
+ OnTagListeningStatusChanged?.Invoke(false);
+
+ var readerError = (NFCReaderError)(long)error.Code;
+ if (readerError != NFCReaderError.ReaderSessionInvalidationErrorFirstNDEFTagRead && readerError != NFCReaderError.ReaderSessionInvalidationErrorUserCanceled)
+ {
+ var alertController = UIAlertController.Create(Configuration.Messages.NFCSessionInvalidated, error.LocalizedDescription.ToLower().Equals(SessionTimeoutMessage) ? Configuration.Messages.NFCSessionTimeout : error.LocalizedDescription, UIAlertControllerStyle.Alert);
+ alertController.AddAction(UIAlertAction.Create(Configuration.Messages.NFCSessionInvalidatedButton, UIAlertActionStyle.Default, null));
+ OniOSReadingSessionCancelled?.Invoke(null, EventArgs.Empty);
+ DispatchQueue.MainQueue.DispatchAsync(() =>
+ {
+ GetCurrentController().PresentViewController(alertController, true, null);
+ });
+ }
+ else if (readerError == NFCReaderError.ReaderSessionInvalidationErrorUserCanceled && !_customInvalidation)
+ {
+ OniOSReadingSessionCancelled?.Invoke(null, EventArgs.Empty);
+ }
+ }
+
+ ///
+ /// Write or Clear a NDEF message
+ ///
+ ///
+ ///
+ /// Clear Message
+ /// Make a tag read-only
+ internal void WriteOrClearMessage(INFCNdefTag tag, ITagInfo tagInfo, bool clearMessage = false, bool makeReadOnly = false)
+ {
+ if (NfcSession == null)
+ {
+ return;
+ }
+
+ if (tag == null)
+ {
+ Invalidate(NfcSession, Configuration.Messages.NFCErrorMissingTag);
+ return;
+ }
+
+ if (tagInfo == null || (!clearMessage && tagInfo.Records.Any(record => record.Payload == null)))
+ {
+ Invalidate(NfcSession, Configuration.Messages.NFCErrorMissingTagInfo);
+ return;
+ }
+
+ if (_tag == null)
+ {
+ Invalidate(NfcSession, Configuration.Messages.NFCErrorNotCompliantTag);
+ return;
+ }
+
+ try
+ {
+ if (!_tag.Available)
+ {
+ NfcSession.ConnectToTag(_tag, (error) =>
+ {
+ if (error != null)
+ {
+ Invalidate(NfcSession, error.LocalizedDescription);
+ return;
+ }
+
+ ExecuteWriteOrClear(NfcSession, _tag, tagInfo, clearMessage);
+ });
+ }
+ else
+ {
+ ExecuteWriteOrClear(NfcSession, _tag, tagInfo, clearMessage);
+ }
+
+ if (!clearMessage && makeReadOnly)
+ {
+ MakeTagReadOnly(NfcSession, tag);
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine(ex.StackTrace);
+ }
+ finally
+ {
+ OnTagDisconnected?.Invoke(null, EventArgs.Empty);
+ }
+ }
+
+ #region Private
+
+ ///
+ /// Returns the current iOS controller
+ ///
+ /// Object
+ UIViewController GetCurrentController()
+ {
+ var window = UIApplication.SharedApplication.KeyWindow;
+ var vc = window.RootViewController;
+ while (vc.PresentedViewController != null)
+ vc = vc.PresentedViewController;
+ return vc;
+ }
+
+ ///
+ /// Writes or clears a TAG
+ ///
+ ///
+ ///
+ ///
+ /// Clear message
+ private void ExecuteWriteOrClear(NFCNdefReaderSession session, INFCNdefTag tag, ITagInfo tagInfo, bool clearMessage = false)
+ {
+ tag.QueryNdefStatus((status, capacity, error) =>
+ {
+ if (error != null)
+ {
+ Invalidate(session, error.LocalizedDescription);
+ return;
+ }
+
+ if (status == NFCNdefStatus.ReadOnly)
+ {
+ Invalidate(session, Configuration.Messages.NFCErrorReadOnlyTag);
+ return;
+ }
+
+ if (Convert.ToInt32(capacity) < NFCUtils.GetSize(tagInfo.Records))
+ {
+ Invalidate(session, Configuration.Messages.NFCErrorCapacityTag);
+ return;
+ }
+
+ NFCNdefMessage message = null;
+ if (!clearMessage)
+ {
+ session.AlertMessage = Configuration.Messages.NFCSuccessWrite;
+
+ var records = new List();
+ for (var i = 0; i < tagInfo.Records.Length; i++)
+ {
+ var record = tagInfo.Records[i];
+ if (NFCNdefPayloadExtensions.GetiOSPayload(record, Configuration) is NFCNdefPayload ndefPayload)
+ records.Add(ndefPayload);
+ }
+
+ if (records.Any())
+ message = new NFCNdefMessage(records.ToArray());
+ }
+ else
+ {
+ session.AlertMessage = Configuration.Messages.NFCSuccessClear;
+ message = NFCNdefMessageExtensions.EmptyNdefMessage;
+ }
+
+ if (message != null)
+ {
+ tag.WriteNdef(message, (error) =>
+ {
+ if (error != null)
+ {
+ Invalidate(session, error.LocalizedDescription);
+ return;
+ }
+
+ tagInfo.Records = NFCNdefPayloadExtensions.GetRecords(message.Records);
+ OnMessagePublished?.Invoke(tagInfo);
+ Invalidate(NfcSession);
+ });
+ }
+ else
+ Invalidate(session, Configuration.Messages.NFCErrorWrite);
+ });
+ }
+
+ ///
+ /// Make a tag read-only
+ /// WARNING: This operation is permanent
+ ///
+ ///
+ ///
+ ///
+ private void MakeTagReadOnly(NFCNdefReaderSession session, INFCNdefTag tag)
+ {
+ session.ConnectToTag(tag, (error) =>
+ {
+ if (error != null)
+ {
+ Console.WriteLine(error.LocalizedDescription);
+ return;
+ }
+
+ tag.WriteLock((error) =>
+ {
+ if (error != null)
+ Console.WriteLine("Error when locking a tag on iOS: " + error.LocalizedDescription);
+ else
+ Console.WriteLine("Locking Successful!");
+ });
+ });
+ }
+
+ ///
+ /// Invalidate the session
+ ///
+ ///
+ /// Message to show
+ void Invalidate(NFCNdefReaderSession session, string message = null)
+ {
+ _customInvalidation = true;
+ if (string.IsNullOrWhiteSpace(message))
+ session.InvalidateSession();
+ else
+ session.InvalidateSession(message);
+ }
+
+ #endregion
+ }
+
+ ///
+ /// NFC Ndef Message extensions class
+ ///
+ internal static class NFCNdefMessageExtensions
+ {
+ ///
+ /// Convert an iOS into an array of bytes
+ ///
+ /// iOS
+ /// Array of bytes
+ public static byte[] ToByteArray(this NSData data)
+ {
+ var bytes = new byte[data.Length];
+ if (data.Length > 0) System.Runtime.InteropServices.Marshal.Copy(data.Bytes, bytes, 0, Convert.ToInt32(data.Length));
+ return bytes;
+ }
+
+ ///
+ /// Converte an iOS into an array of bytes
+ ///
+ /// iOS
+ /// Array of bytes
+ public static byte[] ToByteArray(this NFCNdefMessage message)
+ {
+ var records = message?.Records;
+
+ // Empty message: single empty record
+ if (records == null || records.Length == 0)
+ {
+ records = new NFCNdefPayload[] { null };
+ }
+
+ var m = new MemoryStream();
+ for (var i = 0; i < records.Length; i++)
+ {
+ var record = records[i];
+ var typeNameFormat = record?.TypeNameFormat ?? NFCTypeNameFormat.Empty;
+ var payload = record?.Payload;
+ var id = record?.Identifier;
+ var type = record?.Type;
+
+ var flags = (byte)typeNameFormat;
+
+ // Message begin / end flags. If there is only one record in the message, both flags are set.
+ if (i == 0)
+ flags |= 0x80; // MB (message begin = first record in the message)
+ if (i == records.Length - 1)
+ flags |= 0x40; // ME (message end = last record in the message)
+
+ // cf (chunked records) not supported yet
+
+ // SR (Short Record)?
+ if (payload == null || payload.Length < 255)
+ flags |= 0x10;
+
+ // ID present?
+ if (id != null && id.Length > 0)
+ flags |= 0x08;
+
+ m.WriteByte(flags);
+
+ // Type length
+ if (type != null)
+ m.WriteByte((byte)type.Length);
+ else
+ m.WriteByte(0);
+
+ // Payload length 1 byte (SR) or 4 bytes
+ if (payload == null)
+ {
+ m.WriteByte(0);
+ }
+ else
+ {
+ if ((flags & 0x10) != 0)
+ {
+ // SR
+ m.WriteByte((byte)payload.Length);
+ }
+ else
+ {
+ // No SR (Short Record)
+ var payloadLength = (uint)payload.Length;
+ m.WriteByte((byte)(payloadLength >> 24));
+ m.WriteByte((byte)(payloadLength >> 16));
+ m.WriteByte((byte)(payloadLength >> 8));
+ m.WriteByte((byte)(payloadLength & 0x000000ff));
+ }
+ }
+
+ // ID length
+ if (id != null && (flags & 0x08) != 0)
+ m.WriteByte((byte)id.Length);
+
+ // Type length
+ if (type != null && type.Length > 0)
+ m.Write(type.ToArray(), 0, (int)type.Length);
+
+ // ID data
+ if (id != null && id.Length > 0)
+ m.Write(id.ToArray(), 0, (int)id.Length);
+
+ // Payload data
+ if (payload != null && payload.Length > 0)
+ m.Write(payload.ToArray(), 0, (int)payload.Length);
+ }
+
+ return m.ToArray();
+ }
+
+ ///
+ /// Returns an empty iOS
+ ///
+ /// iOS
+ internal static NFCNdefMessage EmptyNdefMessage
+ {
+ get
+ {
+ var records = new NFCNdefPayload[1];
+ records[0] = NFCNdefPayloadExtensions.EmptyPayload;
+ return new NFCNdefMessage(records);
+ }
+ }
+ }
+
+ ///
+ /// NFC Ndef Payload Extensions Class
+ ///
+ internal static class NFCNdefPayloadExtensions
+ {
+ ///
+ /// Returns ndef payload into MimeType
+ ///
+ ///
+ /// string
+ public static string ToMimeType(this NFCNdefPayload payload)
+ {
+ switch (payload.TypeNameFormat)
+ {
+ case NFCTypeNameFormat.NFCWellKnown:
+ if (payload.Type.ToString() == "T")
+ return "text/plain";
+ break;
+ case NFCTypeNameFormat.Media:
+ return payload.Type.ToString();
+ }
+ return null;
+ }
+
+ ///
+ /// Returns Ndef payload into URI
+ ///
+ ///
+ ///
+ public static Uri ToUri(this NFCNdefPayload payload)
+ {
+ switch (payload.TypeNameFormat)
+ {
+ case NFCTypeNameFormat.NFCWellKnown:
+ if (payload.Type.ToString() == "U")
+ {
+ var uri = payload.Payload?.ParseWktUri();
+ return uri;
+ }
+ break;
+ case NFCTypeNameFormat.AbsoluteUri:
+ case NFCTypeNameFormat.Media:
+ var content = Encoding.UTF8.GetString(payload.Payload?.ToByteArray());
+ if (Uri.TryCreate(content, UriKind.RelativeOrAbsolute, out var result))
+ return result;
+
+ break;
+ }
+ return null;
+ }
+
+ ///
+ /// Returns complete URI of TNF_WELL_KNOWN, RTD_URI records.
+ ///
+ ///
+ private static Uri ParseWktUri(this NSData data)
+ {
+ var payload = data.ToByteArray();
+
+ if (payload.Length < 2)
+ return null;
+
+ var prefixIndex = payload[0] & 0xFF;
+ if (prefixIndex < 0 || prefixIndex >= _uri_Prefixes_Map.Length)
+ return null;
+
+ var prefix = _uri_Prefixes_Map[prefixIndex];
+ var suffix = Encoding.UTF8.GetString(CopyOfRange(payload, 1, payload.Length));
+
+ if (Uri.TryCreate(prefix + suffix, UriKind.Absolute, out var result))
+ return result;
+
+ return null;
+ }
+
+ ///
+ /// Copy a range of an array into another array
+ ///
+ /// Array of
+ /// Start
+ /// End
+ /// Array of
+ private static byte[] CopyOfRange(byte[] src, int start, int end)
+ {
+ var length = end - start;
+ var dest = new byte[length];
+ for (var i = 0; i < length; i++)
+ dest[i] = src[start + i];
+ return dest;
+ }
+
+ ///
+ /// NFC Forum "URI Record Type Definition"
+ /// This is a mapping of "URI Identifier Codes" to URI string prefixes,
+ /// per section 3.2.2 of the NFC Forum URI Record Type Definition document.
+ ///
+ private static readonly string[] _uri_Prefixes_Map = new string[] {
+ "", // 0x00
+ "http://www.", // 0x01
+ "https://www.", // 0x02
+ "http://", // 0x03
+ "https://", // 0x04
+ "tel:", // 0x05
+ "mailto:", // 0x06
+ "ftp://anonymous:anonymous@", // 0x07
+ "ftp://ftp.", // 0x08
+ "ftps://", // 0x09
+ "sftp://", // 0x0A
+ "smb://", // 0x0B
+ "nfs://", // 0x0C
+ "ftp://", // 0x0D
+ "dav://", // 0x0E
+ "news:", // 0x0F
+ "telnet://", // 0x10
+ "imap:", // 0x11
+ "rtsp://", // 0x12
+ "urn:", // 0x13
+ "pop:", // 0x14
+ "sip:", // 0x15
+ "sips:", // 0x16
+ "tftp:", // 0x17
+ "btspp://", // 0x18
+ "btl2cap://", // 0x19
+ "btgoep://", // 0x1A
+ "tcpobex://", // 0x1B
+ "irdaobex://", // 0x1C
+ "file://", // 0x1D
+ "urn:epc:id:", // 0x1E
+ "urn:epc:tag:", // 0x1F
+ "urn:epc:pat:", // 0x20
+ "urn:epc:raw:", // 0x21
+ "urn:epc:", // 0x22
+ "urn:nfc:", // 0x23
+ };
+
+ ///
+ /// Returns an empty iOS
+ ///
+ /// iOS
+ internal static NFCNdefPayload EmptyPayload => new(NFCTypeNameFormat.Empty, new NSData(), new NSData(), new NSData());
+
+ ///
+ /// Transforms an array of into an array of
+ ///
+ /// Array of
+ /// Array of
+ internal static NFCNdefRecord[] GetRecords(NFCNdefPayload[] records)
+ {
+ if (records == null)
+ return new NFCNdefRecord[0];
+
+ var results = new NFCNdefRecord[records.Length];
+ for (var i = 0; i < records.Length; i++)
+ {
+ var record = records[i];
+ var ndefRecord = new NFCNdefRecord
+ {
+ TypeFormat = (NFCNdefTypeFormat)record.TypeNameFormat,
+ Uri = records[i].ToUri()?.ToString(),
+ MimeType = records[i].ToMimeType(),
+ Payload = record.Payload?.ToByteArray()
+ };
+ results.SetValue(ndefRecord, i);
+ }
+ return results;
+ }
+
+ ///
+ /// Returns NDEF payload
+ ///
+ ///
+ ///
+ internal static NFCNdefPayload GetiOSPayload(NFCNdefRecord record, NfcConfiguration configuration)
+ {
+ if (record == null)
+ return null;
+
+ NFCNdefPayload payload = null;
+ switch (record.TypeFormat)
+ {
+ case NFCNdefTypeFormat.WellKnown:
+ var lang = record.LanguageCode;
+ if (string.IsNullOrWhiteSpace(lang)) lang = configuration.DefaultLanguageCode;
+ var langData = Encoding.ASCII.GetBytes(lang.Substring(0, 2));
+ var payloadData = new byte[] { 0x02 }.Concat(langData).Concat(record.Payload).ToArray();
+ payload = new NFCNdefPayload(NFCTypeNameFormat.NFCWellKnown, NSData.FromString("T"), new NSData(), NSData.FromString(Encoding.UTF8.GetString(payloadData), NSStringEncoding.UTF8));
+ break;
+ case NFCNdefTypeFormat.Mime:
+ payload = new NFCNdefPayload(NFCTypeNameFormat.Media, record.MimeType, new NSData(), NSData.FromArray(record.Payload));
+ break;
+ case NFCNdefTypeFormat.Uri:
+ payload = NFCNdefPayload.CreateWellKnownTypePayload(NSUrl.FromString(Encoding.UTF8.GetString(record.Payload)));
+ break;
+ case NFCNdefTypeFormat.External:
+ payload = new NFCNdefPayload(NFCTypeNameFormat.NFCExternal, record.ExternalType, new NSData(), NSData.FromString(Encoding.UTF8.GetString(record.Payload), NSStringEncoding.UTF8));
+ break;
+ case NFCNdefTypeFormat.Empty:
+ payload = EmptyPayload;
+ break;
+ case NFCNdefTypeFormat.Unknown:
+ case NFCNdefTypeFormat.Unchanged:
+ case NFCNdefTypeFormat.Reserved:
+ default:
+ break;
+ }
+ return payload;
+ }
+ }
+
+ ///
+ /// NFC Tag Extensions Class
+ ///
+ internal static class NfcNdefTagExtensions
+ {
+ ///
+ /// Get Ndef tag
+ ///
+ ///
+ ///
+ internal static INFCNdefTag GetNdefTag(INFCTag tag)
+ {
+ if (tag == null || !tag.Available)
+ return null;
+
+ INFCNdefTag ndef;
+
+#if NET6_0_OR_GREATER
+ if (tag.Type == CoreNFC.NFCTagType.MiFare)
+ ndef = tag.AsNFCMiFareTag;
+ else if (tag.Type == CoreNFC.NFCTagType.Iso7816Compatible)
+ ndef = tag.AsNFCIso7816Tag;
+ else if (tag.Type == CoreNFC.NFCTagType.Iso15693)
+ ndef = tag.AsNFCIso15693Tag;
+ else if (tag.Type == CoreNFC.NFCTagType.FeliCa)
+ ndef = tag.AsNFCFeliCaTag;
+ else
+ ndef = null;
+#else
+ if (tag.GetNFCMiFareTag() != null)
+ ndef = tag.GetNFCMiFareTag();
+ else if (tag.GetNFCIso7816Tag() != null)
+ ndef = tag.GetNFCIso7816Tag();
+ else if (tag.GetNFCIso15693Tag() != null)
+ ndef = tag.GetNFCIso15693Tag();
+ else if (tag.GetNFCFeliCaTag() != null)
+ ndef = tag.GetNFCFeliCaTag();
+ else
+ ndef = null;
+#endif
+
+ return ndef;
+ }
+
+ ///
+ /// Returns NFC Tag identifier
+ ///
+ ///
+ /// Tag identifier
+ internal static byte[] GetTagIdentifier(INFCNdefTag tag)
+ {
+ byte[] identifier = null;
+ if (tag is INFCMiFareTag mifareTag)
+ {
+ identifier = mifareTag.Identifier.ToByteArray();
+ }
+ else if (tag is INFCFeliCaTag felicaTag)
+ {
+ identifier = felicaTag.CurrentIdm.ToByteArray();
+ }
+ else if (tag is INFCIso15693Tag iso15693Tag)
+ {
+ identifier = iso15693Tag.Identifier.ToByteArray().Reverse().ToArray();
+ }
+ else if (tag is INFCIso7816Tag iso7816Tag)
+ {
+ identifier = iso7816Tag.Identifier.ToByteArray();
+ }
+ return identifier;
+ }
+ }
+}