Using NFC in Delphi 10 Seattle (and later) Android apps

Brian Long Consultancy & Training Services Ltd.
August 2014 (updated to Delphi 10 Seattle in September 2016)

This is an update to a Delphi XE7 version of the same article. Note that this update is generally identical in content. The only reason for the update is to simplify the process of building the project, as Delphi 10 Seattle alleviates the need to use additional compiled Java code. The processes outlined in the original XE5 article and in the updated XE6 article and updated XE7 article will work perfectly well, but with Delphi 10 Seattle things are simplified to the point where we can do everything within Delphi (with a smattering of JNI) without need to recourse to Java.

Accompanying source files available for Delphi XE5, Delphi XE6, Delphi XE7, Delphi XE8, Delphi 10 Seattle and Delphi 10.1 Berlin are available.

Contents

NFC Android Delphi

Introduction

Delphi Android apps can use Android APIs, both those already present in the RTL and others we can translate manually, in order to interact with parts of the Android system. This article will look at how we can make use of the NFC (Near Field Communications) API to access NFC tags in the same way as other Android NFC apps.

Through this article you should gain an insight into the techniques involved in building NFC apps and working with native Android NFC APIs, some of which are reasonably straightforward to employ and some of which involve metaphoricaly rolling up ones sleeves and getting ones hands dirty.

NFC tags have tag IDs so your app could just recognise an NFC tag and respond to it. However NFC tags can also have varying amounts of data written to them. This can include URLs, text or any other data. Your app can read this data and do things based upon what it finds.

An app that supports NFC tags can operate, broadly speaking, in one of two ways:

  1. It can be installed and have Android understand that it can respond to certain kinds of NFC tag technology. Then when the device scans a tag Android can launch the application automatically. If multiple applications are installed that can respond to the scanned tag then Android will initially present a chooser dialog so the user can choose the application that will respond to the tag. The user can also specify that one of the apps will be the default response in future. This auto-launch behaviour of Android is described as the tag dispatch system.
  2. It can be installed and not notify Android of its support for NFC tags and so not be auto-launched. Instead, when the app is manually launched it can allow NFC tags to be exclusively read by it and respond accordingly at that point to the tag and its content. This is descibed as the foreground dispatch system.

Either way the application setup will involve requiring permission to use the NFC sensor. In the project options dialog this is done in the Uses Permissions section:

Specifying the NFC permission requirement

If the use of NFC is crucial to the operation of your application you can also ensure this is known to the Android ecosystem. Indeed if you do this the Google Play online store will not list your app to devices that do not have NFC hardware. You do this by tweaking your Android manifest file, which as Delphi developers you cause to occur by editing the file AndroidManifest.template.xml that appears in the project directory when you first compile your mobile project.

Within the <manifest> element you add this child element, which will be a sibling to the <application> element:

<uses-feature android:name="android.hardware.nfc" android:required="true" />

The NFC API

Before embarking on any NFC coding we need to have access to the Android NFC API. This is not provided as part of Delphi but that is not an insurmountable obstacle as there are various Android API import tools available. These import tools produce import units that follow the model used by those provided in Delphi's RTL. Delphi's native ARMv7 code communicates with the Java world using the Java Bridge (or JNI Bridge or Native Bridge as it is sometimes called) so these import units are often called Java Bridge units.

The ins and outs of using the Java Bridge interface to represent Java classes using Delphi interfaces (one for the class methods and one for the interface methods, bound together by the TJavaGenericImport-based generic bridge class) are a bit beyond the scope of this particular article, but I did go into it in my CodeRage 8 session on using Android APIs from Delphi XE5:

One such Java API import tool, Java2Pas, was discussed on FMXExpress. It has been used by the person behind FMXExpress to import all APIs in the Android SDK. The results, for a whole range of Android SDK versions, can be found in a Github repository, which is a very useful (even if not particularly usable) resource.

As hinted by the parenthesised comment it turns out the NFC import units (and maybe others - I haven't yet had the need to check) need work to be suitable as input to the compiler. Indeed if you pull down, say the API level 19 import units, android.nfc.*.pas from https://github.com/FMXExpress/android-object-pascal-wrapper/tree/master/android-19 you'll find they won't compile for a whole heap of reasons including a gamut of circular unit references.

Anyway, not being one to be thwarted I took the interfaces and put them into units that were more in keeping with what the compiler was expecting, fixed some errors in the definitions and the resultant import units are included in the example downloads. I've coallesced what I needed from the 27 import units into two units called Androidapi.JNI.Nfc.pas and Androidapi.JNI.Nfc.Tech.pas.

Bear in mind that documentation for the various classes represented by these Java Bridge interface definitions can be found here and here on the Android API reference site. Additionally the Android documentation contains general NFC programming advice (targeted at Java programmers, but still useful background nevertheless), which can be found here.

The NFC tag dispatch system

Building an application that hooks into the NFC tag dispatch system is reasonably straightforward when you have the required API imports and have read the Android documentation on the matter.

In the downloadable source archive that accompanies this article there is an example app that uses the NFC tag dispatch system in the 1_TagDispatch subdirectory.

To tell Android to launch your application you must add additional intent filter definitions into the Android manifest for your application's main activity (the Android UI object that Delphi forms are rendered within). These are statements from your application saying that when a particular NFC action needs to be handled your activity can do the job. An intent is an Android system message represented as an object with varying amounts of associated data available through various methods.

When Android scans an NFC tag it parses the data on it to see if that data is of a known type, such as a URI or plain text. Then Android will locate a suitable activity to launch to handle the NFC tag. If more than one activity has registered an interest in the same tag capabilities then an activity chooser dialog is presented to the user.

In the case of an NFC tag containing NDEF (NFC Data Exchange Format) records Android will parse the data and work out the MIME type or URI. It will then launch an activity that has registered an intent filter for ACTION_NDEF_DISCOVERED (see Android documentation here) with the appropriate MIME type or URI scheme as additional criteria.

If no activity registered an intent filter for the MIME or URI specifics on the tag, or if the tag contains NDEF data not of a MIME/URI type, or if the NFC tag is of a known tag technology but does not contain NDEF data then Android will launch an activity registered for the ACTION_TECH_DISCOVERED intent (see Android documentation here).

Finally, if no activity handles either the ACTION_NDEF_DISCOVERED or ACTION_TECH_DISCOVERED intents then Android looks for one that will handle the ACTION_TAG_DISCOVERED intent (see Android documentation here).

Having seen the general description let's now see some specifics of what we'd need to add into AndroidManifest.template.xml to register for some of these intents.

Any intent filter we add needs to be added as a child element of the existing <activity> element and as a sibling of the existing intent filter for the LAUNCHER category of the MAIN action (this existing intent filter is how Android knows which activity to list for an application in the apps list).

ACTION_NDEF_DISCOVERED

Let's say you want your application to specifically respond to an NFC tag that has an NDEF record containing the URL of my web site: http://blong.com. This can be done by crafting an intent filter or two for ACTION_NDEF_DISCOVERED, for example:

<activity android:name="com.embarcadero.firemonkey.FMXNativeActivity"
       android:label="%activityLabel%"
       android:configChanges="orientation|keyboardHidden"
       android:launchMode="singleTop"
>

    <!-- Tell NativeActivity the name of our .so -->
    <meta-data android:name="android.app.lib_name"
       android:value="%libNameValue%" />
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>

    <intent-filter>
        <action android:name="android.nfc.action.NDEF_DISCOVERED"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <data android:scheme="http"
              android:host="blong.com" />
    </intent-filter>

    <intent-filter>
        <action android:name="android.nfc.action.NDEF_DISCOVERED"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <data android:scheme="http"
              android:host="www.blong.com" />
    </intent-filter>
</activity>

So here we are looking for any NDEF recognised data format where it is a http URI with either of the two host names that represent the site. If Android scans an NFC tag with either of those URLs then the app will be launched.

Similarly if you want your application to respond to an NFC tag containing plain text you define an intent filter specifying the plain text MIME type thus:

<activity android:name="com.embarcadero.firemonkey.FMXNativeActivity"
       android:label="%activityLabel%"
       android:configChanges="orientation|keyboardHidden"
       android:launchMode="singleTop"
>
    <!-- Tell NativeActivity the name of our .so -->
    <meta-data android:name="android.app.lib_name"
       android:value="%libNameValue%" />
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>

    <intent-filter>
        <action android:name="android.nfc.action.NDEF_DISCOVERED"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <data android:mimeType="text/plain" />
    </intent-filter>
</activity>

If you want your app to launch if either the specified URL or plain text is found on a scanned tag then you simply include both intent filters, and correspondingly you can add in any additional ones that make sense for your application as required.

If you want the URL response to be more general, so have the app launch for any URL, you would modify the intent filter like this:

<activity android:name="com.embarcadero.firemonkey.FMXNativeActivity"
       android:label="%activityLabel%"
       android:configChanges="orientation|keyboardHidden"
       android:launchMode="singleTop"
>
    <!-- Tell NativeActivity the name of our .so -->
    <meta-data android:name="android.app.lib_name"
       android:value="%libNameValue%" />
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>

    <intent-filter>
        <action android:name="android.nfc.action.NDEF_DISCOVERED"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <data android:scheme="http" />
    </intent-filter>
</activity>

You can use your imagination and try other combinations.

ACTION_TECH_DISCOVERED

If you aren't particularly interested in any specific data or data type you can instead look at the second of the two intents: ACTION_TECH_DISCOVERED. There are a variety of independently developed NFC tag technologies that may be present on a given tag and these are all represented by Android classes as listed on this documentation page. This intent focuses on responding to specified subsets of these technologies, which you list in an XML resource file that you need to add to the deployment profile for your mobile FireMonkey project.

Each technology subset you want to respond to is set up in a <tech-list> element in the resource file. You can see examples of how the resource file might look on this Android documentation page. For example if you want to respond to tags that support MifareUltralight, NfcA and Ndef technologies you would need an XML file that looks like this:

<?xml version="1.0" encoding="utf-8"?>

<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
    <tech-list>
        <tech>android.nfc.tech.MifareUltralight</tech>
        <tech>android.nfc.tech.NfcA</tech>
        <tech>android.nfc.tech.Ndef</tech>
    </tech-list>
</resources>

Alternatively if you want your app to respond to any technology at all you could set the resource file up with each technology specified alone in its own <tech-list>:

<?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-list>
    <tech-list>
        <tech>android.nfc.tech.NfcA</tech>
    </tech-list>
    <tech-list>
        <tech>android.nfc.tech.NfcB</tech>
    </tech-list>
    <tech-list>
        <tech>android.nfc.tech.NfcF</tech>
    </tech-list>
    <tech-list>
        <tech>android.nfc.tech.NfcV</tech>
    </tech-list>
    <tech-list>
        <tech>android.nfc.tech.Ndef</tech>
    </tech-list>
    <tech-list>
        <tech>android.nfc.tech.NdefFormatable</tech>
    </tech-list>
    <tech-list>
        <tech>android.nfc.tech.MifareClassic</tech>
    </tech-list>
    <tech-list>
        <tech>android.nfc.tech.MifareUltralight</tech>
    </tech-list>
</resources>

To set up this resource file for a Delphi Android app you first save it as an XML file. I recommend creating a res directory in your project directory and creating an xml subdirectory within that. So for example your resource file could be called res\xml\nfc_tech_discovered_filter.xml, relative to the project directory.

Once the file exists you can add it to the Delphi project deployment manager. In Delphi choose Project, Deployment to invoke the deployment manager. Hit the deployment manager toolbutton whose tooltip says: Add Files. Navigate to find your XML file and press Open to add your file to the deployed files list. Set up the deployment of this file by changing these column values in the deployment manager for your resource file:

This should leave the deployment manager ready to deploy your file:

Deploying the tech discovered filter

With the resource file set up you can now add the required intent filter into the Android manifest template file that refers to it via a <meta-data> element:

<activity android:name="com.embarcadero.firemonkey.FMXNativeActivity"
       android:label="%activityLabel%"
       android:configChanges="orientation|keyboardHidden"
       android:launchMode="singleTop"
>
    <!-- Tell NativeActivity the name of our .so -->
    <meta-data android:name="android.app.lib_name"
       android:value="%libNameValue%" />
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>

    <intent-filter>
        <action android:name="android.nfc.action.TECH_DISCOVERED" />
    </intent-filter>
    <meta-data
       android:name="android.nfc.action.TECH_DISCOVERED"
       android:resource="@xml/nfc_tech_discovered_filter">
    </meta-data>
</activity>

The @xml reference in the resource string tells Android to locate the resource file in the xml directory of the standard Android res directory that Android apps typically include, compressed inside the final .apk Android package file.

ACTION_TAG_DISCOVERED

If no application has responded to the scanned NFC tag through ACTION_NDEF_DISCOVERED or ACTION_TECH_DISCOVERED then the final option of ACTION_TAG_DISCOVERED comes into play. You can register an interest in this intent by changing the Android manifest template thus:

<activity android:name="com.embarcadero.firemonkey.FMXNativeActivity"
       android:label="%activityLabel%"
       android:configChanges="orientation|keyboardHidden"
       android:launchMode="singleTop"
>
    <!-- Tell NativeActivity the name of our .so -->
    <meta-data android:name="android.app.lib_name"
       android:value="%libNameValue%" />
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>

    <intent-filter>
        <action android:name="android.nfc.action.TAG_DISCOVERED"/>
    </intent-filter>
</activity>

Tag dispatch system application structure

Now we have the infrastructure nailed down we can look at what goes into the application to make it work when it gets automatically launched by Android when a suitable NFC tag is scanned.

The key to getting the app to work is to run some code when the application starts up and becomes active, so the form's OnActivate event would be helpful here.

In the OnActivate event handler we must get the intent object that was passed to our activity by Android as that contains a "parcelled" copy of the tag object for us to examine and make use of:

uses
  FMX.Helpers.Android,
  Androidapi.JNI.GraphicsContentViewText,
  ...

procedure TMainForm.FormActivate(Sender: TObject);
var
  Intent: JIntent;
begin
  Log.d('OnActivate');
  Intent := TAndroidHelper.Activity.getIntent;
  if Intent <> nil then
  begin
    ...
  end;
end;

That takes us to a crucial point of being able to get to work on the tag object.

We'll come back to how to get the tag object out of the intent object and then pull stuff from an NFC tag object later.

I should be honest here and say that the tag dispatch system sample does demonstrate a shortcoming here. If you have the app launched by Android for a tag then all is well. However if another NFC tag is scanned while the app is in the foreground then the app will crash, citing an error relating to there no being a looper for the current thread. I haven't yet followed this up to see if there is a convenient way of mitigating the problem. I did eventually resolve this. The Android manifest has an optional launchMode attribute for the activity, which needed to be set to singleTask.

The NFC foreground dispatch system

The NFC foreground dispatch system is often the more interesting aspect of NFC tag reading as this is where an application "owns" the NFC sensor and can read whatever tags are presented to the device. However it is also the more cumbersome to implement as there is a little bit of a wrinkle in Delphi's support of Java classes.

In the downloadable source archive that accompanies this article there is an app that uses the NFC foreground dispatch system in the 2_ForegroundDispatch subdirectory. We'll look at what is involved in building such an application in this section.

The basics as far as Android itself is concerned are as follows. To take control of the NFC sensor and enable the foreground dispatch system you call the default NFC adapter object's enableForegroundDispatch() method. To disable it you call the corresponding disableForegroundDispatch() method.

An application will be expected to enable the foreground dispatch system when it comes to the foreground and disable it when it leaves the foreground. If you use the IFMXApplicationEventService FMX platform service to set up an application event handler, this corresponds to when the event handler is triggered with the EnteredBackground and BecameActive values from the TApplicationEvent enumerated type values.

When the foreground dispatch system is active Android will scan NFC tags and send a suitable intent direct to your running activity by calling its onNewIntent() method. The intent will contained a "parcelled" copy of the tag object.

So that's how Android sees things - quite straightforward really. Fortunately when it comes to porting this over to Delphi it's not too much messier there.

However before we get onto the taxing parts (calling the NFC foreground dispatch system methods and receiving new intent objects) let's get some housekeeping sorted out. When the app starts it needs to be able to call NFC APIs and rather relies upon NFC being enabled, so let's check if that's the case. If it is not we can launch the system settings page for NFC to help the user out.

uses
  Androidapi.JNI.GraphicsContentViewText,
  Androidapi.JNI.Nfc,
  Androidapi.JNI.Toast,
...
  TMainForm = class(TForm)
  ...
  private
    NfcAdapter: JNfcAdapter;
  ...
  end;
...
procedure TMainForm.FormCreate(Sender: TObject);
begin
  NfcAdapter := TJNfcAdapter.JavaClass.getDefaultAdapter(TAndroidHelper.Context);
  if NfcAdapter = nil then
    raise Exception.Create('No NFC adapter present');
end;

function TMainForm.ApplicationEventHandler(AAppEvent: TApplicationEvent;
  AContext: TObject): Boolean;
begin
  Log.d('', Self, 'ApplicationEventHandler', Format('+ %s',
    [GetEnumName(TypeInfo(TApplicationEvent), Integer(AAppEvent))]));
  Result := True;
  case AAppEvent of
    ...
    TApplicationEvent.BecameActive:
    begin
      if NfcAdapter <> nil then
      begin
        if not NfcAdapter.isEnabled then
        begin
          if not NFCSettingsChecked then
          begin
            Toast('NFC is not enabled.' + LineFeed + 'Launching NFC settings.');
            TAndroidHelper.Activity.startActivity(
              TJIntent.JavaClass.init(StringToJString('android.settings.NFC_SETTINGS')));
          end
          else
          begin
            Toast('NFC functionality not available in this application due to system settings.');
          end;
        end
      end;
    end;
    ...
  end;
end;

To get access to the NFC adapter we call the getDefaultAdapter() class method of the NfcAdapter class, passing in our activity context. The adapter's isEnabled() method tells us if NFC is switched on. If not we construct an intent object passing in the activity action that shows the settings and pass that intent to our activity's startActivity() method (see here for more on this method) and leave the rest to the Android system and the user. Additionally a toast message is displayed, making use of my toast import unit, discussed elsewhere.

The Androidapi.JNI.GraphicsContentViewText.pas unit in the uses clause is part of the Delphi RTL and gives us access to JIntent, the Java Bridge representation of Android's Intent class. Androidapi.JNI.Nfc.pas is one the new pair of units crafted to make use of NFC (as mentioned earlier). Androidapi.JNI.Toast.pas is my import unit I made to import the Android toast message functionality.

Practical limitations of Delphi 10 Seattle's Android support

FireMonkey provides much functionality necessary for business applications needing to access data and display it to the user, but some common aspects of the Android OS are not 'wrapped up' in FireMonkey classes.

At this point you can call upon the various Android APIs that have been provided in the RTL, or craft or import new Java Bridge APIs if need be (as per the NFC API discussed earlier).

In cases where this isn't simply a case of calling into more APIs whose declarations need to declared with Java Bridge interfaces this poses a problem. Some examples of areas of the Android OS that are not wrapped up in Delphi 10 Seattle and are difficult to incorporate include:

In Delphi 10 Seattle these things and more require additional techniques, sometimes involving Java code to implement (though not for NFC!). If additional Java code is required to solve a certain problem, then some light hacking is needed to make use of it in a Delphi application, which tends to get in the IDE's way, so IDE building/installing of the application becomes interesting, and debugging becomes more challenging. These consequences tend to force you into managing a pair of synchronised projects in many cases (though not for NFC!):

Over and above this general Android integration isue there is also a problem with the level of support of Java APIs offered by Delphi's Java Bridge functionality. It turns out that Java Bridge can handle a good chunk of the Android API, but in the case of a method that takes a 2D array we become unstuck. Support exists for normal parameters and 1D arrays currently.

Controlling the foreground dispatch system

The NfcAdapter.enableForegroundDispatch() method has a signature containing 4 arguments:

  1. an activity that will receive intents for scanned NFC tags
  2. a PendingIntent object that describes the intent to use when an NFC tag is scanned
  3. an array of intent filters to control exactly which tags you would like to respond to, or null/nil to respond to all scanned NFC tags (for ACTION_NDEF_DISCOVERED and ACTION_NDEF_DISCOVERED, coded versions of what we saw earlier)
  4. an array of arrays of strings representing tech lists, used to control which NFC technology subsets in an NFC tag to respond to, or null/nil for any NFC tag technology (for ACTION_TECH_DISCOVERED, programatic equivalent of the XML resource file we saw earlier)

Notice that 4th parameter? It's a 2D array. This means that the Java Bridge is incapable of representing this API currently. So this means we are obliged to make the calls to it indirectly via lower level JNI calls.

...
  PendingIntent: JPendingIntent;
...
procedure TMainForm.FormCreate(Sender: TObject);
var
  ClassIntent: JIntent;
begin
  ...
  // Set up the pending intent needed for enabling NFC foreground dispatch
  ClassIntent := TJIntent.JavaClass.init(TAndroidHelper.Context, TAndroidHelper.Activity.getClass);
  PendingIntent := TJPendingIntent.JavaClass.getActivity(TAndroidHelper.Context, 0,
    ClassIntent.addFlags(TJIntent.JavaClass.FLAG_ACTIVITY_SINGLE_TOP), 0);
end;

{$REGION 'JNI substitute for calling NfcAdapter.enableForegroundDispatch'}
procedure TMainForm.EnableForegroundDispatch;
var
  PEnv: PJniEnv;
  AdapterClass: JNIClass;
  NfcAdapterObject, PendingIntentObject: JNIObject;
  MethodID: JNIMethodID;
begin
  // We can't just call the imported NfcAdapter method enableForegroundDispatch
  // as it will crash due to a shortcoming in the JNI Bridge, which does not
  // support 2D array parameters. So instead we call it via a manual JNI call.
  PEnv := TJNIResolver.GetJNIEnv;
  NfcAdapterObject := (NfcAdapter as ILocalObject).GetObjectID;
  PendingIntentObject := (PendingIntent as ILocalObject).GetObjectID;
  AdapterClass := PEnv^.GetObjectClass(PEnv, NfcAdapterObject);
  // Get the signature with:
  // javap -s -classpath <path_to_android_platform_jar> android.nfc.NfcAdapter
  MethodID := PEnv^.GetMethodID(
    PEnv, AdapterClass, 'enableForegroundDispatch',
    '(Landroid/app/Activity;Landroid/app/PendingIntent;' +
    '[Landroid/content/IntentFilter;[[Ljava/lang/String;)V');
  // Clean up
  PEnv^.DeleteLocalRef(PEnv, AdapterClass);
  // Finally call the target Java method
  PEnv^.CallVoidMethodA(PEnv, NfcAdapterObject, MethodID,
    PJNIValue(ArgsToJNIValues([JavaContext, PendingIntentObject, nil, nil])));
end;
{$ENDREGION}

Given the expectations of how the methods should be called it would be wise to plug these calls into the IFMXApplicationEventService handler.

function TMainForm.ApplicationEventHandler(AAppEvent: TApplicationEvent;
  AContext: TObject): Boolean;
begin
  ...
  Result := True;
  case AAppEvent of
    TApplicationEvent.BecameActive:
    begin
      ...
      if NfcAdapter <> nil then
      begin
        if not NfcAdapter.isEnabled then
        begin
          ...
        end
        else
          EnableForegroundDispatch
      end;
    end;
    TApplicationEvent.WillBecomeInactive:
    begin
      if NfcAdapter <> nil then
        NfcAdapter.disableForegroundDispatch(TAndroidHelper.Activity);
    end;
  end;
  ...
end;

Receiving new intents at runtime

To receive the intent that Android will send when the foreground dispatch system scans an NFC tag we need to hook into the underlying activity's onNewIntent() method. Fortunately the Android activity present in our FireMonkey Android application has some new support in Delphi 10 Seattle for allowing us to pick up onNewIntent() calls. New intents are now sent into the RTL cross-platorm messaging system. We use message-receiving code like this to pick up new intents and process them, This intent messaging is done via an additional (and seemingly undocumented) use of the Android-specific TMessageReceivedNotification message.

procedure TMainForm.FormCreate(Sender: TObject);
var
  ClassIntent: JIntent;
begin
  ...
  // Subscribe to the FMX message that is sent when onNewIntent is called
  // with an intent containing any of these 3 intent actions.
  // Support for this was added in Delphi 10 Seattle.
  MainActivity.registerIntentAction(TJNfcAdapter.JavaClass.ACTION_NDEF_DISCOVERED);
  MainActivity.registerIntentAction(TJNfcAdapter.JavaClass.ACTION_TECH_DISCOVERED);
  MainActivity.registerIntentAction(TJNfcAdapter.JavaClass.ACTION_TAG_DISCOVERED);
  MessageSubscriptionID := TMessageManager.DefaultManager.SubscribeToMessage(
    TMessageReceivedNotification, HandleIntentMessage);
  ...
end;

procedure TMainForm.HandleIntentMessage(const Sender: TObject;
  const M: TMessage);
var
  Intent: JIntent;
begin
  if M is TMessageReceivedNotification then
  begin
    Intent := TMessageReceivedNotification(M).Value;
    if Intent <> nil then
    begin
      if TJNfcAdapter.JavaClass.ACTION_NDEF_DISCOVERED.equals(Intent.getAction) or
         TJNfcAdapter.JavaClass.ACTION_TECH_DISCOVERED.equals(Intent.getAction) or
         TJNfcAdapter.JavaClass.ACTION_TAG_DISCOVERED.equals(Intent.getAction) then
      begin
        // Pass the NFC intent along to a dedicated handler

        OnNewNfcIntent(Intent);
      end;
    end;
  end;
end;

You can see that we use TMessageManager.DefaultManager to subscribe to TMessageReceivedNotification notifications passing in HandleIntentMessage as the handler before launching the activity, where TMessageReceivedNotification is actually a class inheriting from a common TMessage base notification message class. When the new intent comes in the message notification passes all the information to the HandleIntentMessage method as a TMessage and we cast to type TMessageReceivedNotification to access the intent reference contained inside. This is then passed along to OnNewNfcIntent.

This now leaves us in essentially the same position that we were in earlier with the tag dispatch system. We need to extract the tag and process it, which we'll look at shortly.

Building the app

The application can now be compiled to a native Android library and then deployed into an Android application package (.apk file) using the Project, Compile NFC_Sample and Project, Deploy libNFC_Sample.so menu items respectively.

If you wish you can skip those two steps and have them done implicitly by choosing the Run, Run (F9) or Run, Run Without Debugging menu item (Ctrl+Shift+F9). This will install the app and launch it. If it is already installed then the app will be re-installed, preserving any data that the app has already built up.

Working with NFC tag objects

After all the preamble of how to set up the code for the two approaches to coding up an NFC app using either the tag dispatch system or the foreground dispatch system we can now look at the NFC tag object itself.

Both approaches ultimately end up with a tag object parcelled into an intent object that is delivered into your code. We need to pull out the tag object and inspect it and see what we can glean from it.

Reading an NFC tag

Here is the code from the second sample app's OnNewNfcIntent method. The first job is to ensure that the intent really represents a scanned NFC tag by checking it against the 3 possible intent actions discussed above.

procedure TMainForm.OnNewNfcIntent(Intent: JIntent);
var
  TagParcel: JParcelable;
  Tag: JTag;
begin
  Log.d('TMainForm.OnNewIntent');
  TAndroidHelper.Activity.setIntent(Intent);
  Log.d('Intent action = %s', [JStringToString(Intent.getAction)]);
  PromptLabel.Visible := False;
  Log.d('Getting Tag parcel from the received Intent');
  TagParcel := Intent.getParcelableExtra(TJNfcAdapter.JavaClass.EXTRA_TAG);
  if TagParcel <> nil then
  begin
    Log.d('Wrapping tag from the parcel');
    Tag := TJTag.Wrap(TagParcel);
  end;
  InfoList.Items.Clear;
  NFCTagIdLabel.Text := HandleNfcTag(Tag,
    procedure (const Msg: string)
    var
      Strings: TStrings;
      I: Integer;
    begin
      Strings := TStringList.Create;
      try
        Strings.Text := Msg;
        for I := 0 to Pred(Strings.Count) do
        begin
          Log.d('Adding to UI: ' + Strings[I]);
          InfoList.Items.Add(Strings[I]);
        end;
      finally
        Strings.Free;
      end;
    end);
  InfoList.Visible := True;
end;

This foreground dispatch sample app starts with a prompt label suggesting the user scans a tag. If we now have an intent containing a scanned NFC tag object then the prompt label is cleared.

Now we get the tag from the intent as a Parcelable object (Parcelable is an interface implemented by all parcelable objects, including NFC Tag objects).

In Java we would simply cast the object back to a Tag but it's a little different using the Java Bridge interfaces in Delphi. To do the cast operation we pass the JParcelable interface to the TJTag class's Wrap method.

So now we have an NFC Tag object!

The code clears an information list box in preparation for some text to be written there and then calls a helper routine, HandleNfcTag, to get a dump of what's in the tag object. HandleNfcTag takes as arguments the tag object and also a reference to a routine that will be passed various data pulled from the tag object - in this case I'm passing in an anonymous method that adds in the new text into the information list box. When the function is done it will return a string containing the NFC tag's ID bytes turned into text, which are written onto another label.

Before we delve into the guts of the helper routine, here's the simple sample app showing information read from a scanned NFC tag:

NFC sample app

The top line of text is the text rendering of the NFC tag ID bytes. The rest is information pulled from the tag object. In this case the technologies supported by my tag are MifareUltralight, NfcA and Ndef, each of which are represented by Android classes, MifareUltralight, NfcA and Ndef, which themselves are represented by Java Bridge interface types in my Androidapi.JNI.Nfc.Tech.pas unit, JMifareUltralight, JNfcA and JNdef. You can see an overview of the various tag technology classes in the Android documentation here.

Here's the helper routine laid bare:

function HandleNfcTag(Tag: JTag; AddMsg: TTextLogger): string;
var
  I: Integer;
  JTagID: TJavaArray<Byte>;
  JTagTechList: TJavaObjectArray<JString>;
  JTechType: JString;
  TechType: String;
  TagTechList: TStrings;
const
  NFCA_Type = 'android.nfc.tech.NfcA';
  NFCB_Type = 'android.nfc.tech.NfcB';
  NFCF_Type = 'android.nfc.tech.NfcF';
  NFCV_Type = 'android.nfc.tech.NfcV';
  NFCNDef_Type = 'android.nfc.tech.Ndef';
  NFCIsoDep_Type = 'android.nfc.tech.IsoDep';
  MandatoryAndroidTechs: array[0..5] of string = (
    NFCA_Type,
    NFCB_Type,
    NFCF_Type,
    NFCV_Type,
    NFCNDef_Type,
    NFCIsoDep_Type);
begin
  if Tag <> nil then
  begin
    JTagID := Tag.getId;
    // Write out the ID byte values
    if JTagID <> nil then
    begin
      Result := 'NFC Tech Tag Id: ' + JavaBytesToString(JTagID);
    end;
    // Examine the tag
    JTagTechList := Tag.getTechList;
    TagTechList := TStringList.Create;
    try
      Log.d('Listing tag techs');
      if JTagTechList <> nil then
      begin
        for I := 0 to Pred(JTagTechList.Length) do
        begin
          JTechType := JTagTechList.Items[I];
          if JTechType <> nil then
          begin
            TechType := JStringToString(JTechType);
            TagTechList.Add(TechType);
            AddMsg(TechType);
          end;
        end;
        AddMsg('');
      end;
      // Process the different possible tag technology types and do something with them
      if TagTechList.IndexOf(NFCA_Type) >= 0 then
        AddMsg(DumpNFC_A(Tag));
      if TagTechList.IndexOf(NFCB_Type) >= 0 then
        AddMsg(DumpNFC_B(Tag));
      if TagTechList.IndexOf(NFCF_Type) >= 0 then
        AddMsg(DumpNFC_F(Tag));
      if TagTechList.IndexOf(NFCV_Type) >= 0 then
        AddMsg(DumpNFC_V(Tag));
      if TagTechList.IndexOf(NFCNDef_Type) >= 0 then
        AddMsg(DumpNDef(Tag));
      if TagTechList.IndexOf(NFCIsoDep_Type) >= 0 then
        AddMsg(DumpIsoDep(Tag));
    finally
      TagTechList.Free;
    end;
  end;
end;

The first thing it does is pull out the tag's ID, which comes out as a Java array of bytes. A helper routine in the unit (JavaBytesToString) iterates over each byte calling IntToHex on it to build up the textual ID.

The next task is to list out the tag technologies. The tag's getTechList() method returns them as a Java array of strings so a loop iterates across them adding Delphi string versions of them into a TStringList.

There are a variety of tag technologies as listed here and here. Both pages divide them into mandatory technologies, supported on all Android devices, and optional ones. However one of the pages lists seven mandatory tech types and the other lists just six, so the story is a little unclear. I haven't followed it up yet (it wasn't too important to me) so am currently unsure which is correct.

The sample code just pays attention to six mandatory tag technologies and checks for each one in turn. If the tag technology is supported by the tag then the tag is passed along to one of six helper routines designed to dump information for each of these tag tech types.

Five of the six dump routines are simple enough. They make use of the various methods exposed by the Java tag tech objects and add the results to the information string:

function DumpNFC_A(Tag: JTag): string;
var
  JATag: JNfcA;
begin
  Log.d('Found tech type A');
  // Technical details to be found at http://www.waazaa.org/download/fcd-14443-3.pdf
  JATag := TJNfcA.JavaClass.get(Tag);
  Result := Result + Format('NFC-A (ISO 14443-3A) data:%:0s%:0s', [LineFeed]);
  // Answer To Request of Type A
  Result := Result + Format('ATQA/SENS_RES: %s%s', [JavaBytesToStringReverse(JATag.getAtqa), LineFeed]);
  // Select Acknowledge
  Result := Result + Format('SAK/SEL_RES: %d%s', [JATag.getSak, LineFeed]);
  // ATQA+SAK can help ID a tag: http://nfc-tools.org/index.php?title=ISO14443A
end;

function DumpNFC_B(Tag: JTag): string;
var
  JBTag: JNfcB;
begin
  Log.d('Found tech type B');
  // Technical details to be found at http://www.waazaa.org/download/fcd-14443-3.pdf
  JBTag := TJNfcB.JavaClass.get(Tag);
  Result := Result + Format('NFC-B (ISO 14443-3B) data:%:0s%:0s', [LineFeed]);
  // Answer To Request of Type B
  Result := Result + Format('ATQB/SENSB_RES application data: %s%s', [JavaBytesToStringReverse(JBTag.getApplicationData),
    LineFeed]);
  Result := Result + Format('ATQB/SENSB_RES protocol info: %s%s', [JavaBytesToStringReverse(JBTag.getProtocolInfo), LineFeed]);
end;

function DumpNFC_F(Tag: JTag): string;
var
  JFTag: JNfcF;
begin
  Log.d('Found tech type F');
  JFTag := TJNfcF.JavaClass.get(Tag);
  Result := Result + Format('NFC-F (JIS 6319-4) data:%:0s%:0s', [LineFeed]);
  Result := Result + Format('System code: %s (%s)%s', [JavaBytesToString(JFTag.getSystemCode),
    JavaBytesToText(JFTag.getSystemCode), LineFeed]);
  Result := Result + Format('Manufacturer: %s (%s)%s', [JavaBytesToString(JFTag.getManufacturer),
    JavaBytesToText(JFTag.getManufacturer), LineFeed]);
end;

function DumpNFC_V(Tag: JTag): string;
var
  JVTag: JNfcV;
begin
  Log.d('Found tech type V');
  // Technical details to be found at http://www.waazaa.org/download/fcd-15693-3.pdf
  JVTag := TJNfcV.JavaClass.get(Tag);
  Result := Result + Format('NFC-V (ISO 15693) data:%:0s%:0s', [LineFeed]);
  // Data Storage Format ID
  Result := Result + Format('DSF ID: %x%s', [JVTag.getDsfId, LineFeed]);
  Result := Result + Format('Response flags: %x%s', [JVTag.getResponseFlags, LineFeed]);
end;

function DumpIsoDep(Tag: JTag): string;
var
  JIsoTag: JIsoDep;
begin
  Log.d('Found tech type ISO Dep');
  JIsoTag := TJIsoDep.JavaClass.get(Tag);
  Result := Result + Format('ISO-DEP (ISO 14443-4) data:%s', [LineFeed]);
  Result := Result + Format('ISO-DEP historical data for NfcA tags: %s%s',
    [JavaBytesToString(JIsoTag.getHistoricalBytes), LineFeed]);
  Result := Result + Format('Higher layer response data for NfcB tags: %s%s',
    [JavaBytesToString(JIsoTag.getHiLayerResponse), LineFeed]);
end;

It's the Ndef tag tech type dissection that contains all the interesting bits. As the documentation page illustrates there are four types of standardised NFC tags that support NDEF data. The methods in the class allow you to ascertain certain standard information, such as the tag type (one of the four standardised types), whether the tag is read-only or not, whether the tag can be made read-only, the maximum NDEF message size and so on.

You can download the formal NDEF specification to get full details on the layout of data in an NDEF message from the NFC Forum or you can get summary information on the Android documentation site. For the sake of the sample we'll try and keep it simple.

On an NDEF tag is an NDEF message containing one or more NDEF records. We'll look through the records and see if they contain some information we can dump out.

You can get hold of an object that represents this NDEF message in the form of an NdefMessage object. You can either read a cached NDEF message that was read when the tag was discovered, or read the current message, on the off-chance it may have been updated. The sample code reads the cached message.

An NdefMessage object offers a getRecords() method that returns a Java array of NdefRecord objects.

Unfortunately when iterating over the record array and trying to access each record therein we hit upon some crash bug whereby the record objects aren't surfaced correctly for us. Fortunately we can work around this problem by pulling the raw object ID from the array wrapper and manually wrapping it into a JNdefRecord Java Bridge object. Props to Daniel Magin for spotting that issue and working out a nifty way round it!

Note: this issue no longer occurs in Delphi 10.1 Berlin and 10.2 Tokyo, however I cannot check for sure that the issue perists in Delphi 10 Seattle.

function DumpNDef(Tag: JTag): string;
var
  JNDefTag: JNdef;
  JNDefTagType: JString;
  JNDefMsg: JNdefMessage;
  JNDefMsgRecords: TJavaObjectArray<JNdefRecord>;
  JNDefMsgRecord: JNdefRecord;
  JRecordType: TJavaArray<Byte>;
  I: Integer;
begin
  Log.d('Found tech type NDEF');
  JNDefTag := TJNdef.JavaClass.get(Tag);
  JNDefTagType := JNDefTag.getType;
  Log.d('NDEF type: ' + JStringToString(JNDefTagType));
  if JNDefTagType.equals(TJNDef.JavaClass.NFC_FORUM_TYPE_1) then
    Result := Result + Format('NFC Forum Type 1 data:%:0s%:1s', [LineFeed])
  else if JNDefTagType.equals(TJNDef.JavaClass.NFC_FORUM_TYPE_2) then
    Result := Result + Format('NFC Forum Type 2 data:%:0s%:1s', [LineFeed])
  else if JNDefTagType.equals(TJNDef.JavaClass.NFC_FORUM_TYPE_3) then
    Result := Result + Format('NFC Forum Type 3 data:%:0s%:1s', [LineFeed])
  else if JNDefTagType.equals(TJNDef.JavaClass.NFC_FORUM_TYPE_4) then
    Result := Result + Format('NFC Forum Type 4 data:%:0s%:1s', [LineFeed]);
  Result := Result + Format('Tag is %swritable%s', [IfThen(JNDefTag.isWritable, '', 'not '), LineFeed]);
  Result := Result + Format('Tag can%s be made read-only%s', [IfThen(JNDefTag.canMakeReadOnly, '', 'not'), LineFeed]);
  JNDefMsg := JNDefTag.getCachedNdefMessage;
  if JNDefMsg = nil then
    Result := Result + Format('No NDEF message found%s', [LineFeed])
  else
  begin
    JNDefMsgRecords := JNDefMsg.getRecords;
    if JNDefMsgRecords <> nil then
    begin
      for I := 0 to Pred(JNDefMsgRecords.Length) do
      begin
        // This does not work as expected - presumably a bug
        //JNDefMsgRecord := JNDefMsgRecords.Items[I];
        // So instead we wrap up the raw object ID manually
        JNDefMsgRecord := TJNdefRecord.Wrap(JNDefMsgRecords.GetRawItem(I));
        Result := Result + Format('NDEF message record %d: %s', [I, LineFeed]);
        case JNDefMsgRecord.getTnf of
          TJNdefRecordTNF_EMPTY:
            Result := Result + Format('  TNF_EMPTY (empty record)%s', [LineFeed]);
          TJNdefRecordTNF_MIME_MEDIA:
            Result := Result + Format('  TNF_MIME_MEDIA (RFC 2046 media-type BNF construct)%s', [LineFeed]);
          TJNdefRecordTNF_ABSOLUTE_URI:
            Result := Result + Format('  TNF_ABSOLUTE_URI (RFC 3986 absolute-URI BNF construct)%s', [LineFeed]);
          TJNdefRecordTNF_EXTERNAL_TYPE:
            Result := Result + Format('  TNF_EXTERNAL_TYPE (external type name)%s', [LineFeed]);
          TJNdefRecordTNF_UNKNOWN:
            Result := Result + Format('  TNF_UNKNOWN (unknown payload type)%s', [LineFeed]);
          TJNdefRecordTNF_UNCHANGED:
            Result := Result + Format('  TNF_UNCHANGED (an intermediate or final chunk of a chunked NDEF Record)%s', [LineFeed]);
          TJNdefRecordTNF_WELL_KNOWN:
          begin
            Result := Result + Format('  TNF_WELL_KNOWN (well-known RTD type name)%s', [LineFeed]);
            // NFC Record Type Definition technical spec.s are at http://members.nfc-forum.org/specs/spec_list
            JRecordType := JNDefMsgRecord.getType;
            if MatchingRecordType(JRecordType, TJNdefRecord.JavaClass.RTD_TEXT) then
              Result := Result + Format('  RTD_TEXT: %s%s', [DecodeText(JNDefMsgRecord.getPayload), LineFeed])
            else
            if
MatchingRecordType(JRecordType, TJNdefRecord.JavaClass.RTD_URI) then
              Result := Result + Format('  RTD_URI: %s%s', [DecodeURI(JNDefMsgRecord.getPayload), LineFeed])
            else
            if MatchingRecordType(JRecordType, TJNdefRecord.JavaClass.RTD_SMART_POSTER) then
              // TODO: Haven't pulled out the smart poster URI yet
              Result := Result + Format('  RTD_SMART_POSTER: %s%s', [JavaBytesToText(JNDefMsgRecord.getPayload), LineFeed])
            else
            if MatchingRecordType(JRecordType, TJNdefRecord.JavaClass.RTD_HANDOVER_SELECT) then
              Result := Result + Format('  RTD_HANDOVER_SELECT%s', [LineFeed])
            else
            if MatchingRecordType(JRecordType, TJNdefRecord.JavaClass.RTD_HANDOVER_REQUEST) then
              Result := Result + Format('  RTD_HANDOVER_REQUEST%s', [LineFeed])
            else
            if MatchingRecordType(JRecordType, TJNdefRecord.JavaClass.RTD_HANDOVER_CARRIER) then
              Result := Result + Format('  RTD_HANDOVER_CARRIER%s', [LineFeed])
            else
            if MatchingRecordType(JRecordType, TJNdefRecord.JavaClass.RTD_ALTERNATIVE_CARRIER) then
              Result := Result + Format('  RTD_ALTERNATIVE_CARRIER%s', [LineFeed]);
          end;
        end;
      end;
    end;
  end;
end;

As you can see the record object offers a getTnf() method to learn its TNF or Type Name Format field. This is a 3 bit value with values available as constants in the NdefRecord class (see here). The TNF value dictates how you should parse the data in the rest of the record. In the case of TNF_WELL_KNOWN, which has a value of 1, the record is one of a number of well known types and so the NFC Forum's Record Type Definitions (RTD) kick in.

The RTD is a byte array and obtained through the record's getType() method. You match it to one of the RTD byte array fields offered by the NdefRecord class to see which known type it is, and thereby you can determine the NDEF message record payload format.

The NFC Forum makes available the payload format for plain text, URI and smart poster. The documentation there also confirms that the RTD byte arrays for text, URI and smart poster are byte equivalents of T ($54), U ($55) and Sp ($53, $70) respectively. The sample code caters for the first two of these three but doesn't cover smart poster format.

I'll leave the code that decodes text and URI NDEF records out of this article and refer you to the sample code to see it instead. It would serve no specific benefit to the business of how to access NFC tags when it is really just a case of parsing byte arrays.

The key thing to take away from this article is that Delphi apps can make good use of NFC tags in either mode of standard NFC reading you choose.

Writing an NFC tag

Before leaving the subject we probably ought to also look at how to write to an NFC tag.

A common approach is to write an NDEF record to the tag, assuming the tag supports the NDEF technology. So the actual sample shipped in the downloads for this article really looks like this:

NFC sample app

You can see text can be entered at the bottom of the form and the button can be used to write the text in an NDEF record in an NDEF message to an NDEF-compatible tag.

Incidentally, the sample app has an obvious shortcoming in that I haven't done anything to move the edit control up when the virtual keyboard appears on the screen, so when typing in text you can't see what you are typing into. However in this context that is an irrelevance.

The button has this code for an event handler:

procedure TMainForm.TagWriteButtonClick(Sender: TObject);
var
  TagParcel: JParcelable;
  Tag: JTag;
  Intent: JIntent;
begin
  if (NfcAdapter <> nil) and NfcAdapter.isEnabled then
  begin
    Intent := TAndroidHelper.Activity.getIntent;
    TagParcel := Intent.getParcelableExtra(TJNfcAdapter.JavaClass.EXTRA_TAG);
    if TagParcel <> nil then
    begin
      Log.d('Wrapping tag from the parcel');
      Tag := TJTag.Wrap(TagParcel);
      if not WriteTagText(TagWriteEdit.Text, Tag) then
        raise Exception.Create('Error connecting to tag');
    end;
  end
  else
    raise Exception.Create('NFC is not available');
end;

It gets the activity's intent and pulls the NFC tag object from it. Note that in the earlier code we set the activity's intent to any new intent that we were delivered by the foreground dispatch system.

The tag then gets passed to a helper routine that looks like this:

function WriteTagText(const Msg: string; Tag: JTag): Boolean;
var
  NDef: JNdef;
  NDefMsg: JNdefMessage;
  NDefRecords: TJavaObjectArray<JNdefRecord>;
begin
  NDef := TJNdef.JavaClass.get(Tag);
  if NDef <> nil then
  begin
    try
      NDef.connect;
      Result := NDef.isConnected;
      if Result then
      begin
        NDefRecords := TJavaObjectArray<JNdefRecord>.Create(2);
        NDefRecords.Items[0] := EncodeText(Msg);
        NDefRecords.Items[1] := TJNdefRecord.JavaClass.createUri(StringToJString('http://blong.com'));
        NDefMsg := TJNdefMessage.JavaClass.init(NDefRecords);
        NDef.writeNdefMessage(NDefMsg);
        NDef.close;
      end;
    except
      on EJNIException do
        Result := False;
    end
  end
  else
    raise Exception.Create('This is not an NDEF-compatible tag!');
end;

This code tries to get an NDEF tag technology object for the scanned tag, which will only work if the tag is NDEF-compatible. Assuming we get the NDEF tag tech object we next open a connection to the tag in order to write to it, and also to later close the connection.

Assuming we establish a connection to the tag (which relies upon it still being there) we can build up a message to write. The NdefMessage class has a constructor that takes a Java array of NdefRecord objects, so we set up a suitable array of records.

In this sample app's case we write out 2 records - one is the text record generated by another helper routine, EncodeText, and one is a URI record generated by a static method of the NdefRecord class specifically for packaging up URIs in the correct way. So as well as the text entered by the user the NFC tag also gets a record containing my web site address.

Again, the details of encoding some text into a record aren't too important, but are available to read in the code sample. It just follows the information available in the NFC Forum text record type definition.

NFC sample app

Conclusion

You can extend the functionality of your application by taking advantage of available Android functionality, including support for scanning NFC tags on devices that have an appropriate NFC sensor.

Writing code in an app that is auto-launched by Android when an NFC tag is detected is quite straightforward once you have the API imports available.

However having a regular app set up support for priority NFC tag scanning using the NFC foreground dispatch system is not quite so straightforward but quite manageable.


Go back to the top of this page