Working with NFC tags on Android
In this post, I will show you how to read and write an NFC tag on an Android device. We would be using Android’s NFC capabilities to read and write a tag. In a different post, I will illustrate how APDU commands could be used to talk directly with an NFC tag.
Prerequisites
You will need an NFC capable Android device and NFC tags before you get started with this tutorial.
In my experience, I found Mifare Ultralight C to be suitable in most of the standard use-cases. Here’s a link to its datasheet which you might use when you start digging deeper into its specifications. If you are just getting started then you can buy the 10 pack of Mifare Ultralight C from Amazon or if you want to order in bulk then get the 100 pack instead.
Apart from Ultralight C, I found Mifare Classic 1k to be a great choice for some scenarios when your data size is larger. Here’s a link to its datasheet for more details. You can easily get a pack of 100 cards from Amazon.
Introduction
Earlier I talked about the structure of NDEF messages. You can go through this post to understand the format of NDEF messages.
Understanding the format of NDEF Messages — Part 1
Broadly these actions are performed when a tag is tapped on an Android device:
- NFC tag is handled with the tag dispatch system, which analyses discovered NFC tags
- it categorizes the data and starts an application that is interested in the categorized data
- an application that wants to handle the scanned NFC tag can declare an intent filter and request to handle the data
Request NFC access for your app
Firstly, to access NFC hardware you need to add the following permission in AndroidManifest.xml
<uses-permission android:name="android.permission.NFC" />
Note: If you want your application to show up only for devices with NFC hardware then add the following uses-feature
element in the manifest file.
<uses-feature android:name="android.hardware.nfc" android:required="true" />
Setup Foreground Dispatch
You need to register foreground dispatch so that your activity can handle the NFC intents.
Initialize NFC Adapter
First, init the NFCAdapter
in the onCreate
method of the Activity
or a similar Fragment
lifecycle method.
private var adapter: NfcAdapter? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
.
.
.
initNfcAdapter()
}
private fun initNfcAdapter() {
val nfcManager = getSystemService(Context.NFC_SERVICE) as NfcManager
adapter = nfcManager.defaultAdapter
}
Enable Foreground Dispatch
Next, call enableForegroundDispatch
to enable foreground dispatch of the NFCAdapter
. You can call this method in the onResume
lifecycle method of the activity.
override fun onResume() {
super.onResume()
enableNfcForegroundDispatch()
}
private fun enableNfcForegroundDispatch() {
try {
val intent = Intent(this, javaClass).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
val nfcPendingIntent = PendingIntent.getActivity(this, 0, intent, 0)
adapter?.enableForegroundDispatch(this, nfcPendingIntent, null, null)
} catch (ex: IllegalStateException) {
Log.e(getTag(), "Error enabling NFC foreground dispatch", ex)
}
}
Disable Foreground Dispatch
When the activity is paused or destroyed, make sure that you disable the foreground dispatch.
override fun onPause() {
disableNfcForegroundDispatch()
super.onPause()
}
private fun disableNfcForegroundDispatch() {
try {
adapter?.disableForegroundDispatch(this)
} catch (ex: IllegalStateException) {
Log.e(getTag(), "Error disabling NFC foreground dispatch", ex)
}
}
Prepare your NDEF records
We will take the example from the previous post and try to write the same data on an NFC tag.
Custom data
- tnf: TNF_MIME_MEDIA,
- type: application/vnd.com.tickets [in bytes]
- id: null
- payload: cxwwhcfxympwbbonxymwritcqytcnfvgmwcnzfanqytc [in bytes]
val typeBytes = mimeType.toByteArray()
val payload = tagData.toByteArray()
val r1 = NdefRecord(TNF_MIME_MEDIA, typeBytes, null, payload)
Android Application Record(AAR)
- tnf: TNF_EXTERNAL_TYPE
- type: android.com:pkg [in bytes]
- id: null
- payload: com.example.tickets [in bytes]
val r2 = NdefRecord.createApplicationRecord(context.packageName)
Construct your NdefMessage
using both the records.
NdefMessage(arrayOf(record1, record2))
Write NDEF message to the NFC tag
Now, that we have the foreground dispatch setup and the NDEF message prepared, we are ready to write the message on the NFC tag.
Listen to NFC intent
Listen to NFC intent and when a EXTRA_TAG
data is present in the incoming intent, handle it to write to an NFC tag.
val tagFromIntent = intent.getParcelableExtra<Tag>(NfcAdapter.EXTRA_TAG)
try {
tag = WritableTag(tagFromIntent)
} catch (e: FormatException) {
Log.e(getTag(), "Unsupported tag tapped", e)
return
}
Write the NDEF messages
The final step is to actually write the information on the tag. I have extracted out all the code to a helper class called WritableTag
to abstract out all the logic of writing on an NFC tag to a separate class.
class WritableTag @Throws(FormatException::class) constructor(tag: Tag) {
private val NDEF = Ndef::class.java.canonicalName
private val NDEF_FORMATABLE = NdefFormatable::class.java.canonicalName
private val ndef: Ndef?
private val ndefFormatable: NdefFormatable?
val tagId: String?
get() {
if (ndef != null) {
return bytesToHexString(ndef.tag.id)
} else if (ndefFormatable != null) {
return bytesToHexString(ndefFormatable.tag.id)
}
return null
}
init {
val technologies = tag.techList
val tagTechs = Arrays.asList(*technologies)
if (tagTechs.contains(NDEF)) {
Log.i("WritableTag", "contains ndef")
ndef = Ndef.get(tag)
ndefFormatable = null
} else if (tagTechs.contains(NDEF_FORMATABLE)) {
Log.i("WritableTag", "contains ndef_formatable")
ndefFormatable = NdefFormatable.get(tag)
ndef = null
} else {
throw FormatException("Tag doesn't support ndef")
}
}
@Throws(IOException::class, FormatException::class)
fun writeData(tagId: String,
message: NdefMessage): Boolean {
if (tagId != tagId) {
return false
}
if (ndef != null) {
ndef.connect()
if (ndef.isConnected) {
ndef.writeNdefMessage(message)
return true
}
} else if (ndefFormatable != null) {
ndefFormatable.connect()
if (ndefFormatable.isConnected) {
ndefFormatable.format(message)
return true
}
}
return false
}
@Throws(IOException::class)
private fun close() {
ndef?.close() ?: ndefFormatable?.close()
}
companion object {
fun bytesToHexString(src: ByteArray): String? {
if (ByteUtils.isNullOrEmpty(src)) {
return null
}
val sb = StringBuilder()
for (b in src) {
sb.append(String.format("%02X", b))
}
return sb.toString()
}
}
}
Note:
- When the tag is read, we iterate through all the supported technologies of the tag to check if it supports
NDEF
orNDEF_FORMATABLE
. If not, we throw aFormatException
as we can’t write a NDEF message on an unsupported tag. - You can check if your tag supports NFC or not by using any of the NFC reader apps from the play store. I prefer this app.
- If the tag is compatible, we simple
connect
to the tag and callwriteNdefMessage
to write the message on the tag.
Reading NDEF message from an NFC Tag
Now that we have written our NDEF message on the NFC tag, we would probably want to read it. Also, it might be useful to read the tag UID for uniquely identifying the tag. Let us see how to do that.
Handle NFC Intent
To read the NFC tag, the app needs to register for handling ACTION_NDEF_DISCOVERED
intent. Registering this intent will let your app handle any NFC tag that is tapped to the Android device.
if (NfcAdapter.ACTION_NDEF_DISCOVERED == intent.action) {
val rawMsgs = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)
if (rawMsgs != null) {
onTagTapped(NfcUtils.getUID(intent), NfcUtils.getData(rawMsgs))
}
}
If you wish to handle only those tags that belong to your application then you can add a filter. Earlier we created an application record while writing the tag. We can use the same package name as the filter if the app needs to handle only those tags that have that particular application record(AAR).
fun getIntentFilters(): Array<IntentFilter> {
val ndefFilter = IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED)
try {
ndefFilter.addDataType("application/vnd.com.tickets")
} catch (e: IntentFilter.MalformedMimeTypeException) {
Log.e("NfcUtils", "Problem in parsing mime type for nfc reading", e)
}
return arrayOf(ndefFilter)
}
Reading Tag UID
You can read the tag ID of the tag using the following method.
fun getUID(intent: Intent): String {
val myTag = intent.getParcelableExtra<Tag>(NfcAdapter.EXTRA_TAG)
return BaseEncoding.base16().encode(myTag.id)
}
Read Tag Data
You can read all the NDEF records using the following code snippet.
fun getData(rawMsgs: Array<Parcelable>): String {
val msgs = arrayOfNulls<NdefMessage>(rawMsgs.size)
for (i in rawMsgs.indices) {
msgs[i] = rawMsgs[i] as NdefMessage
}
val records = msgs[0]!!.records
var recordData = ""
for (record in records) {
recordData += record.toString() + "\n"
}
return recordData
}
Conclusion
Android makes it quite easy to read and write NFC tags and it supports a variety of tags and tag technologies. Once you understand the basics, you can build your own NFC supported app within a few hours.
You can find the complete source code of the example that I have used in this article on Github.
maskaravivek/AndroidNfcExample
You can buy me a coffee if this post really helped you learn something or fix a nagging issue!
Written on March 16, 2020 by Vivek Maskara.
Originally published on Medium