Search code examples
androidiosxamarin.formsnfc

Xamarin.Forms NFC: Automatically Launch App and Read Tag Data on Android and iOS


I'm creating a Xamarin.Forms app where, upon placing an NFC card on the phone, the app should launch automatically and immediately read all tag data (ID and payload at least) in one cycle.

On some Android devices, the app launches as expected, but OnNewIntent in MainActivity is not triggered. And it also starts my app without reading any data. So somehow the tag is not detected.

On other Android phones, the app doesn't launch at all when placing the NFC card on the device.

I'm using the following code adapted from this project but have made significant changes.

My first question is that how can I ensure reliable app launching and data reading across Android devices? Are there any modifications needed to make OnNewIntent trigger consistently?

My second question is about achieving the same behavior on iOS. Is it possible to start the app and read NFC data in a single cycle on iOS with Xamarin.Forms, considering iOS NFC limitations.

NativeNFCAdapterService

using System;
using System.Linq;
using System.Threading.Tasks;
using NfcAdapter = Android.Nfc.NfcAdapter;
using Android.Content;
using Android.Nfc.Tech;
using Android.Nfc;
using Android.OS;
using NFCTestApp.Interfaces;
using NFCTestApp.Droid.Services;
using Xamarin.Essentials;
using Xamarin.Forms;
using NFCTestApp.Droid.Enums;
using System.IO;

[assembly: Dependency(typeof(NativeNFCAdapterService))]

namespace NFCTestApp.Droid.Services
{
    class NativeNFCAdapterService : INfcAdapter
    {
        private readonly MainActivity mainActivity = (MainActivity)Platform.CurrentActivity;
        private Lazy<NfcAdapter> lazynfcAdapter = new Lazy<NfcAdapter>(() => NfcAdapter.GetDefaultAdapter(Platform.CurrentActivity));
        private NfcAdapter NfcAdapter => lazynfcAdapter.Value;
        private PendingIntent pendingIntent;
        private IntentFilter[] writeTagFilters;
        private string[][] techList;
        private ReaderCallback readerCallback;

        public event Action<string> TagDiscovered;
        public event Action<string> AllDataRead;

        private NfcStatus NfcStatus => NfcAdapter == null ?
                                       NfcStatus.Unavailable : NfcAdapter.IsEnabled ?
                                       NfcStatus.Enabled : NfcStatus.Disabled;

        public static Tag DetectedTag { get; set; }

        public NativeNFCAdapterService()
        {
            Platform.ActivityStateChanged += Platform_ActivityStateChanged;
        }

        private void Platform_ActivityStateChanged(object sender, ActivityStateChangedEventArgs e)
        {
            switch (e.State)
            {
                case ActivityState.Resumed:
                    EnableForegroundDispatch();
                    break;

                case ActivityState.Paused:
                    DisableForegroundDispatch();
                    break;
            }
        }

        public void ConfigureNfcAdapter()
        {
            IntentFilter tagdiscovered = new IntentFilter(NfcAdapter.ActionTagDiscovered);
            IntentFilter ndefDiscovered = new IntentFilter(NfcAdapter.ActionNdefDiscovered);
            IntentFilter techDiscovered = new IntentFilter(NfcAdapter.ActionTechDiscovered);
            tagdiscovered.AddCategory(Intent.CategoryDefault);
            ndefDiscovered.AddCategory(Intent.CategoryDefault);
            techDiscovered.AddCategory(Intent.CategoryDefault);

            var intent = new Intent(mainActivity, mainActivity.Class).AddFlags(ActivityFlags.SingleTop);
            pendingIntent = PendingIntent.GetActivity(mainActivity, 0, intent, PendingIntentFlags.Immutable);

            techList = new string[][]
            {
                new string[] { nameof(NfcA) },
                new string[] { nameof(NfcB) },
                new string[] { nameof(NfcF) },
                new string[] { nameof(NfcV) },
                new string[] { nameof(IsoDep) },
                new string[] { nameof(NdefFormatable) },
                new string[] { nameof(MifareClassic) },
                new string[] { nameof(MifareUltralight) },
            };

            readerCallback = new ReaderCallback();
            readerCallback.OnTagDiscoveredEvent += HandleTagDiscovered;

            writeTagFilters = new IntentFilter[] { tagdiscovered, ndefDiscovered, techDiscovered };
        }

        public void DisableForegroundDispatch()
        {
            NfcAdapter?.DisableForegroundDispatch(Platform.CurrentActivity);
            NfcAdapter?.DisableReaderMode(Platform.CurrentActivity);
        }

        public void EnableForegroundDispatch()
        {
            if (pendingIntent == null || writeTagFilters == null || techList == null) { return; }

            NfcAdapter?.EnableForegroundDispatch(Platform.CurrentActivity, pendingIntent, writeTagFilters, techList);
            NfcAdapter?.EnableReaderMode(Platform.CurrentActivity, readerCallback, NfcReaderFlags.NfcA, null);

            Task.Run(async () => await ReadAllTagInfoAsync());
        }

        public void UnconfigureNfcAdapter()
        {
            Platform.ActivityStateChanged -= Platform_ActivityStateChanged;
        }

        public void HandleTagDiscovered(string tagId)
        {
            TagDiscovered?.Invoke(tagId);
        }

        public async Task SendAsync(byte[] bytes)
        {
            Ndef ndef = null;
            try
            {
                if (DetectedTag == null)
                    DetectedTag = await GetDetectedTag();

                ndef = Ndef.Get(DetectedTag);
                if (ndef == null) return;

                if (!ndef.IsWritable)
                {
                    await Application.Current.MainPage.DisplayAlert("Error", "Tag is readonly", "Ok");
                    return;
                }

                if (!ndef.IsConnected)
                {
                    await ndef.ConnectAsync();
                }

                await WriteToTag(ndef, bytes);
            }
            catch (IOException)
            {
                await Application.Current.MainPage.DisplayAlert("Error", "Transmission error - possibly due to movement.", "Ok");
            }
            catch (Exception)
            {
                await Application.Current.MainPage.DisplayAlert("Error", "Request error", "Ok");
            }
            finally
            {
                if (ndef?.IsConnected == true) ndef.Close();
                ndef = null;
                DetectedTag = null;
            }
        }

        private async Task<Tag> GetDetectedTag()
        {
            mainActivity.NfcTag = new TaskCompletionSource<Tag>();
            readerCallback.NFCTag = new TaskCompletionSource<Tag>();
            var tagDetectionTask = await Task.WhenAny(mainActivity.NfcTag.Task, readerCallback.NFCTag.Task);
            return await tagDetectionTask;
        }

        private async Task WriteToTag(Ndef ndef, byte[] chunkedBytes)
        {
            var ndefRecord = new NdefRecord(NdefRecord.TnfWellKnown, NdefRecord.RtdText?.ToArray(), Array.Empty<byte>(), chunkedBytes);
            NdefMessage message = new NdefMessage(new[] { ndefRecord });
            ndef.WriteNdefMessage(message);
            await Application.Current.MainPage.DisplayAlert("NFC", "Write Successful", "Ok");
        }

        public async Task<string> ReadAllTagInfoAsync()
        {
            if (DetectedTag == null)
            {
                DetectedTag = await GetDetectedTag();
            }

            var info = new System.Text.StringBuilder();
            info.AppendLine("Tech List:");
            foreach (var tech in DetectedTag.GetTechList())
            {
                info.AppendLine($"- {tech}");
            }

            Ndef ndef = Ndef.Get(DetectedTag);
            if (ndef != null)
            {
                info.AppendLine("NDEF Supported: Yes");
                info.AppendLine($"NDEF Type: {ndef.Type}");
                info.AppendLine($"Is Writable: {ndef.IsWritable}");
                info.AppendLine($"Max Size: {ndef.MaxSize} bytes");

                var ndefMessage = ndef.CachedNdefMessage;
                if (ndefMessage != null && ndefMessage.GetRecords().Any())
                {
                    foreach (var ndefRecord in ndefMessage.GetRecords())
                    {
                        info.AppendLine($"Payload: {System.Text.Encoding.UTF8.GetString(ndefRecord.GetPayload())}");
                    }
                }
            }
            else
            {
                info.AppendLine("NDEF Supported: No");
            }

            AllDataRead?.Invoke(info.ToString());

            return info.ToString();
        }
    }
}

ReaderCallback.cs

public class ReaderCallback : Java.Lang.Object, NfcAdapter.IReaderCallback
{
    public TaskCompletionSource<Tag> NFCTag { get; set; }
    public event Action<string> OnTagDiscoveredEvent;

    public void OnTagDiscovered(Tag tag)
    {
        var isSuccess = NFCTag?.TrySetResult(tag);
        if (NFCTag == null || !isSuccess.Value)
            NativeNFCAdapterService.DetectedTag = tag;

        byte[] tagIdBytes = tag.GetId();
        string tagId = BitConverter.ToString(tagIdBytes).Replace("-", "");
        OnTagDiscoveredEvent?.Invoke(tagId);
    }
}

MainActivity.cs

[MetaData(NfcAdapter.ActionTechDiscovered, Resource = "@xml/nfc_tech_filter")]
[IntentFilter(new[] { NfcAdapter.ActionTechDiscovered }, Categories = new[] { Intent.CategoryDefault }, DataMimeType = "text/plain")]
[IntentFilter(new[] { NfcAdapter.ActionNdefDiscovered }, Categories = new[] { Intent.CategoryDefault }, DataMimeType = "text/plain")]
[IntentFilter(new[] { NfcAdapter.ActionTagDiscovered }, Categories = new[] { Intent.CategoryDefault }, DataMimeType = "text/plain")]
[Activity(Label = "NFCTestApp", Icon = "@mipmap/icon", Theme = "@style/MainTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize)]
public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity
{
    public TaskCompletionSource<Tag> NfcTag { get; set; }

    protected override void OnCreate(Bundle savedInstanceState)
    {
        base.OnCreate(savedInstanceState);

        Xamarin.Essentials.Platform.Init(this, savedInstanceState);
        global::Xamarin.Forms.Forms.Init(this, savedInstanceState);
        LoadApplication(new App());
    }

    protected override void OnNewIntent(Intent intent)
    {
        System.Diagnostics.Debug.WriteLine("It is here");
        base.OnNewIntent(intent);

        Xamarin.Forms.Device.BeginInvokeOnMainThread(async () =>
        {
            await Xamarin.Forms.Application.Current.MainPage.DisplayAlert("NFC Tag Discovered", "A1", "OK");
        });
    }
}

AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="NFCTestApp.Droid">
    <uses-permission android:name="android.permission.NFC" />
    <application android:label="NFCTestApp" android:icon="@mipmap/icon">
        <meta-data android:name="android.nfc.action.TECH_DISCOVERED" android:resource="@xml/nfc_tech_filter" />
    </application>
</manifest>

here is the card info

EDIT: nfc_tech_filter.xml

<?xml version="1.0" encoding="utf-8" ?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
  <tech-list>
    <tech>android.nfc.tech.IsoDep</tech>
    <tech>android.nfc.tech.NfcA</tech>
    <tech>android.nfc.tech.NfcB</tech>
    <tech>android.nfc.tech.NfcF</tech>
    <tech>android.nfc.tech.NfcV</tech>
    <tech>android.nfc.tech.Ndef</tech>
    <tech>android.nfc.tech.NdefFormatable</tech>
    <tech>android.nfc.tech.MifareClassic</tech>
    <tech>android.nfc.tech.MifareUltralight</tech>
  </tech-list>
</resources>

Solution

  • Update Now that the nfc_tech_filter.xml has been upload it show the real problem.

    Each tech element in tech-list element is additive.

    And as you list all tech type in on tech-list then it technically impossible for a single tag to be a technology types at once as some of them are incompatible with each other.

    This is explained in the documentation but unfortunately first example in the documentation is wrong (I have raised a documentation bug with Google on this). The later examples are right.

    So you nfc_tech_filter.xml would be better like as you are looking for Ndef on a NTAG215

    <?xml version="1.0" encoding="utf-8" ?>
    <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
      <tech-list>
        <!--Match all these techs -->
        <tech>android.nfc.tech.NfcA</tech>
        <!--AND -->
        <tech>android.nfc.tech.Ndef</tech>
      </tech-list>
      <!--OR -->
      <tech-list>
        <tech>android.nfc.tech.NfcA</tech>
        <tech>android.nfc.tech.NdefFormatable</tech>
      </tech-list>
    </resources>
    

    There are two other possible problems, starting an App via manifest filters can deliver then Intent notification to one of two possible code locations, OnNewIntent is one but it can also be delivered just to the main Activity instead.

    This is because there are various other factors affecting where the Intent is delivered.

    From the Android Documentation:-

    This is called for activities that set launchMode to "singleTop" in their package, or if a client used the Intent#FLAG_ACTIVITY_SINGLE_TOP flag when calling startActivity(Intent). In either case, when the activity is re-launched while at the top of the activity stack instead of a new instance of the activity being started, onNewIntent() will be called on the existing instance with the Intent that was used to re-launch it.

    The main words here are "re-launched"

    So if you want to start your app via NFC then you should also check the Intent that the Main Activity was passed when it was started.

    You do this by calling getIntent() in the main activity when it starts, this is usually done in onCreate. Then you would getParcelableExtra(NfcAdapter.EXTRA_TAG) on this Intent to see if the initial Intent contained Data about the NFC Tag (You would null check the return of getParcelableExtra)

    If you are only interested in NDEF data the you could use NfcAdapter.EXTRA_NDEF_MESSAGES instead of the raw Tag as long as you have not turned of the parsing of NDEF by the NFC System Service.

    This leads to a second possible problem.

    Your code seems to use both the EnableForegroundDispatch and it's newer and better replacement EnableReaderMode. Your code should only use one or the other with EnableReaderMode being the better one to use. Under various different conditions this could mean that the ReaderCallback could be called instead of onNewIntent().

    I would remove all the code related to EnableForegroundDispatch