Reading NFC tags with Android (Kotlin)

In this post we discuss reading NFC Tags in Android that do not contain NDEF data, but instead use their own custom read-write methods like nfcA, nfcB.

Near Field Communication(NFC) Tags are used to store data such as URLs, contact information or even simple text. Mobile devices that support NFC Technology have the ability to read these tags.

Prerequisites: add the requisite permissions in the android app,

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
< uses-permission android:name= "android.permission.NFC" / >
<uses-permission android:name="android.permission.NFC" />
<uses-permission android:name="android.permission.NFC" />

Creating a Tech List XML

Since we are dealing with non-NDEF NFC Tags, we need to specify the NFC Technologies that our app supports. These handle intents from only the relevant tag technologies that we wish to use in the app.

Create a file named techlist.xml and place it under the Resources → XML folder. Create a new folder if it doesn’t already exist.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
< resources xmlns:xliff= "urn:oasis:names:tc:xliff:document:1.2" >
< tech-list >
< tech > android. nfc . tech . NfcA < /tech >
< tech > android. nfc . tech . NfcB < /tech >
< tech > android. nfc . tech . NfcF < /tech >
< tech > android. nfc . tech . IsoDep < /tech >
< tech > android. nfc . tech . NfcV < /tech >
< tech > android. nfc . tech . Ndef < /tech >
< tech > android. nfc . tech . NdefFormatable < /tech >
< tech > android. nfc . tech . MifareClassic < /tech >
< tech > android. nfc . tech . MifareUltralight < /tech >
< /tech-list >
< /resources >
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <tech-list> <tech>android.nfc.tech.NfcA</tech> <tech>android.nfc.tech.NfcB</tech> <tech>android.nfc.tech.NfcF</tech> <tech>android.nfc.tech.IsoDep</tech> <tech>android.nfc.tech.NfcV</tech> <tech>android.nfc.tech.Ndef</tech> <tech>android.nfc.tech.NdefFormatable</tech> <tech>android.nfc.tech.MifareClassic</tech> <tech>android.nfc.tech.MifareUltralight</tech> </tech-list> </resources>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
    <tech-list>
          <tech>android.nfc.tech.NfcA</tech>
          <tech>android.nfc.tech.NfcB</tech>
          <tech>android.nfc.tech.NfcF</tech>
          <tech>android.nfc.tech.IsoDep</tech>
          <tech>android.nfc.tech.NfcV</tech>
          <tech>android.nfc.tech.Ndef</tech>
          <tech>android.nfc.tech.NdefFormatable</tech>
          <tech>android.nfc.tech.MifareClassic</tech>
          <tech>android.nfc.tech.MifareUltralight</tech>
    </tech-list>
</resources>

Place a meta-data tag in your activity tag and your manifest.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
< activity >
..
< meta-data android:name= "android.nfc.action.TECH_DISCOVERED"
android:resource= "@xml/techlist" / >
..
< /activity >
<activity> .. <meta-data android:name="android.nfc.action.TECH_DISCOVERED" android:resource="@xml/techlist" /> .. </activity>
<activity>
            ..
            <meta-data android:name="android.nfc.action.TECH_DISCOVERED"
                android:resource="@xml/techlist" />
            ..
</activity>

Intent Filters

Intent Filters specify the type of intent the activity answers to. The Intent filter will invoke the respective activity in our NFC Reader App when the device reads an NFC tag. The invoked activity can then handle the intent.

We would be using “ACTION_TECH_DISCOVERED” for our intent filter, which is defined to be used to start an activity when an NFC tag that uses the technologies specified in the tech filter is discovered.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
< activity >
..
< intent-filter >
< action android:name= "android.nfc.action.TECH_DISCOVERED" / >
< /intent-filter >
..
< /activity >
<activity> .. <intent-filter> <action android:name="android.nfc.action.TECH_DISCOVERED"/> </intent-filter> .. </activity>
<activity>
   ..
   <intent-filter>
                <action android:name="android.nfc.action.TECH_DISCOVERED"/>
    </intent-filter>
   ..
</activity>

android:launchMode

Launch Mode defines instructions on how the activity should launch. We are discussing activity launch modes here as we do not want a new instance of the activity to open up every time an NFC Tag is read. For this, we have to set launch mode as “singleTop” in the manifest file. Setting this would invoke an override method called onNewIntent in our invoked activity, where we would handle the intent.

The activity tag in your android manifest file should look something like this:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
< activity android:name= ".NfcReaderActivity" android:launchMode= "singleTask" >
< intent-filter >
< action android:name= "android.nfc.action.TECH_DISCOVERED" / >
< /intent-filter >
< meta-data android:name= "android.nfc.action.TECH_DISCOVERED"
android:resource= "@xml/techlist" / >
< /activity >
<activity android:name=".NfcReaderActivity" android:launchMode="singleTask"> <intent-filter> <action android:name="android.nfc.action.TECH_DISCOVERED"/> </intent-filter> <meta-data android:name="android.nfc.action.TECH_DISCOVERED" android:resource="@xml/techlist" /> </activity>
<activity android:name=".NfcReaderActivity" android:launchMode="singleTask">
            
            <intent-filter>
                <action android:name="android.nfc.action.TECH_DISCOVERED"/>
            </intent-filter>

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

You’re all set to handle the intents in your activity now!

Step 1. Create an instance of NfcAdapter and initialize it in the onCreate method of your NfcReaderActivity

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
class NfcReaderActivity : AppCompatActivity ()
{
private var nfcAdapter: NfcAdapter? = null
...
...
override fun onCreate ( savedInstanceState: Bundle? )
{
super. onCreate ( savedInstanceState )
setContentView ( R. layout . nfc_reader_activity )
..
..
this . nfcAdapter = NfcAdapter. getDefaultAdapter ( this ) ?.let { it }
..
..
}
}
class NfcReaderActivity :AppCompatActivity() { private var nfcAdapter: NfcAdapter? = null ... ... override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.nfc_reader_activity) .. .. this.nfcAdapter = NfcAdapter.getDefaultAdapter(this)?.let { it } .. .. } }
class NfcReaderActivity :AppCompatActivity()
{
    private var nfcAdapter: NfcAdapter? = null
     ...
     ...
     override fun onCreate(savedInstanceState: Bundle?)
     {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.nfc_reader_activity)
        ..
        ..
        this.nfcAdapter = NfcAdapter.getDefaultAdapter(this)?.let { it }
        ..
        ..
     }
}

Step 2. Since we are using our launch Mode as singleTop, we can service the intent in the onNewIntent method. We are using NFC-A tag technology to receive the tag object from the intent using the Tag Technology that is relevant to the tag.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
class NfcReaderActivity : AppCompatActivity ()
{
..
..
override fun onNewIntent ( intent: Intent? )
{
super. onNewIntent ( intent )
var tagFromIntent: Tag? = intent?. getParcelableExtra ( NfcAdapter. EXTRA_TAG )
val n = NfcA. get ( tagFromIntent )
}
..
..
}
class NfcReaderActivity :AppCompatActivity() { .. .. override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) var tagFromIntent: Tag? = intent?.getParcelableExtra(NfcAdapter.EXTRA_TAG) val n = NfcA.get(tagFromIntent) } .. .. }
class NfcReaderActivity :AppCompatActivity()
{
  ..
  ..
 
override fun onNewIntent(intent: Intent?)
    {
        super.onNewIntent(intent)
        var tagFromIntent: Tag? = intent?.getParcelableExtra(NfcAdapter.EXTRA_TAG)
        val n = NfcA.get(tagFromIntent)
        
     }
  ..
  ..
}

Step 3. After receiving the NFC tag object, we can get its properties like ATQA and SAK (look them up if necessary) and then connect with the tag. We can use the isConnected( ) to see if the tag is connected.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
class NfcReaderActivity : AppCompatActivity ()
{
..
..
override fun onNewIntent ( intent: Intent? )
{
super. onNewIntent ( intent )
var tagFromIntent: Tag? = intent?. getParcelableExtra ( NfcAdapter. EXTRA_TAG )
val nfc = NfcA. get ( tagFromIntent )
val atqa: ByteArray = nfc. getAtqa ()
val sak: Short = nfc. getSak ()
nfc. connect ()
}
..
..
}
class NfcReaderActivity :AppCompatActivity() { .. .. override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) var tagFromIntent: Tag? = intent?.getParcelableExtra(NfcAdapter.EXTRA_TAG) val nfc = NfcA.get(tagFromIntent) val atqa: ByteArray = nfc.getAtqa() val sak: Short = nfc.getSak() nfc.connect() } .. .. }
class NfcReaderActivity :AppCompatActivity()
{
  ..
  ..
 
override fun onNewIntent(intent: Intent?)
    {
        super.onNewIntent(intent)
        var tagFromIntent: Tag? = intent?.getParcelableExtra(NfcAdapter.EXTRA_TAG)
        val nfc = NfcA.get(tagFromIntent)
        
        val atqa: ByteArray = nfc.getAtqa()
        val sak: Short = nfc.getSak()
        nfc.connect()

     }
  ..
  ..
}

Step 4. If the tag is connected, we can use the Transceive( ) method to send in a byte array command that would return the requisite data stored in the card in the form of a byte array.

Here the NFC_READ_COMMAND would be the custom command byte array that you would have to send to your NFC Tag to read it (tag manufacturers usually specify this command).

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
class NfcReaderActivity : AppCompatActivity ()
{
..
..
override fun onNewIntent ( intent: Intent? ) {
super. onNewIntent ( intent )
var tagFromIntent: Tag? = intent?. getParcelableExtra ( NfcAdapter. EXTRA_TAG )
val nfc = NfcA. get ( tagFromIntent )
val atqa: ByteArray = n. getAtqa ()
val sak: Short = nfc. getSak ()
nfc. connect ()
val isConnected= nfc. isConnected ()
if ( isConnected )
{
val receivedData:ByteArray= nfc. transceive ( NFC_READ_COMMAND )
..
//code to handle the received data
// Received data would be in the form of a byte array that can be converted to string
//NFC_READ_COMMAND would be the custom command you would have to send to your NFC Tag in order to read it
..
}
} else {
Log. e ( "ans" , "Not connected" )
}
}
..
}
class NfcReaderActivity :AppCompatActivity() { .. .. override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) var tagFromIntent: Tag? = intent?.getParcelableExtra(NfcAdapter.EXTRA_TAG) val nfc = NfcA.get(tagFromIntent) val atqa: ByteArray = n.getAtqa() val sak: Short = nfc.getSak() nfc.connect() val isConnected= nfc.isConnected() if(isConnected) { val receivedData:ByteArray= nfc.transceive(NFC_READ_COMMAND) .. //code to handle the received data // Received data would be in the form of a byte array that can be converted to string //NFC_READ_COMMAND would be the custom command you would have to send to your NFC Tag in order to read it .. } }else{ Log.e("ans", "Not connected") } } .. }
class NfcReaderActivity :AppCompatActivity()
{
  ..
  ..
override fun onNewIntent(intent: Intent?) {
        super.onNewIntent(intent)
        var tagFromIntent: Tag? = intent?.getParcelableExtra(NfcAdapter.EXTRA_TAG)
        val nfc = NfcA.get(tagFromIntent)

        val atqa: ByteArray = n.getAtqa()
        val sak: Short = nfc.getSak()

        nfc.connect()
        val isConnected= nfc.isConnected()
 

        if(isConnected)
           {
            val receivedData:ByteArray= nfc.transceive(NFC_READ_COMMAND)
            ..
            //code to handle the received data
            // Received data would be in the form of a byte array that can be converted to string
            //NFC_READ_COMMAND would be the custom command you would have to send to your NFC Tag in order to read it
            ..            
            }

        }else{
            Log.e("ans", "Not connected")
        }
    }
    ..
}

Step 5. Enabling Foreground Dispatch to ensure that, if the activity is already active it will take precedence over any other activity or an app with the same intent filters. Essentially it does not handle the NFC Reader intent in our activity.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
class NfcReaderActivity : AppCompatActivity ()
{
..
private fun enableForegroundDispatch ( activity: AppCompatActivity, adapter: NfcAdapter? ) {
val intent = Intent ( activity. applicationContext , activity. javaClass )
intent. flags = Intent. FLAG_ACTIVITY_SINGLE_TOP
val pendingIntent = PendingIntent. getActivity ( activity. applicationContext , 0 , intent, 0 )
val filters = arrayOfNulls < IntentFilter >( 1 )
val techList = arrayOf < Array < String >>()
filters [ 0 ] = IntentFilter ()
with ( filters [ 0 ]) {
this ?. addAction ( NfcAdapter. ACTION_NDEF_DISCOVERED )
this ?. addCategory ( Intent. CATEGORY_DEFAULT )
try {
this ?. addDataType ( "text/plain" )
} catch ( ex: IntentFilter. MalformedMimeTypeException ) {
throw RuntimeException ( e )
}
}
adapter?. enableForegroundDispatch ( activity, pendingIntent, filters, techList )
}
override fun onResume () {
super. onResume ()
enableForegroundDispatch ( this , this . nfcAdapter )
}
..
}
class NfcReaderActivity :AppCompatActivity() { .. private fun enableForegroundDispatch(activity: AppCompatActivity, adapter: NfcAdapter?) { val intent = Intent(activity.applicationContext, activity.javaClass) intent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP val pendingIntent = PendingIntent.getActivity(activity.applicationContext, 0, intent, 0) val filters = arrayOfNulls<IntentFilter>(1) val techList = arrayOf<Array<String>>() filters[0] = IntentFilter() with(filters[0]) { this?.addAction(NfcAdapter.ACTION_NDEF_DISCOVERED) this?.addCategory(Intent.CATEGORY_DEFAULT) try { this?.addDataType("text/plain") } catch (ex: IntentFilter.MalformedMimeTypeException) { throw RuntimeException(e) } } adapter?.enableForegroundDispatch(activity, pendingIntent, filters, techList) } override fun onResume() { super.onResume() enableForegroundDispatch(this, this.nfcAdapter) } .. }
class NfcReaderActivity :AppCompatActivity()
{
..

  private fun enableForegroundDispatch(activity: AppCompatActivity, adapter: NfcAdapter?) {

        val intent = Intent(activity.applicationContext, activity.javaClass)
        intent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP

        val pendingIntent = PendingIntent.getActivity(activity.applicationContext, 0, intent, 0)

        val filters = arrayOfNulls<IntentFilter>(1)
        val techList = arrayOf<Array<String>>()

        filters[0] = IntentFilter()
        with(filters[0]) {
            this?.addAction(NfcAdapter.ACTION_NDEF_DISCOVERED)
            this?.addCategory(Intent.CATEGORY_DEFAULT)
            try {
                this?.addDataType("text/plain")
            } catch (ex: IntentFilter.MalformedMimeTypeException) {
                throw RuntimeException(e)
            }
        }

        adapter?.enableForegroundDispatch(activity, pendingIntent, filters, techList)
    }
    override fun onResume() {
        super.onResume()

        enableForegroundDispatch(this, this.nfcAdapter)

    }
    ..
}

And voila! We have read the data from the NFC Card.

About the author

Abhishek Bagdare

Abhishek Bagdare, is an Android Developer and app development enthusiast working with Excellarate for the past 7 months as a Software Developer.

Share this post