Blog Infos
Author
Published
Topics
, , , ,
Published

Hello! This article will explore how to implement NFC in an Android application by reading and writing data to tags using the NDEF format. We will demonstrate this by implementing a simple prepaid payment system similar to public transport systems, where tags, usually in the format of cards, can be recharged and used for boarding. The Android device will act as a payment terminal, and the tags store the balance and a list of acquired products.

The examples will only focus on the integration with NFC and how to structure the data that will be transmitted. The complete source code, including the UI implementation and features, can be accessed in the Clevent repository on GitHub.

Before we move on to code, let’s quickly learn some basic concepts about NFC.

NFC

NFC (Near-field Communication) is a technology that allows short-range data exchange wirelessly, generally at a distance of up to 4 cm, between a connecting device, like an Android device, and an NFC tag. This technology can be applied in many contexts, such as authorization, payments, or automation in general.

Android SDK has vast tools for working with NFC, allowing you to read and write data to tags and even make the device act like a tag.

Tags

NFC tags are small memory cards that are built into different form factors, such as stickers, cards, or bracelets. They have a chip capable of storing and transmitting data to other devices without a built-in power source. All the energy needed to power the chip comes from the magnetic field generated by the motion of the connecting device.

Tags have different technical capabilities, like storage size, data retention time, and support for encryption. They can also support a variety of protocols for communicating and structuring data, such as NFC-A, NFC-B, and NFC-F. Each one has advantages and disadvantages depending on its application.

 

LSaranzaya, CC BY-SA 4.0, via Wikimedia Commons
NDEF

The NFC Forum, an organization that creates specifications for NFC technology, developed the NDEF (NFC Data Exchange Format). This format abstracts the complexity of communicating with different tag protocols in a standardized way. This format is flexible enough to structure data in known formats, like plain text, media, and URIs, or to create your own custom data type.

Messages and Records are the main concepts we must be familiar with when working with the NDEF format.

An NDEF Message is like a bundle of data that can be transmitted between devices. Every NDEF tag can store up to one NDEF Message.

Inside a message, there are one or more NDEF Records. Records are pieces of information that we want to transmit. In short, if we want to transmit a list of website links in NDEF, we can create a message composed of several URI records.

Every record will have a type, a data payload, and, optionally, an ID. The type is expressed by two properties: TNF and Type.

  • TNF (Type Name Format) is a constant value that specifies how to interpret the data. Some examples of TNFs are TNF_WELL_KNOWNTNF_MIME_MEDIA, and TNF_EXTERNAL_TYPE for data that is in some application-specific format;
  • Type is used to define the Record type. Some examples RTD_TEXT and RTD_URI;

A Record that stores plain text data will use TNF_WELL_KNOWN with RTD_TEXT, and a Record that stores a URI will use TNF_WELL_KNOWN with RTD_URI. We won’t usually need to deal directly with TNFs and Types to create records because the SDK has several methods that help us create more complex records and avoid mistakes. We will explore some of these methods throughout this post.

Tag Dispatch System

When an Android device enters the field of an NFC tag, the OS reads its content and creates an Intent with that data. This Intent is used to launch Activities that filter for them using Intent Filters. The system responsible for discovering and dispatching tags is called the Tag Dispatch System.

Apps can create intent filters for tags with specific content type, like JSON or any media content. However, as an NDEF Message can contain several Records of different types, the Tag Dispatch System always uses only the first record to categorize the type of the entire message. The categorization is made by mapping the previously mentioned TNF and Type properties.

NFC Intents can have three different actions depending on the data contained in the tag:

  • ACTION_NDEF_DISCOVERED: When the tag contains data in NDEF format and a known type;
  • ACTION_TECH_DISCOVERED: When the tag does not contain data in NDEF format or that cannot be mapped to known types;
  • ACTION_TAG_DISCOVERED: Fallback when no Activity registers an Intent Filter for any of the previous Intents;

We can then add Intent Filters for these types of Intent in the Android Manifest to start an Activity when an NFC tag is discovered. However, we will not explore this option further because newer versions of Android have certain limitations for opening Activities if the app is in the background. To avoid these limitations, we will use the Foreground Dispatch System instead.

Foreground Dispatch System

When an Activity is in the foreground, it can use the Foreground Dispatch System, which works similarly to the Tag Dispatch System but gives priority to the Activity to receive all NFC Intents without any other app being able to interpret them. Let’s see a step-by-step guide on how to enable it in an Activity.

The first step is to declare the permission android.permission.NFC in the app’s manifest file:

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

This is a normal-type permission, so we don’t need to request it for the user at runtime, as it is granted when the app is installed.

Next, it will be necessary to initialize two objects in the Activity: an instance of NfcAdapter, which is the class that allows us to start the Foreground Dispatch System, and a PendingIntent that will be used to receive NFC Intents in the Activity:

import android.app.PendingIntent
import android.nfc.NfcAdapter

class MainActivity : ComponentActivity() {

    private var mNfcAdapter: NfcAdapter? = null
    private var mPendingIntent: PendingIntent? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        // ...
        this.mNfcAdapter = NfcAdapter.getDefaultAdapter(this)
            ?: return // If the device doesn't have NFC support, null will be returned
 
        val intent = Intent(this, javaClass).apply {
            addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
        }
 
        this.mPendingIntent = PendingIntent.getActivity(
            /* context = */ this,
            /* requestCode = */ 0,
            /* intent = */ intent,
            /* flags = */ PendingIntent.FLAG_MUTABLE,
        )
    }
}

With the NfcAdapter instance ready, we can use the enableForegroundDispatch and disableForegroundDispatch methods to control the Foreground Dispatch System.

The enableForegroundDispatch method needs to be invoked from the main thread and only when the Activity is in the foreground, so we will invoke it on onResume:

import android.nfc.tech.Ndef

override fun onResume() {
    super.onResume()
    
    // Intents we want to filter
    val filters = arrayOf(
        IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED),
        IntentFilter(NfcAdapter.ACTION_TECH_DISCOVERED),
    )
  
    // An array of tag technologies we can handle. Here we will only handle NDEF.
    val techList = arrayOf(Ndef::class.java.name)

    mNfcAdapter?.enableForegroundDispatch(
        /* activity = */ this,
        /* intent = */ mPendingIntent,
        /* filters = */ filters,
        /* techLists = */ arrayOf(techList),
    )
}

We need to stop the Foreground Dispatch when the Activity leaves the foreground; we can do this in onPause:

public override fun onPause() {
    super.onPause()
    mNfcAdapter?.disableForegroundDispatch(this)
}

Now, we need to implement the onNewIntent method that will be invoked when an NFC tag is discovered:

override fun onNewIntent(intent: Intent) {
    super.onNewIntent(intent)
    Toast.makeText(this, "Tag scanned!", Toast.LENGTH_SHORT).show()
}

By running the app and approaching the device closer to an NFC tag, the toast will be shown on the screen:

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

No results found.

Jobs

Reading data from a tag

Now that we can successfully listen for NFC Intents in the app, we can extract information from them.

NFC Intents can contain the following extras:

  • EXTRA_TAG: A Tag object that represents the scanned tag. This object allows us to perform various operations on the tag. This extra is always present in any NFC Intent;
  • EXTRA_NDEF_MESSAGES: An array with all NDEF messages written in the scanned tag. This extra is always present in NDEF_DISCOVERED Intents and optional in the others. When this extra is present, it will always have at least one NDEF message. NDEF tags usually only store one message, but Android implements it as an Array for future compatibility;
  • EXTRA_ID: Optional tag ID;

Let’s use these extras to extract which technologies the scanned tag supports and how many NDEF Messages there are written to it with the following code inside the onNewIntent method:

import android.nfc.NfcAdapter
import android.nfc.Tag

override fun onNewIntent(intent: Intent) {
    super.onNewIntent(intent)

    // Obtain tag from intent extra
    val tag: Tag? = getTagFromIntent(intent)
 
    // Print tag's supported technologies
    val availableTechnologies = tag?.techList?.joinToString()
    Log.d("NFC", "Technologies available in tag: $availableTechnologies")
 
    // Print the number of NDEF Messages stored in the tag
    val messages = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)
    Log.d("NFC", "NDEF Messages on tag: ${messages?.size}")
}

private fun getTagFromIntent(intent: Intent): Tag? {
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
        @Suppress("DEPRECATION")
        return intent.getParcelableExtra(NfcAdapter.EXTRA_TAG)
    }
    return intent.getParcelableExtra(NfcAdapter.EXTRA_TAG, Tag::class.java)
}

Running the app and reading a formatted NTAG-216 model tag, we get the following result in Logcat:

Technologies available in tag: android.nfc.tech.NfcA, android.nfc.tech.MifareUltralight, android.nfc.tech.Ndef
NDEF Messages on tag: null

This tag supports three technologies: NfaA, Mifare Ultralight, and NDEF. EXTRA_NDEF_MESSAGES extra is not present because no NDEF Message is written in it.

The next step is to start writing data in the tag, but first, we need to think about how to structure our data.

Modeling the data

We already know that our data needs to be structured within an NDEF Message with one or more Records, and these Records can be of different types, but which type should we use?

When we create an application that will exchange data that does not need to be understood by other applications, as in our case, we can use the External type. This format allows us to store byte arrays that only our application will need to know how to interpret them.

A customer must have two essential data in our app: the balance and the list of acquired products. We can model this with two classes like this:

import java.io.Serializable

class Customer(
    var balance: Int = 0,
    products: Map<Product, Int> = emptyMap(),
) : Serializable {
    private val products: MutableMap<Product, Int> = products.toMutableMap()
}

data class Product(
    val id: Int,
    val name: String,
    val price: Int,
) : Serializable

The balance is typed as an integer because it is a simple way to store monetary values. The value is equivalent to the smallest currency unit, such as cents, avoiding rounding problems when performing operations. A Map stores the number of items for a given Product.

With this structure, we can represent a Customer who has a balance of 80 euros and who consumed three items, two popcorn, and one soda:

val soda = Product(id = 1, name = "Soda", price = 200)
val popcorn = Product(id = 2, name = "Popcorn", price = 150)

val customer = Customer(
    balance = 8000,
    products = mapOf(soda to 1, popcorn to 2),
)

We can then add any methods to these classes to help us implement all use cases, such as the addProduct and addToBalance methods:

class Customer() {

    // ...

    fun addProduct(product: Product, quantity: Int) {
        val total = product.price * quantity
        addToBalance(-total)

        this.products = this.products?.plus(quantity) ?: quantity
    }
 
    fun addToBalance(value: Int) {
        val newBalance: Long = balance.toLong() + value
  
        if (newBalance !in 0..Int.MAX_VALUE) {
            throw Exception("Balance boundaries exceeded.")
        }
  
        balance = newBalance.toInt()
    }
}
Serializing the data and creating the NDEF Message

Now, we need to transform an instance of Customer into an External NDEF Record and place it inside an NDEF Message.

The first step is to transform the Customer instance into a byte array. This process is called Serialization and can be done in different ways, depending on your object’s complexity and needs. In this example, we will use the most convenient way, which is using the language’s built-in functions that can serialize classes that implement the java.io.Serializable interface.

Serializing the Customer object into a byte array:

import java.io.ByteArrayOutputStream
import java.io.ObjectOutputStream

fun serializeCustomer(customer: Customer): ByteArray {
    val outputStream = ByteArrayOutputStream()
    ObjectOutputStream(outputStream).use { it.writeObject(customer) }

    return outputStream.toByteArray()
}

Deserializing the bytes back to a Customer instance:

import java.io.ByteArrayInputStream
import java.io.ObjectInputStream

fun deserializeCustomer(data: ByteArray): Customer {
    val inputStream = ByteArrayInputStream(data)
    val objectInputStream = ObjectInputStream(inputStream)

    return objectInputStream.readObject() as Customer
}

Although functional for our example, be aware that this method is inefficient. The generated byte array will be unnecessarily large, occupying too much space in tags, which usually have a very limited capacity.

Instead of serializing the entire object with all its properties, a better idea is to serialize only the data that matters, as long as it allows you to reconstruct the object again later without any loss. For this, you can use formats like Protobuf or write our own serializer, like this example, that serializes only the IDs and quantity of items since the name of the items can be retrieved from a local database by the ID:

With the Customer object serialized, we can now use the static createExternal method, one of those helper methods available to create records:

import android.nfc.NdefRecord

override fun onNewIntent(intent: Intent) {
    // ...
    val serializedCustomer = serializeCustomer(customer)
 
    val customerRecord = NdefRecord.createExternal(
        /* domain = */ "com.myapplication", 
        /* type = */ "customer",
        /* data = */ serializedCustomer,
    )
}
  • domain is used to identify the Record issuer. Generally, it will be the application’s package name;
  • type identifies the data type that will be stored. In our case, the type will be customer;
  • data is the record payload where the serialized Customer object will be placed.

Under the hood, the createExternal method will create a Record with the TNF as TNF_EXTERNAL_TYPE, and the Type will be the concatenation of the domain and type parameters, separated by a colon: com.myapplication:customer.

With the record created, we need to place it inside a message, and this process is simple, we only need to pass the record to the constructor of the NdefMessage class.

import android.nfc.NdefMessage

val ndefMessage = NdefMessage(customerRecord)
Writing data to the tag

To write data on NDEF tags, we need an instance of the android.nfc.tech.Ndef class. We can obtain this instance using the static get method by passing the tag object as the parameter:

import android.nfc.tech.Ndef

val tag = getTagFromIntent(intent)
val ndef = Ndef.get(tag)

The list below shows the main operations we can perform using the Ndef class. Some of these methods may cause radio frequency activity, meaning that Android will communicate with the tag when the method is invoked. These methods must be executed outside the main thread, as they will block until the operation is completed.

  • connect(): Open connection with the tag to perform I/O operations. It may cause RF activity.
  • isConnected(): Return true if the connection to the tag is open and ready to receive I/O commands. Does not cause RF activity.
  • getMaxSize(): Return the maximum size in bytes that an NDEF Message can have to fit into the tag. Does not cause RF activity.
  • getNdefMessage(): Return the NDEF Message written in the tag at that moment. It may cause RF activity.
  • getCachedNdefMessage(): Return the NDEF Message written in the tag when it was scanned. This value is not updated if the content is modified later. Does not cause RF activity.
  • writeNdefMessage(): Write an NDEF Message to the tag, overwriting any other existing. It may cause RF activity.
  • close(): Close connection with the tag, canceling any operation that is still open. Does not cause RF activity.

We will use three methods from this list to write the message to the tag: connect()writeNdefMessage(), and close():

lifecycleScope.launch(Dispatchers.IO) {
    ndef.connect()
    ndef.writeNdefMessage(ndefMessage)
    ndef.close()

    Log.d("NFC", "${ndefMessage.byteArrayLength} bytes message successfully written")
}

As the connect() and writeNdefMessage() are blocking methods, we used Coroutines to avoid too much work on the main thread, but you can use any other technique you wish.

Running the app and scanning a tag will print the following output in Logcat:

484 bytes message successfully written
Putting it all together

Now that we know how to receive NFC intents and perform read and write operations, we will combine all this to implement the balance recharge feature. We will extract the Customer instance from the tag, add balance, and then rewrite it back to the tag.

Let’s start by creating some extension functions to help us manipulate the data, starting with one that allows us to validate that the data written in the tag is a Customer instance:

private fun NdefRecord.isCustomer(): Boolean {
    return this.type contentEquals "com.myapplication:customer".toByteArray()
}

This one extracts the data from the tag and deserializes it back to a Customer instance:

private fun Ndef.getCustomer(): Customer {
    val ndefMessage = this.cachedNdefMessage
    val ndefRecord = ndefMessage?.records?.first()

    if (ndefRecord == null || !ndefRecord.isCustomer()) {
        return Customer()
    }

    return deserializeCustomer(ndefRecord.payload)
}

Finally, this last one serializes a Customer and writes it in the tag:

private suspend fun Ndef.writeCustomer(customer: Customer) {
    val customerRecord = NdefRecord.createExternal(
        /* domain = */ "com.myapplication",
        /* type = */ "customer",
        /* data = */ serializeCustomer(customer),
    )
    val ndefMessage = NdefMessage(customerRecord)

    withContext(Dispatchers.IO) {
        connect()
        writeNdefMessage(ndefMessage)
        close()
    }
}

The complete example will then look like this:

override fun onNewIntent(intent: Intent) {
    super.onNewIntent(intent)

    val tag = getTagFromIntent(intent)
    val ndef = Ndef.get(tag)

    val customer = ndef.getCustomer()

    customer.addToBalance(10000)

    lifecycleScope.launch(Dispatchers.IO) {
        ndef.writeCustomer(customer)
        Log.d("NFC", "Tag balance is ${customer.balance}")
    }
}

Running the app, we can see the balance increasing every time we read the tag:

Tag balance is 10000
Tag balance is 20000
Tag balance is 30000

Placing the AAR as the second Record is important because the Tag Dispatch System always uses the first record to categorize the Message type. An AAR should only be the first Record if the message has only one.

Conclusion

This article discussed NFC, Tags, and the NDEF format. We also saw how Android communicates with tags, categorizes data, and launches Intents that apps can obtain and use to perform reading and writing operations on the tags.

The complete source code, including all features implemented with additional layers of security, such as data encryption and integrity validation using checksums, can be found on the GitHub repository.

References

https://developer.android.com/develop/connectivity/nfc?source=post_page—–8105e46340b4——————————–

https://nfc-forum.org/?source=post_page—–8105e46340b4——————————–

This article is previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
Hi, today I come to you with a quick tip on how to update…
READ MORE
blog
Automation is a key point of Software Testing once it make possible to reproduce…
READ MORE
blog
Drag and Drop reordering in Recyclerview can be achieved with ItemTouchHelper (checkout implementation reference).…
READ MORE
Menu