Video icon 64
Learning to code? Skill up faster with our practical video courses. Start your free trial today.
Advertisement

Reading NFC Tags with Android

by
Student iconAre you a student? Get a yearly Tuts+ subscription for $45 →

Are you curious about what NFC is and how it can be integrated into your own Android applications? This tutorial will quickly introduce you to the topic before diving in and teaching you how to build a simple NFC reader app!


What is NFC?

NFC is the abbreviation for Near Field Communication. It is the international standard for contactless exchange of data. In contrast to a large range of other technologies, such as wireless LAN and Bluetooth, the maximum distance of two devices is 10cm. The development of the standard started in 2002 by NXP Semiconductors and Sony. The NFC Forum, a consortium of over 170 companies and members, which included Mastercard, NXP, Nokia, Samsung, Intel, and Google, has been designing new specifications since 2004.

There are various possibilities for NFC use with mobile devices; for example, paperless tickets, access controls, cashless payments, and car keys. With the help of NFC tags you can control your phone and change settings. Data can be exchanged simply by holding two devices next to each other.

In this tutorial I want to explain how to implement NFC with the Android SDK, which pitfalls exist, and what to keep in mind. We will create an app step by step, which can read the content of NFC tags supporting NDEF.


NFC Technologies

There are a variety of NFC tags that can be read with a smartphone. The spectrum ranges from simple stickers and key rings to complex cards with integrated cryptographic hardware. Tags also differ in their chip technology. The most important is NDEF, which is supported by most tags. In addidition, Mifare should be mentioned as it is the most used contactless chip technology worldwide. Some tags can be read and written, while others are read-only or encrypted.

Only the NFC Data Exchange Format (NDEF) is discussed in this tutorial.

girogo-card

Adding NFC Support in an App

We start with a new project and a blank activity. It is important to select a minimum SDK version of level 10, because NFC is only supported after Android 2.3.3. Remember to choose your own package name. I've chosen net.vrallev.android.nfc.demo, because vrallev.net is the domain of my website and the other part refers to the topic of this application.

	<uses-sdk
		android:minSdkVersion="10"
        android:targetSdkVersion="17" />

The default layout generated by Eclipse is almost sufficient for us. I've only added an ID to the TextView and changed the text.

	<TextView
        android:id="@+id/textView_explanation"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/explanation" />

To get access to the NFC hardware, you have to apply for permission in the manifest. If the app won't work without NFC, you can specify the condition with the uses-feature tag. If NFC is required, the app can't be installed on devices without it and Google Play will only display your app to users who own a NFC device.

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

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

The MainActivity should only consist of the onCreate() method. You can interact with the hardware via the NfcAdapter class. It is important to find out whether the NfcAdapter is null. In this case, the Android device does not support NFC.

	package net.vrallev.android.nfc.demo;

	import android.app.Activity;
	import android.nfc.NfcAdapter;
	import android.os.Bundle;
	import android.widget.TextView;
	import android.widget.Toast;
	
	/**
	 * Activity for reading data from an NDEF Tag.
	 * 
	 * @author Ralf Wondratschek
	 *
	 */
	public class MainActivity extends Activity {
	
		public static final String TAG = "NfcDemo";
	
		private TextView mTextView;
		private NfcAdapter mNfcAdapter;
	
		@Override
		protected void onCreate(Bundle savedInstanceState) {
			super.onCreate(savedInstanceState);
			setContentView(R.layout.activity_main);
	
			mTextView = (TextView) findViewById(R.id.textView_explanation);
	
			mNfcAdapter = NfcAdapter.getDefaultAdapter(this);
	
			if (mNfcAdapter == null) {
				// Stop here, we definitely need NFC
				Toast.makeText(this, "This device doesn't support NFC.", Toast.LENGTH_LONG).show();
				finish();
				return;
	
			}
		
			if (!mNfcAdapter.isEnabled()) {
				mTextView.setText("NFC is disabled.");
			} else {
				mTextView.setText(R.string.explanation);
			}
			
			handleIntent(getIntent());
		}
		
		private void handleIntent(Intent intent) {
			// TODO: handle Intent
		}
	}

If we start our app now, we can see the text whether NFC is enabled or disabled.

How to Filter for NFC Tags

We have our sample app and want to receive a notification from the system when we attach an NFC tag to the device. As usual, Android uses its Intent system to deliver tags to the apps. If multiple apps can handle the Intent, the activity chooser gets displayed and the user can decide which app will be opened. Opening URLs or sharing information is handled the same way.


NFC Intent Filter

There are three different filters for tags:

  1. ACTION_NDEF_DISCOVERED
  2. ACTION_TECH_DISCOVERED
  3. ACTION_TAG_DISCOVERED

The list is sorted from the highest to the lowest priority.

Now what happens when a tag is attached to the smartphone? If the system detects a tag with NDEF support, an Intent is triggered. An ACTION_TECH_DISCOVERED Intent is triggered if no Activity from any app is registered for the NDEF Intent or if the tag does not support NDEF. If again no app is found for the Intent or the chip technology could not be detected, then a ACTION_TAG_DISCOVERED Intent is fired. The following graphic shows the process:

nfc tag dispatch

In summary this means that each app needs to filter after the Intent with the highest priority. In our case, this is the NDEF Intent. We implement the ACTION_TECH_DISCOVERED Intent first to highlight the difference between priorities.


Tech Discovered Intent

We must specify the technology we are interested in. For this purpose, we create a subfolder called xml in the res folder. In this folder we create the file nfc_tech_filter.xml, in which we specify the technologies.

	<?xml version="1.0" encoding="utf-8"?>
	<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
	    <tech-list>
	        <tech>android.nfc.tech.Ndef</tech>
	        <!-- class name -->
	    </tech-list>
	</resources>
	
	<!-- 
	<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
	    <tech-list>
	        <tech>android.nfc.tech.IsoDep</tech>
	        <tech>android.nfc.tech.NfcA</tech>
	        <tech>android.nfc.tech.NfcB</tech>
	        <tech>android.nfc.tech.NfcF</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>
	-->

Now we must create an IntentFilter in the manifest, and the app will be started when we attach a tag.

	<?xml version="1.0" encoding="utf-8"?>
	<activity
		android:name="net.vrallev.android.nfc.demo.MainActivity"
        android:label="@string/app_name" >
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
            
        <intent-filter>
            <action android:name="android.nfc.action.TECH_DISCOVERED" />
        </intent-filter>

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

If no other app is registered for this Intent, our Activity will start immediately. On my device, however, other apps are installed, so the activity chooser gets displayed.

screenshot

NDEF Discovered Intent

As I mentioned before, the Tech Discovered Intent has the second highest priority. However, since our app will support only NDEF, we can use the NDEF Discovered Intent instead, which has a higher priority. We can delete the technology list again and replace the IntentFilter with the following one.

	<intent-filter>
		<action android:name="android.nfc.action.NDEF_DISCOVERED" />

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

		<data android:mimeType="text/plain" />
	</intent-filter>

When we attach the tag now, the app will be started like before. There is a difference for me, however. The activity chooser does not appear and the app starts immediately, because the NDEF Intent has a higher priority and the other apps only registered for the lower priorities. That's exactly what we want.


Foreground Dispatch

Note that one problem remains. When our app is already opened and we attach the tag again, the app is opened a second time instead of delivering the tag directly. This is not our intended behavior. You can bypass the problem by using a Foreground Dispatch.

Instead of the system having distributed the Intent, you can register your Activity to receive the tag directly. This is important for a particular workflow, where it makes no sense to open another app.

I've inserted the explanations at the appropriate places in the code.

	package net.vrallev.android.nfc.demo;

	import android.app.Activity;
	import android.app.PendingIntent;
	import android.content.Intent;
	import android.content.IntentFilter;
	import android.content.IntentFilter.MalformedMimeTypeException;
	import android.nfc.NfcAdapter;
	import android.os.Bundle;
	import android.widget.TextView;
	import android.widget.Toast;
	
	/**
	 * Activity for reading data from an NDEF Tag.
	 * 
	 * @author Ralf Wondratschek
	 *
	 */
	public class MainActivity extends Activity {
	
		public static final String MIME_TEXT_PLAIN = "text/plain";
		public static final String TAG = "NfcDemo";
	
		private TextView mTextView;
		private NfcAdapter mNfcAdapter;
	
		@Override
		protected void onCreate(Bundle savedInstanceState) {
			super.onCreate(savedInstanceState);
			setContentView(R.layout.activity_main);
	
			mTextView = (TextView) findViewById(R.id.textView_explanation);
	
			mNfcAdapter = NfcAdapter.getDefaultAdapter(this);
	
			if (mNfcAdapter == null) {
				// Stop here, we definitely need NFC
				Toast.makeText(this, "This device doesn't support NFC.", Toast.LENGTH_LONG).show();
				finish();
				return;
	
			}
		
			if (!mNfcAdapter.isEnabled()) {
				mTextView.setText("NFC is disabled.");
			} else {
				mTextView.setText(R.string.explanation);
			}
			
			handleIntent(getIntent());
		}
		
		@Override
		protected void onResume() {
			super.onResume();
			
			/**
			 * It's important, that the activity is in the foreground (resumed). Otherwise
			 * an IllegalStateException is thrown. 
			 */
			setupForegroundDispatch(this, mNfcAdapter);
		}
		
		@Override
		protected void onPause() {
			/**
			 * Call this before onPause, otherwise an IllegalArgumentException is thrown as well.
			 */
			stopForegroundDispatch(this, mNfcAdapter);
			
			super.onPause();
		}
		
		@Override
		protected void onNewIntent(Intent intent) { 
			/**
			 * This method gets called, when a new Intent gets associated with the current activity instance.
			 * Instead of creating a new activity, onNewIntent will be called. For more information have a look
			 * at the documentation.
			 * 
			 * In our case this method gets called, when the user attaches a Tag to the device.
			 */
			handleIntent(intent);
		}
		
		private void handleIntent(Intent intent) {
			// TODO: handle Intent
		}
		
		/**
		 * @param activity The corresponding {@link Activity} requesting the foreground dispatch.
		 * @param adapter The {@link NfcAdapter} used for the foreground dispatch.
		 */
		public static void setupForegroundDispatch(final Activity activity, NfcAdapter adapter) {
			final Intent intent = new Intent(activity.getApplicationContext(), activity.getClass());
			intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
	
			final PendingIntent pendingIntent = PendingIntent.getActivity(activity.getApplicationContext(), 0, intent, 0);
	
			IntentFilter[] filters = new IntentFilter[1];
			String[][] techList = new String[][]{};
	
			// Notice that this is the same filter as in our manifest.
			filters[0] = new IntentFilter();
			filters[0].addAction(NfcAdapter.ACTION_NDEF_DISCOVERED);
			filters[0].addCategory(Intent.CATEGORY_DEFAULT);
			try {
				filters[0].addDataType(MIME_TEXT_PLAIN);
			} catch (MalformedMimeTypeException e) {
				throw new RuntimeException("Check your mime type.");
			}
			
			adapter.enableForegroundDispatch(activity, pendingIntent, filters, techList);
		}
	
		/**
		 * @param activity The corresponding {@link BaseActivity} requesting to stop the foreground dispatch.
		 * @param adapter The {@link NfcAdapter} used for the foreground dispatch.
		 */
		public static void stopForegroundDispatch(final Activity activity, NfcAdapter adapter) {
			adapter.disableForegroundDispatch(activity);
		}
	}

Now, when you attach a tag and our app is already opened, onNewIntent is called and no new Activity is created.


Reading Data From an NDEF Tag

The last step is to read the data from the tag. The explanations are inserted at the appropriate places in the code once again. The NdefReaderTask is a private inner class.

	package net.vrallev.android.nfc.demo;

	import java.io.UnsupportedEncodingException;
	import java.util.Arrays;
	
	import android.app.Activity;
	import android.app.PendingIntent;
	import android.content.Intent;
	import android.content.IntentFilter;
	import android.content.IntentFilter.MalformedMimeTypeException;
	import android.nfc.NdefMessage;
	import android.nfc.NdefRecord;
	import android.nfc.NfcAdapter;
	import android.nfc.Tag;
	import android.nfc.tech.Ndef;
	import android.os.AsyncTask;
	import android.os.Bundle;
	import android.util.Log;
	import android.widget.TextView;
	import android.widget.Toast;
	
	/*
	 * ... other code parts
	 */
	
	private void handleIntent(Intent intent) {
		String action = intent.getAction();
		if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action)) {
			
			String type = intent.getType();
			if (MIME_TEXT_PLAIN.equals(type)) {

				Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
				new NdefReaderTask().execute(tag);
				
			} else {
				Log.d(TAG, "Wrong mime type: " + type);
			}
		} else if (NfcAdapter.ACTION_TECH_DISCOVERED.equals(action)) {
			
			// In case we would still use the Tech Discovered Intent
			Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
			String[] techList = tag.getTechList();
			String searchedTech = Ndef.class.getName();
			
			for (String tech : techList) {
				if (searchedTech.equals(tech)) {
					new NdefReaderTask().execute(tag);
					break;
				}
			}
		}
	}
	/**
	 * Background task for reading the data. Do not block the UI thread while reading. 
	 * 
	 * @author Ralf Wondratschek
	 *
	 */
	private class NdefReaderTask extends AsyncTask<Tag, Void, String> {

		@Override
		protected String doInBackground(Tag... params) {
			Tag tag = params[0];
			
			Ndef ndef = Ndef.get(tag);
			if (ndef == null) {
				// NDEF is not supported by this Tag. 
				return null;
			}

			NdefMessage ndefMessage = ndef.getCachedNdefMessage();

			NdefRecord[] records = ndefMessage.getRecords();
			for (NdefRecord ndefRecord : records) {
				if (ndefRecord.getTnf() == NdefRecord.TNF_WELL_KNOWN && Arrays.equals(ndefRecord.getType(), NdefRecord.RTD_TEXT)) {
					try {
						return readText(ndefRecord);
					} catch (UnsupportedEncodingException e) {
						Log.e(TAG, "Unsupported Encoding", e);
					}
				}
			}

			return null;
		}
		
		private String readText(NdefRecord record) throws UnsupportedEncodingException {
			/*
			 * See NFC forum specification for "Text Record Type Definition" at 3.2.1 
			 * 
			 * http://www.nfc-forum.org/specs/
			 * 
			 * bit_7 defines encoding
			 * bit_6 reserved for future use, must be 0
			 * bit_5..0 length of IANA language code
			 */

			byte[] payload = record.getPayload();

			// Get the Text Encoding
			String textEncoding = ((payload[0] & 128) == 0) ? "UTF-8" : "UTF-16";

			// Get the Language Code
			int languageCodeLength = payload[0] & 0063;
			
			// String languageCode = new String(payload, 1, languageCodeLength, "US-ASCII");
			// e.g. "en"
			
			// Get the Text
			return new String(payload, languageCodeLength + 1, payload.length - languageCodeLength - 1, textEncoding);
		}
		
		@Override
		protected void onPostExecute(String result) {
			if (result != null) {
				mTextView.setText("Read content: " + result);
			}
		}
	}

The app now successfully reads the content.

screen

Useful Apps

To check whether data is read and written properly, I personally like to use following apps:

  • NFC TagInfo by NFC Research Lab for reading data
  • TagInfo by NXP SEMICONDUCTORS for reading data
  • TagWriter by NXP SEMICONDUCTORS for writing data

Conclusion

In this tutorial I have shown you how the data from a NDEF tag can be extracted. You could expand the example to other mime types and chip technologies; a feature to write data would be useful as well. The first step to work with NFC was made. However, the Android SDK offers much more possibilities, such as an easy exchange of data (called Android Beam).


About the Author

Ralf Wondratschek is a computer science student from Germany. In addition to his studies, Ralf works as a freelancer in the field of mobile computing. In the last few years he has worked with Java, XML, HTML, JSP, JSF, Eclipse, Google App Engine, and of course Android. He has published two Android apps to date which can be found here.

You can find out more about the author’s work on his homepage vrallev.net.


Sources

http://www.nfc-forum.org/home/n-mark.jpg

http://commons.wikimedia.org/wiki/File%3A%C3%9Cberlagert.jpg

http://developer.android.com/images/nfc_tag_dispatch.png


Advertisement