There are a lot of usage scenarios: pass system , loyalty cards, transport cards, obtaining additional information about exhibits in the museum, password manager .
At the same time, the application on the phone emulating the card may be launched or not, and the screen of your phone may be locked.
For Xamarin Android, there are ready-made examples of a card emulator and reader .
Let's try using these examples to make 2 Xamarin Forms applications, an emulator and a reader, and solve the following problems in them:
- display data from the emulator on the reader screen
- display data from the reader on the emulator screen
- the emulator should work with an unreleased application and a locked screen
- control emulator settings
- launch the emulator application when a reader is detected
- checking the status of the nfc adapter and switching to the nfc settings
This article is about android, therefore, if you have an application also for iOS, then there should be a separate implementation.
The minimum of theory.
As written in the android documentation , starting with version 4.4 (kitkat), the ability to emulate ISO-DEP cards and process APDU commands has been added.
Card emulation is based on android services known as “HCE services”.
When a user attaches a device to an NFC reader, the android needs to understand which HCE service the reader wants to connect to. ISO / IEC 7816-4 describes an application selection method based on Application ID (AID).
If it’s interesting to delve into the beautiful world of byte arrays, here and here more about APDU commands. This article uses only a couple of the commands needed to exchange data.
Reader application
Let's start with the reader, because it is simpler.
We create a new project in Visual Studio like “Mobile App (Xamarin.Forms)”, then select the “Blank” template and leave only the “Android” checkmark in the “Platforms” section.
In the android project you need to do the following:
- The CardReader class contains several constants and the OnTagDiscovered method
- MainActivity - initialization of the CardReader class, as well as the OnPause and OnResume methods for turning the reader on / off when minimizing the application
- AndroidManifest.xml - permissions for nfc
And in the cross-platform project in the App.xaml.cs file:
- Method for displaying a message to the user
CardReader Class
using Android.Nfc; using Android.Nfc.Tech; using System; using System.Linq; using System.Text; namespace ApduServiceReaderApp.Droid.Services { public class CardReader : Java.Lang.Object, NfcAdapter.IReaderCallback { // ISO-DEP command HEADER for selecting an AID. // Format: [Class | Instruction | Parameter 1 | Parameter 2] private static readonly byte[] SELECT_APDU_HEADER = new byte[] { 0x00, 0xA4, 0x04, 0x00 }; // AID for our loyalty card service. private static readonly string SAMPLE_LOYALTY_CARD_AID = "F123456789"; // "OK" status word sent in response to SELECT AID command (0x9000) private static readonly byte[] SELECT_OK_SW = new byte[] { 0x90, 0x00 }; public async void OnTagDiscovered(Tag tag) { IsoDep isoDep = IsoDep.Get(tag); if (isoDep != null) { try { isoDep.Connect(); var aidLength = (byte)(SAMPLE_LOYALTY_CARD_AID.Length / 2); var aidBytes = StringToByteArray(SAMPLE_LOYALTY_CARD_AID); var command = SELECT_APDU_HEADER .Concat(new byte[] { aidLength }) .Concat(aidBytes) .ToArray(); var result = isoDep.Transceive(command); var resultLength = result.Length; byte[] statusWord = { result[resultLength - 2], result[resultLength - 1] }; var payload = new byte[resultLength - 2]; Array.Copy(result, payload, resultLength - 2); var arrayEquals = SELECT_OK_SW.Length == statusWord.Length; if (Enumerable.SequenceEqual(SELECT_OK_SW, statusWord)) { var msg = Encoding.UTF8.GetString(payload); await App.DisplayAlertAsync(msg); } } catch (Exception e) { await App.DisplayAlertAsync("Error communicating with card: " + e.Message); } } } public static byte[] StringToByteArray(string hex) => Enumerable.Range(0, hex.Length) .Where(x => x % 2 == 0) .Select(x => Convert.ToByte(hex.Substring(x, 2), 16)) .ToArray(); } }
In the read mode of the nfc adapter, when the card is detected, the OnTagDiscovered method will be called. In it, IsoDep is an object with which we will exchange commands with the map (isoDep.Transceive (command)). Commands are byte arrays.
The code shows that we send the emulator a sequence consisting of the SELECT_APDU_HEADER header, the length of our AID in bytes, and the AID itself:
0 164 4 0 // SELECT_APDU_HEADER 5 // AID 241 35 69 103 137 // SAMPLE_LOYALTY_CARD_AID (F1 23 45 67 89)
MainActivity Reader
Here you need to declare the reader field:
public CardReader cardReader;
and two helper methods:
private void EnableReaderMode() { var nfc = NfcAdapter.GetDefaultAdapter(this); if (nfc != null) nfc.EnableReaderMode(this, cardReader, READER_FLAGS, null); } private void DisableReaderMode() { var nfc = NfcAdapter.GetDefaultAdapter(this); if (nfc != null) nfc.DisableReaderMode(this); }
in the OnCreate () method, initialize the reader and enable read mode:
protected override void OnCreate(Bundle savedInstanceState) { ... cardReader = new CardReader(); EnableReaderMode(); LoadApplication(new App()); }
and also, enable / disable read mode when minimizing / opening the application:
protected override void OnPause() { base.OnPause(); DisableReaderMode(); } protected override void OnResume() { base.OnResume(); EnableReaderMode(); }
App.xaml.cs
Static method for displaying a message:
public static async Task DisplayAlertAsync(string msg) => await Device.InvokeOnMainThreadAsync(async () => await Current.MainPage.DisplayAlert("message from service", msg, "ok"));
AndroidManifest.xml
The android documentation says that to use nfc in your application and work correctly with it, you need to declare these elements in AndroidManifest.xml:
<uses-permission android:name="android.permission.NFC" /> <uses-sdk android:minSdkVersion="10"/> <uses-sdk android:minSdkVersion="14"/> <uses-feature android:name="android.hardware.nfc" android:required="true" />
At the same time, if your application can use nfc, but this is not a required function, then you can skip the uses-feature element and check the availability of nfc during operation.
That's all for the reader.
Emulator application
Again, create a new project in Visual Studio like “Mobile App (Xamarin.Forms)”, then select the “Blank” template and leave only the “Android” checkmark in the “Platforms” section.
In an Android project, do the following:
- CardService class - here you need constants and the ProcessCommandApdu () method, as well as the SendMessageToActivity () method
- Description of service in aid_list.xml
- Message sending mechanism in MainActivity
- Application launch (if necessary)
- AndroidManifest.xml - permissions for nfc
And in the cross-platform project in the App.xaml.cs file:
- Method for displaying a message to the user
CardService Class
using Android.App; using Android.Content; using Android.Nfc.CardEmulators; using Android.OS; using System; using System.Linq; using System.Text; namespace ApduServiceCardApp.Droid.Services { [Service(Exported = true, Enabled = true, Permission = "android.permission.BIND_NFC_SERVICE"), IntentFilter(new[] { "android.nfc.cardemulation.action.HOST_APDU_SERVICE" }, Categories = new[] { "android.intent.category.DEFAULT" }), MetaData("android.nfc.cardemulation.host_apdu_service", Resource = "@xml/aid_list")] public class CardService : HostApduService { // ISO-DEP command HEADER for selecting an AID. // Format: [Class | Instruction | Parameter 1 | Parameter 2] private static readonly byte[] SELECT_APDU_HEADER = new byte[] { 0x00, 0xA4, 0x04, 0x00 }; // "OK" status word sent in response to SELECT AID command (0x9000) private static readonly byte[] SELECT_OK_SW = new byte[] { 0x90, 0x00 }; // "UNKNOWN" status word sent in response to invalid APDU command (0x0000) private static readonly byte[] UNKNOWN_CMD_SW = new byte[] { 0x00, 0x00 }; public override byte[] ProcessCommandApdu(byte[] commandApdu, Bundle extras) { if (commandApdu.Length >= SELECT_APDU_HEADER.Length && Enumerable.SequenceEqual(commandApdu.Take(SELECT_APDU_HEADER.Length), SELECT_APDU_HEADER)) { var hexString = string.Join("", Array.ConvertAll(commandApdu, b => b.ToString("X2"))); SendMessageToActivity($"Recieved message from reader: {hexString}"); var messageToReader = "Hello Reader!"; var messageToReaderBytes = Encoding.UTF8.GetBytes(messageToReader); return messageToReaderBytes.Concat(SELECT_OK_SW).ToArray(); } return UNKNOWN_CMD_SW; } public override void OnDeactivated(DeactivationReason reason) { } private void SendMessageToActivity(string msg) { Intent intent = new Intent("MSG_NAME"); intent.PutExtra("MSG_DATA", msg); SendBroadcast(intent); } } }
Upon receipt of the APDU command from the reader, the ProcessCommandApdu method will be called and the command will be transferred to it as an array of bytes.
First, we verify that the message begins with SELECT_APDU_HEADER and, if so, compose a response to the reader. In reality, an exchange can take place in several steps, question-answer question-answer, etc.
Before the class, the Service attribute describes the parameters of the android service. When building, xamarin converts this description into such an element in AndroidManifest.xml:
<service name='md51c8b1c564e9c74403ac6103c28fa46ff.CardService' permission='android.permission.BIND_NFC_SERVICE' enabled='true' exported='true'> <meta-data name='android.nfc.cardemulation.host_apdu_service' resource='@res/0x7F100000'> </meta-data> <intent-filter> <action name='android.nfc.cardemulation.action.HOST_APDU_SERVICE'> </action> <category name='android.intent.category.DEFAULT'> </category> </intent-filter> </service>
Description of service in aid_list.xml
In the xml folder, create the aid_list.xml file:
<?xml version="1.0" encoding="utf-8"?> <host-apdu-service xmlns:android="http://schemas.android.com/apk/res/android" android:description="@string/service_name" android:requireDeviceUnlock="false"> <aid-group android:description="@string/card_title" android:category="other"> <aid-filter android:name="F123456789"/> </aid-group> </host-apdu-service>
A reference to it is in the Service attribute in the CardService class - Resource = "@ xml / aid_list"
Here we set the AID of our application, according to which the reader will access it and the attribute requireDeviceUnlock = "false" so that the card is read with an unlocked screen.
There are 2 constants in the code:
@string/service_name
and
@string/card_title
. They are declared in the values / strings.xml file:
<resources> <string name="card_title">My Loyalty Card</string> <string name="service_name">My Company</string> </resources>
Message sending mechanism:
The service has no links to MainActivity, which at the time of receiving the APDU command may not even start at all. Therefore, we send messages from CardService to MainActivity using BroadcastReceiver as follows:
Method for sending a message from CardService:
private void SendMessageToActivity(string msg) { Intent intent = new Intent("MSG_NAME"); intent.PutExtra("MSG_DATA", msg); SendBroadcast(intent); }
Receiving a message:
Create the MessageReceiver class:
using Android.Content; namespace ApduServiceCardApp.Droid.Services { public class MessageReceiver : BroadcastReceiver { public override async void OnReceive(Context context, Intent intent) { var message = intent.GetStringExtra("MSG_DATA"); await App.DisplayAlertAsync(message); } } }
Register MessageReceiver in MainActivity:
protected override void OnCreate(Bundle savedInstanceState) { ... var receiver = new MessageReceiver(); RegisterReceiver(receiver, new IntentFilter("MSG_NAME")); LoadApplication(new App()); }
App.xaml.cs
The same as in the reader method for displaying a message:
public static async Task DisplayAlertAsync(string msg) => await Device.InvokeOnMainThreadAsync(async () => await Current.MainPage.DisplayAlert("message from service", msg, "ok"));
AndroidManifest.xml
<uses-feature android:name="android.hardware.nfc.hce" android:required="true" /> <uses-feature android:name="FEATURE_NFC_HOST_CARD_EMULATION"/> <uses-permission android:name="android.permission.NFC" /> <uses-permission android:name="android.permission.BIND_NFC_SERVICE" /> <uses-sdk android:minSdkVersion="10"/> 14
At the moment, we already have the following functions:
- display data from the emulator on the reader screen
- display data from the reader on the emulator screen
- The emulator should work with the application not running and with the screen off.
Further.
Emulator control
I will store the settings using Xamarin.Essentials.
Let's do this: when we restart the emulator application, we will update the setting:
Xamarin.Essentials.Preferences.Set("key1", Guid.NewGuid().ToString());
and in the ProcessCommandApdu method we will take this value again every time:
var messageToReader = $"Hello Reader! - {Xamarin.Essentials.Preferences.Get("key1", "key1 not found")}";
now every time you restart (not minimize) the emulator application we see a new guid, for example:
Hello Reader! - 76324a99-b5c3-46bc-8678-5650dab0529d
Also, through the settings, turn on / off the emulator:
Xamarin.Essentials.Preferences.Set("IsEnabled", false);
and at the beginning of the ProcessCommandApdu method add:
var IsEnabled = Xamarin.Essentials.Preferences.Get("IsEnabled", false); if (!IsEnabled) return UNKNOWN_CMD_SW; // 0x00, 0x00
This is an easy way, but there are others .
Running the emulator application when a reader is detected
If you just need to open the emulator application, add the line in the ProcessCommandApdu method:
StartActivity(typeof(MainActivity));
If you need to pass parameters to the application, then like this:
var activity = new Intent(this, typeof(MainActivity)); intent.PutExtra("MSG_DATA", "data for application"); this.StartActivity(activity);
You can read the passed parameters in the MainActivity class in the OnCreate method:
... LoadApplication(new App()); if (Intent.Extras != null) { var message = Intent.Extras.GetString("MSG_DATA"); await App.DisplayAlertAsync(message); }
Checking the status of the nfc adapter and switching to the nfc settings
This section applies to both the reader and the emulator.
Create NfcHelper in the android project and use DependencyService to access it from the MainPage page code.
using Android.App; using Android.Content; using Android.Nfc; using ApduServiceCardApp.Services; using Xamarin.Forms; [assembly: Dependency(typeof(ApduServiceCardApp.Droid.Services.NfcHelper))] namespace ApduServiceCardApp.Droid.Services { public class NfcHelper : INfcHelper { public NfcAdapterStatus GetNfcAdapterStatus() { var adapter = NfcAdapter.GetDefaultAdapter(Forms.Context as Activity); return adapter == null ? NfcAdapterStatus.NoAdapter : adapter.IsEnabled ? NfcAdapterStatus.Enabled : NfcAdapterStatus.Disabled; } public void GoToNFCSettings() { var intent = new Intent(Android.Provider.Settings.ActionNfcSettings); intent.AddFlags(ActivityFlags.NewTask); Android.App.Application.Context.StartActivity(intent); } } }
Now in the cross-platform project, add the INfcHelper interface:
namespace ApduServiceCardApp.Services { public interface INfcHelper { NfcAdapterStatus GetNfcAdapterStatus(); void GoToNFCSettings(); } public enum NfcAdapterStatus { Enabled, Disabled, NoAdapter } }
and use all this in MainPage.xaml.cs code:
protected override async void OnAppearing() { base.OnAppearing(); await CheckNfc(); } private async Task CheckNfc() { var nfcHelper = DependencyService.Get<INfcHelper>(); var status = nfcHelper.GetNfcAdapterStatus(); switch (status) { case NfcAdapterStatus.Enabled: default: await App.DisplayAlertAsync("nfc enabled!"); break; case NfcAdapterStatus.Disabled: nfcHelper.GoToNFCSettings(); break; case NfcAdapterStatus.NoAdapter: await App.DisplayAlertAsync("no nfc adapter found!"); break; } }
GitHub Links
emulator
reader