As promised some time ago, this post will be about Android.
About half year ago I bought Samsung Galaxy Nexus as my first Android phone and I found one thing interesting – the Near Field Communication (NFC).
Near field communication (NFC) is a set of standards for smartphones and similar devices to establish radio communication with each other by touching them together or bringing them into close proximity, usually no more than a few centimeters.
In short, this is a technology based on RFID tags that allows smartphone to do 3 things:
- read and write RFID tags,
- emulate RFID tag,
- send small amount of data between devices in small proximity.
The first one can be used to read tags that matches MIME types on the device to activate some functionality, configure the device or just to read cards that uses the RFID technology. It’s also possible to write to those tags. The second enables the device to behave as RFID tag for another devices. And the third is used for communication between two devices, mostly to setup another, more powerful connection type such as Bluetooth or WiFi.
In this post I will show how to read data from the RFID tags since I don’t have any available card to write to. Fortunately this task is quite well covered by Android SDK documentation so I will just write a quick setup of the basic project. I assume that an empty project is created with basic implementation provided by Eclipse or Android tool from SDK.
Enabling NFC in the project
First of all, to enable NFC in the project the modification of the AndroidManifest.xml file is required. The NFC is available since version 10 and requires special permission – android.permission.NFC. To enable it just add following code to the manifest node:
1
2
3
4
5
6
7
8
9
10
11
12
13
| <uses-sdk
android:minSdkVersion="10"
android:targetSdkVersion="15"
/>
<uses-permission
android:name="android.permission.NFC"
/>
<uses-feature
android:name="android.hardware.nfc"
android:required="true"
/> |
<uses-sdk
android:minSdkVersion="10"
android:targetSdkVersion="15"
/>
<uses-permission
android:name="android.permission.NFC"
/>
<uses-feature
android:name="android.hardware.nfc"
android:required="true"
/>
uses-feature entry means here that the application requires NFC and won’t work without it.
Since RFID tags are read and dispatched by the Android, application must register an Intent filter and a Techlist to get them. To do this add following code to activity node in manifest file:
1
2
3
4
5
6
7
| <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"
/> |
<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"
/>
There are free types of actions that can be used with NFC – android.nfc.action.NDEF_DISCOVERED, android.nfc.action.TECH_DISCOVERED and android.nfc.action.TAG_DISCOVERED. I chose the android.nfc.action.TECH_DISCOVERED because none of my RFID chips had NDEF messages and TAG, as the documentation says, is too general. Also TECH just worked.
As you can see the code also specifies the techlist xml file. This file contains a list of NFC technologies supported by the application. @xml/techlist means that this file has name techlist.xml and lies in res/xml/ directory. Following code shows an example of my file:
1
2
3
4
5
6
| <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<tech-list>
<tech>android.nfc.tech.MifareClassic</tech>
<tech>android.nfc.tech.NfcA</tech>
</tech-list>
</resources> |
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<tech-list>
<tech>android.nfc.tech.MifareClassic</tech>
<tech>android.nfc.tech.NfcA</tech>
</tech-list>
</resources>
This covers basic setup of NFC but requires the user to specify the application which will handle the Tag everytime it is read by Android. It is quite annoying so Android allows to register an Activity in the foreground to be the default one for handling Tag intents. To do that add following code to your Activity class:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
| private NfcAdapter mAdapter = null;
private PendingIntent mPendingIntent = null;
private IntentFilter[] mIntentFilters = null;
private String[][] mTechLists = null;
public void onCreate(Bundle savedInstanceState) {
/* ... */
mAdapter = (NfcAdapter)NfcAdapter.getDefaultAdapter(this);
// Creating PendingIndent for foreground dispatching
mPendingIntent = PendingIntent.getActivity(
this,
0,
new Intent(this, this.getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP),
0);
IntentFilter tech = new IntentFilter(NfcAdapter.ACTION_TECH_DISCOVERED);
try {
ndef.addDataType("*/*");
} catch(MalformedMimeTypeException e) {
throw new RuntimeException("fail", e);
}
mIntentFilters = new IntentFilter[] { tech };
mTechLists = new String[][] { getTechList(this.getResources()) };
/* ... */
}
public void onResume() {
/* ... */
mAdapter.enableForegroundDispatch(this, mPendingIntent, mIntentFilters, mTechLists);
/* ... */
}
public void onPause() {
/* ... */
mAdapter.disableForegroundDispatch(this);
/* ... */
}
private static String[] getTechList(Resources resources) {
ArrayList<String> list = new ArrayList<String>();
XmlResourceParser xml = resources.getXml(R.xml.techlist);
// Assumed simple parsing because of simple file structure
for(;;) {
int eventType = XmlPullParser.END_DOCUMENT;
try {
eventType = xml.next();
} catch (XmlPullParserException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if(eventType == XmlPullParser.TEXT) {
list.add(xml.getText());
} else if(eventType == XmlPullParser.END_DOCUMENT) {
break;
}
}
return list.toArray(new String[list.size()]);
} |
private NfcAdapter mAdapter = null;
private PendingIntent mPendingIntent = null;
private IntentFilter[] mIntentFilters = null;
private String[][] mTechLists = null;
public void onCreate(Bundle savedInstanceState) {
/* ... */
mAdapter = (NfcAdapter)NfcAdapter.getDefaultAdapter(this);
// Creating PendingIndent for foreground dispatching
mPendingIntent = PendingIntent.getActivity(
this,
0,
new Intent(this, this.getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP),
0);
IntentFilter tech = new IntentFilter(NfcAdapter.ACTION_TECH_DISCOVERED);
try {
ndef.addDataType("*/*");
} catch(MalformedMimeTypeException e) {
throw new RuntimeException("fail", e);
}
mIntentFilters = new IntentFilter[] { tech };
mTechLists = new String[][] { getTechList(this.getResources()) };
/* ... */
}
public void onResume() {
/* ... */
mAdapter.enableForegroundDispatch(this, mPendingIntent, mIntentFilters, mTechLists);
/* ... */
}
public void onPause() {
/* ... */
mAdapter.disableForegroundDispatch(this);
/* ... */
}
private static String[] getTechList(Resources resources) {
ArrayList<String> list = new ArrayList<String>();
XmlResourceParser xml = resources.getXml(R.xml.techlist);
// Assumed simple parsing because of simple file structure
for(;;) {
int eventType = XmlPullParser.END_DOCUMENT;
try {
eventType = xml.next();
} catch (XmlPullParserException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if(eventType == XmlPullParser.TEXT) {
list.add(xml.getText());
} else if(eventType == XmlPullParser.END_DOCUMENT) {
break;
}
}
return list.toArray(new String[list.size()]);
}
In short, this code creates an instance of PendingIntent, IntentFilter for TECH_DISCOVERED action and all MIME types (see */*), and loads list of supported technologies from file specified earlier. Those objects are then used in onResume and onPause methods to enable and disable foreground dispatching so the application could handle the Intent before others.
Handling NFC intent
Reading NFC tags is easy. Android provides classes to read tags from some technologies such as those specified earlier in techlist.xml. In this post I will also show an example that utilizes the MifareClassic class. MIFARE Classic is a standard that defines tags with blocks of data that can be written to. The one that is commonly used is a MIFARE Classic 1K which can hold 1 KB in 16 sectors with 4 blocks each. Each sector is secured by pair of 48-bit keys – first for read access and second for write access. There are 3 commonly used keys that by default protect all sectors and allow the access to them if hadn’t been changed – KEY_DEFAULT, KEY_MIFARE_APPLICATION_DIRECTORY and KEY_NFC_FORUM.
Ok, lets try now to handle an Intent with RFID tag. First of all, there are two moments when Activity can handle an Intent. First is when Activity is registered as a handler for this type of intents and second is when Activity is already on the screen. To handle both situations add following code to your Activity class:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| @Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
/* ... */
// Passing intent
Intent intent = getIntent();
resolveIntent(intent); // This handles first situation
}
@Override
public void onNewIntent(Intent intent) {
resolveIntent(intent); // This handles second situation
} |
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
/* ... */
// Passing intent
Intent intent = getIntent();
resolveIntent(intent); // This handles first situation
}
@Override
public void onNewIntent(Intent intent) {
resolveIntent(intent); // This handles second situation
}
When Activity is created because of the NFC Intent it can be retrived in onCreate, but when Activity is already on the screen and handles this type of Intent you must override the onNewIntent method to handle it.
Ok, but how to exactly handle the tag Intent? Following function does that for MIFARE Classic tag:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
| private void resolveIntent(Intent intent) {
if(NfcAdapter.ACTION_TECH_DISCOVERED.equals(intent.getAction())) {
Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
MifareClassic mfc = MifareClassic.get(tag);
try {
mfc.connect();
mLogger.pushStatus("");
mLogger.pushStatus("== MifareClassic Info == ");
mLogger.pushStatus("Size: " + mfc.getSize());
mLogger.pushStatus("Timeout: " + mfc.getTimeout());
mLogger.pushStatus("Type: " + mfc.getType());
mLogger.pushStatus("BlockCount: " + mfc.getBlockCount());
mLogger.pushStatus("MaxTransceiveLength: " + mfc.getMaxTransceiveLength());
mLogger.pushStatus("SectorCount: " + mfc.getSectorCount());
mStatus.setStatus("Reading sectors...");
for(int i = 0; i < mfc.getSectorCount(); ++i) {
if(mfc.authenticateSectorWithKeyA(i, MifareClassic.KEY_MIFARE_APPLICATION_DIRECTORY)){
mLogger.pushStatus("Authorization granted to sector " + i + " with MAD key");
} else if(mfc.authenticateSectorWithKeyA(i, MifareClassic.KEY_DEFAULT)) {
mLogger.pushStatus("Authorization granted to sector " + i + " with DEFAULT key");
} else if(mfc.authenticateSectorWithKeyA(i, MifareClassic.KEY_NFC_FORUM)) {
mLogger.pushStatus("Authorization granted to sector " + i + " with NFC_FORUM key");
} else {
mLogger.pushStatus("Authorization denied to sector " + i);
continue;
}
for(int k = 0; k < mfc.getBlockCountInSector(i); ++k)
{
int block = mfc.sectorToBlock(i) + k;
byte[] data = null;
try {
data = mfc.readBlock(block);
} catch (IOException e) {
mLogger.pushStatus("Block " + block + " data: " + e.getMessage());
continue;
}
String blockData = Common.getHexString(data);
mLogger.pushStatus("Block " + block + " data: " + blockData);
}
}
mfc.close();
} catch (IOException e) {
mLogger.pushStatus(e.getMessage());
} finally {
try {
mfc.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
} |
private void resolveIntent(Intent intent) {
if(NfcAdapter.ACTION_TECH_DISCOVERED.equals(intent.getAction())) {
Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
MifareClassic mfc = MifareClassic.get(tag);
try {
mfc.connect();
mLogger.pushStatus("");
mLogger.pushStatus("== MifareClassic Info == ");
mLogger.pushStatus("Size: " + mfc.getSize());
mLogger.pushStatus("Timeout: " + mfc.getTimeout());
mLogger.pushStatus("Type: " + mfc.getType());
mLogger.pushStatus("BlockCount: " + mfc.getBlockCount());
mLogger.pushStatus("MaxTransceiveLength: " + mfc.getMaxTransceiveLength());
mLogger.pushStatus("SectorCount: " + mfc.getSectorCount());
mStatus.setStatus("Reading sectors...");
for(int i = 0; i < mfc.getSectorCount(); ++i) {
if(mfc.authenticateSectorWithKeyA(i, MifareClassic.KEY_MIFARE_APPLICATION_DIRECTORY)){
mLogger.pushStatus("Authorization granted to sector " + i + " with MAD key");
} else if(mfc.authenticateSectorWithKeyA(i, MifareClassic.KEY_DEFAULT)) {
mLogger.pushStatus("Authorization granted to sector " + i + " with DEFAULT key");
} else if(mfc.authenticateSectorWithKeyA(i, MifareClassic.KEY_NFC_FORUM)) {
mLogger.pushStatus("Authorization granted to sector " + i + " with NFC_FORUM key");
} else {
mLogger.pushStatus("Authorization denied to sector " + i);
continue;
}
for(int k = 0; k < mfc.getBlockCountInSector(i); ++k)
{
int block = mfc.sectorToBlock(i) + k;
byte[] data = null;
try {
data = mfc.readBlock(block);
} catch (IOException e) {
mLogger.pushStatus("Block " + block + " data: " + e.getMessage());
continue;
}
String blockData = Common.getHexString(data);
mLogger.pushStatus("Block " + block + " data: " + blockData);
}
}
mfc.close();
} catch (IOException e) {
mLogger.pushStatus(e.getMessage());
} finally {
try {
mfc.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
At first the tag is retrived from the Intent by method getParcelableExtra with argument of NfcAdapter.EXTRA_TAG. Then using static function from MifareClassic the object of this class is created from tag. The next step is to use connect method to establish the connection. At the time of connection the card must be close to the reader till the end of this method.
To actually read the data of any block the sector that this block belongs to must be authenticated with matching key. This is done with method authenticateSectorWithKeyA which expects an index of the sector and 6-byte authentication key. After authentication it is possible to read all 4 blocks from this sector which is done by method readBlock that expects an absolute index of the block (not relative to sector). Finally after doing all operations the close method should be invoked, even if an Exception occurs because it doesn’t close the connection by default and may cause a problem when trying to read the same tag with other technology.
Summary
Reading an RFID tag is pretty easy on Android devices as the SDK provides some high level API to do that. As I read the Android Beam is also supported with nice easy to use class. Unfortunately there is a problem with emulation of RFID tags because there’s no official API to for this kinds of operations as some people say. On the other hand the device supports that and it is possible to build an Android with some support of this. I shall try it when I would move to CyanogenMod.
The actual code of what I am doing with my phone and NFC can be found on GitHub here.