How to use NFC Tags: Detect, Read and Write NFCs with Android Studio
By: Cawin Chan
Tags: Android App Development, Android, AndroidDev, Nfc, Android Studio

How to use NFC Tags: Detect, Read and Write NFCs with Android Studio

Introduction

Near-Field-Communication (NFC) isn’t new technology. With the events of Covid 19, there has been a surge in contactless NFC usage as seen by the new trend of mobile payment like Google Pay and Apple Pay. This is similar to its well-known brother, QR codes.

It has come a long way. First used in 1997 for Hasbro Star Wars toys, the now highly accessible tech which currently exists in around 73% of smartphones, is an untapped source for innovation.

Similar to my previous article on installing TF lite for Raspberry Pi, finding useful NFC material was a chore. This is meant to be a quick and clear guide for the general-use cases of NFCs.

Complete Code on my Github: starter code

Prerequisites

Android Studio — Installation guide here.

An Android Phone with NFC capabilities (How to check if I have NFC?)

NFC Device (Note: I used an NFC card)

NFC Detection

We will first detect the NFC device information:

1. NFC id

2. NFC Technologies

3. NFC type (Mifare Classic/Ultralight)

Note: We will be using the Mifare Ultralight C for this guide.

Step 0: AndroidManifest.xml, Allow NFC permissions for our app

<uses-permission android:name="android.permission.NFC" />

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

Step 1: onCreate, Initialize the NFC adapter and define the Pending Intent

public class MainActivity extends AppCompatActivity { //Intialize attributes

NfcAdapter nfcAdapter;

PendingIntent pendingIntent; final static String TAG = "nfc_test"; @Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main); //Initialise NfcAdapter

nfcAdapter = NfcAdapter.getDefaultAdapter(this); //If no NfcAdapter, display that the device has no NFC

if (nfcAdapter == null){

Toast.makeText(this,"NO NFC Capabilities",

Toast.LENGTH_SHORT).show();

finish();

} //Create a PendingIntent object so the Android system can

//populate it with the details of the tag when it is scanned. //PendingIntent.getActivity(Context,requestcode(identifier for

// intent),intent,int)

pendingIntent = PendingIntent.getActivity(this,0,new Intent(this,this.getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP),0);

}

}

Step 2: onResume(), Enable the Foreground Dispatch to listen for NFC intent (Waiting for NFC card to be tapped)

enableForegroundDispatch allows your current (foreground) activity to intercept our NFC intent and claim priority over all other activities both within the app and other apps.

@Override

protected void onResume() {

super.onResume();

assert nfcAdapter != null;

//nfcAdapter.enableForegroundDispatch(context,pendingIntent,

// intentFilterArray,

// techListsArray)

nfcAdapter.enableForegroundDispatch(this,pendingIntent,null,null);

}

Step 3: onPause(), Disable the foreground dispatch (Stop listening for NFC tag)

protected void onPause() {

super.onPause();

//Onpause stop listening

if (nfcAdapter != null) {

nfcAdapter.disableForegroundDispatch(this);

}

}

Step 4: onNewIntent(), Process our new NFC Intent (Card detected)

After getting the intent, you have to parse the intent to detect the card: Code here for external helper functions.

These are our NFC Intent filters

nfcAdapter.ACTION_TAG_DISCOVERED — NFC tag discovered.

nfcAdapter.ACTION_TECH_DISCOVERED — NFC tag discovered and activities are registered for the specific technologies on the tag.

nfcAdapter.ACTION_NDEF_DISCOVERED — NFC tag discovered with NDEF payload discovered.

TAG_DISCOVERED is meant to be a fallback if no apps can handle TECH_DISCOVERED and NDEF_DISCOVERED . The same for TECH_DISCOVERED which is meant to be a fallback for NDEF_DISCOVERED .

For our case, we don’t want any filters as such we will be using TAG_DISCOVERED and TECH_DISCOVERED for global detection of the NFC.

@Override

protected void onNewIntent(Intent intent) {

super.onNewIntent(intent);

setIntent(intent);

resolveIntent(intent);

}



private void resolveIntent(Intent intent) {

String action = intent.getAction();

if (NfcAdapter.ACTION_TAG_DISCOVERED.equals(action)

|| NfcAdapter.ACTION_TECH_DISCOVERED.equals(action)

|| NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action)) {

Tag tag = (Tag) intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);

assert tag != null;

byte[] payload = detectTagData(tag).getBytes();

}

}

Step 5: Helper functions, to detect and parse our NFC Tag Data

private String detectTagData(Tag tag) {

StringBuilder sb = new StringBuilder();

byte[] id = tag.getId();

sb.append("ID (hex): ").append(toHex(id)).append('

');

sb.append("ID (reversed hex): ").append(toReversedHex(id)).append('

');

sb.append("ID (dec): ").append(toDec(id)).append('

');

sb.append("ID (reversed dec): ").append(toReversedDec(id)).append('

');



String prefix = "android.nfc.tech.";

sb.append("Technologies: ");

for (String tech : tag.getTechList()) {

sb.append(tech.substring(prefix.length()));

sb.append(", ");

}



sb.delete(sb.length() - 2, sb.length());



for (String tech : tag.getTechList()) {

if (tech.equals(MifareClassic.class.getName())) {

sb.append('

');

String type = "Unknown";



try {

MifareClassic mifareTag = MifareClassic.get(tag);



switch (mifareTag.getType()) {

case MifareClassic.TYPE_CLASSIC:

type = "Classic";

break;

case MifareClassic.TYPE_PLUS:

type = "Plus";

break;

case MifareClassic.TYPE_PRO:

type = "Pro";

break;

}

sb.append("Mifare Classic type: ");

sb.append(type);

sb.append('

');



sb.append("Mifare size: ");

sb.append(mifareTag.getSize() + " bytes");

sb.append('

');



sb.append("Mifare sectors: ");

sb.append(mifareTag.getSectorCount());

sb.append('

');



sb.append("Mifare blocks: ");

sb.append(mifareTag.getBlockCount());

} catch (Exception e) {

sb.append("Mifare classic error: " + e.getMessage());

}

}



if (tech.equals(MifareUltralight.class.getName())) {

sb.append('

');

MifareUltralight mifareUlTag = MifareUltralight.get(tag);

String type = "Unknown";

switch (mifareUlTag.getType()) {

case MifareUltralight.TYPE_ULTRALIGHT:

type = "Ultralight";

break;

case MifareUltralight.TYPE_ULTRALIGHT_C:

type = "Ultralight C";

break;

}

sb.append("Mifare Ultralight type: ");

sb.append(type);

}

}

Log.v("test",sb.toString());

return sb.toString();

}

private String toHex(byte[] bytes) {

StringBuilder sb = new StringBuilder();

for (int i = bytes.length - 1; i >= 0; --i) {

int b = bytes[i] & 0xff;

if (b < 0x10)

sb.append('0');

sb.append(Integer.toHexString(b));

if (i > 0) {

sb.append(" ");

}

}

return sb.toString();

}



private String toReversedHex(byte[] bytes) {

StringBuilder sb = new StringBuilder();

for (int i = 0; i < bytes.length; ++i) {

if (i > 0) {

sb.append(" ");

}

int b = bytes[i] & 0xff;

if (b < 0x10)

sb.append('0');

sb.append(Integer.toHexString(b));

}

return sb.toString();

}



private long toDec(byte[] bytes) {

long result = 0;

long factor = 1;

for (int i = 0; i < bytes.length; ++i) {

long value = bytes[i] & 0xffl;

result += value * factor;

factor *= 256l;

}

return result;

}



private long toReversedDec(byte[] bytes) {

long result = 0;

long factor = 1;

for (int i = bytes.length - 1; i >= 0; --i) {

long value = bytes[i] & 0xffl;

result += value * factor;

factor *= 256l;

}

return result;

}

Step 6: Compile and Run the app → Go to Logcat/Run

NFC Read and Write

From our Mifare Ultralight C NFC Tag specifications: https://www.nxp.com/docs/en/data-sheet/MF0ICU2.pdf

We can only use the 4th to 39th-page address. Each page is 4 bytes, thus, we have at most 140 bytes of storage page.

Since each String letter in UTF-8 takes up around 1 byte, We only at most 140 words to work with assuming that none of our user memory pages are used. In my case, we only had 137 bytes in our card. Some had experimented with gzip to compress this data.

Each readPages reads 4 pages (16 characters/16 bytes).

To read pages 4–12, we have to readPages(4) [pages 4–7] and readPages(8) [pages 8–11] and readPages(12) [pages 12–15]. We would then have to remove the extra 3 pages(12 characters/12 bytes) from our String.

Each writePage writes 1 page(4 characters/4 bytes).

public void writeTag(MifareUltralight mifareUlTag) {

try {

mifareUlTag.connect();

mifareUlTag.writePage(4, "get ".getBytes(Charset.forName("US-ASCII")));

mifareUlTag.writePage(5, "fast".getBytes(Charset.forName("US-ASCII")));

mifareUlTag.writePage(6, " NFC".getBytes(Charset.forName("US-ASCII")));

mifareUlTag.writePage(7, " now".getBytes(Charset.forName("US-ASCII")));

} catch (IOException e) {

Log.e(TAG, "IOException while writing MifareUltralight...", e);

} finally {

try {

mifareUlTag.close();

} catch (IOException e) {

Log.e(TAG, "IOException while closing MifareUltralight...", e);

}

}

}

public String readTag(MifareUltralight mifareUlTag) {

try {

mifareUlTag.connect();

byte[] payload = mifareUlTag.readPages(4);

return new String(payload, Charset.forName("US-ASCII"));

} catch (IOException e) {

Log.e(TAG, "IOException while reading MifareUltralight message...", e);

} finally {

if (mifareUlTag != null) {

try {

mifareUlTag.close();

}

catch (IOException e) {

Log.e(TAG, "Error closing tag...", e);

}

}

}

return null;

}

Next, simply change our previous detectTagData(Tag tag).

private String detectTagData(Tag tag) {

StringBuilder sb = new StringBuilder();

byte[] id = tag.getId();

sb.append("NFC ID (dec): ").append(toDec(id)).append('

');

for (String tech : tag.getTechList()) {

if (tech.equals(MifareUltralight.class.getName())) {

MifareUltralight mifareUlTag = MifareUltralight.get(tag);

readTag(mifareUlTag);

writeTag(mifareUlTag);

}

}

}

Log.v("test",sb.toString());

return sb.toString();

}

To learn more on reading and writing: https://developer.android.com/guide/topics/connectivity/nfc/advanced-nfc#read-write

Global NFC Usage

If you want to be able to read and write your NFC in the background or outside your app, simply add this intent filter in your AndroidManifest.xml.

The idea of global usage is to have NFC presence at anywhere on the phone.

<intent-filter>

<action android:name="android.nfc.action.TAG_DISCOVERED" />

<action android:name="android.nfc.action.TECH_DISCOVERED" />

<category android:name="android.intent.category.DEFAULT" />

</intent-filter>

Complete Code on my Github: starter code

If any of my work has helped you, donations would be greatly appreciated!

BTC: 16Z1wZaL3NkmYW8ydJk6A2VNsZJxyzB56H (BTC Network)

ETH: 0x214e3f9a3cc96a44bf93f03b401cc88d10f9693d (ERC20)

LTC: Laq8z31x9UwyQMnoi4RC3ML4Ei3qoE61AH (LTC Network)

BCH: 16Z1wZaL3NkmYW8ydJk6A2VNsZJxyzB56H (BCH Network)

References:

Special credits to Mika Baumeister for the image on Unsplash!