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; + } + } +}