Getting Started With IPWorksNFC on Android
Requirements: IPWorksNFC
Introduction
IPWorksNFC provides a simple, event-driven API for Near-Field Communication. This library enables applications to read and write to NFC tags, as well as process various NDEF record types.
This guide will walk through project setup, handling NFC intents, reacting to NFC events, and reading/writing tags.
Project Setup
First, add the IPWorksNFC library to your Android project. Include the JAR package in your project's /libs folder or configure it via Gradle if available.
Afterwards, before the component can run successfully, the project's AndroidManifest.xml file must be configured appropriately. At minimum, the following permission and feature need to be added:
<uses-permission android:name="android.permission.NFC" />
<uses-feature android:name="android.hardware.nfc" android:required="true" />
Above, android:required may be set to false if your app doesn't require NFC to run but can take advantage of NFC when present.
Lastly, you may wish to declare certain intent filters to ensure your Activity is eligible to handle NFC-related intents, even if your Activity is not on the foreground. There are three types of NFC intents that may be applicable when scanning a tag, being ACTION_NDEF_DISCOVERED, ACTION_TECH_DISCOVERED, and ACTION_TAG_DISCOVERED. For example, the following intents may be added to the AndroidManifest.xml file:
<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="/" />
</intent-filter>
<intent-filter>
<action android:name="android.nfc.action.TECH_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<meta-data android:name="android.nfc.action.TECH_DISCOVERED"
android:resource="@xml/nfc_tech_filter" />
For information about handling these intents in your Activity, refer to the Foreground Dispatch System section below. For additional information regarding NFC intents, please refer to the Android documentation here.
Initialization
To use the NFC component, you will need to import the required classes in your Activity:
import ipworksnfc.*;
After doing so, create the NFC component in your Activity's onCreate method and pass your Activity context to the constructor:
public class MainActivity extends AppCompatActivity {
private NFC m_NFC;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
m_NFC = new NFC(this); // initialize with Activity context
// Add event handlers
try {
m_NFC.addNFCEventListener(new DefaultNFCEventListener() {
public void error(NFCErrorEvent args) { }
public void log(NFCLogEvent args) { }
public void record(NFCRecordEvent args) { }
public void tag(NFCTagEvent args) { }
});
} catch (Exception ex) {
System.out.println("Error: " + ex.getMessage());
}
}
}
Foreground Dispatch System
The foreground dispatch system allows your Activity to receive NFC tag intents with priority when it is in the foreground. By enabling foreground dispatch, your app can intercept NFC intents before they are handled by other apps. The component provides three methods to integrate with this system:
- EnableForegroundDispatch
- DisableForegroundDispatch
- HandleIntent
To enable the foreground dispatch for your Activity, call the EnableForegroundDispatch method from the onResume callback like so:
@Override
protected void onResume() {
super.onResume();
try {
m_NFC.enableForegroundDispatch();
} catch (IPWorksNFCException e) {
Toast.makeText(this, e.getMessage(), Toast.LENGTH_LONG).show();
}
}
This ensures your Activity intercepts NFC tag scans before other applications. Foreground dispatch is optional, but recommended when your Activity is actively running. Without it, Android may route tag intents to other apps.
To disable the foreground dispatch for your Activity, call the DisableForegroundDispatch method from the onPause callback like so:
@Override
protected void onPause() {
super.onPause();
try {
m_NFC.disableForegroundDispatch();
} catch (IPWorksNFCException e) {
Toast.makeText(this, e.getMessage(), Toast.LENGTH_LONG).show();
}
}
Disabling dispatch when the Activity is paused is critical to avoid unexpected behavior and system warnings.
Lastly, when a tag is discovered, Android delivers the tag's intent to the onNewIntent callback. Forward this intent to the component using the HandleIntent method so that it can process the NFC intent (either NDEF_DISCOVERED, TECH_DISCOVERED, or TAG_DISCOVERED) like so:
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
try {
m_NFC.handleIntent(intent);
} catch (IPWorksNFCException e) {
Toast.makeText(this, e.getMessage(), Toast.LENGTH_LONG).show();
}
}
The component filters the intent automatically, so no additional checks or filters are required here. For more information regarding the foreground dispatch system, please refer to the Android documentation here.
Reading a Tag
To read the data from a tag, you must call the Read method. After doing so, simply place the tag near (typically very close to) the device. Please note that the Read method is blocking. To prevent the UI from freezing, the method can be called from a separate thread like so:
new Thread(() -> {
try {
m_NFC.read();
runOnUiThread(() -> {
try {
Toast.makeText(this, "Read finished successfully: " + nfcManager.getTagSerialNumber(), Toast.LENGTH_SHORT).show();
} catch (IPWorksNFCException e) {
Toast.makeText(this, "Read finished successfully (error reading tag serial number).", Toast.LENGTH_SHORT).show();
}
dialog.dismiss();
});
} catch (IPWorksNFCException e) {
runOnUiThread(() -> Toast.makeText(this, "Read error: " + e.getMessage(), Toast.LENGTH_SHORT).show());
dialog.dismiss();
}
}).start();
After the tag is detected, the component will attempt to parse the tag information and any stored records. As soon as the component has read all of the tag information, the Tag event will fire, containing relevant details about the tag, such as the tag's type and serial number, current and maximum size (i.e., the amount of data the tag is currently storing or can store), and whether the tag can be written to. For example, in the Tag event:
public void tag(NFCTagEvent args) {
System.out.println("Tag Detected. Details:");
System.out.println("Tag Type: " + args.forumType);
System.out.println("Serial Number: " + args.serialNumber);
System.out.println("Size: " + args.size + " / " + args.maxSize + " Bytes");
System.out.println("Writable: " + args.writable);
}
After the Tag event fires, the Record event will fire for each record that is detected. The Record event will contain relevant details about the record, such as the record TNF (type name format, or record type format), the actual record type (which is interpreted based on the TNF), the payload (actual record content), and the optional record ID (which is typically empty). For example, in the Record event:
public void record(NFCRecordEvent args) {
System.out.println("Record Detected.");
System.out.println("TNF: " + args.recordTypeFormat);
System.out.println("Record Type: " + args.recordType);
System.out.println("Record Payload: " + args.payload);
}
Note that, in addition to the tag and record details being made available via their corresponding events, the Tag property and Records collection will be populated after the Read method returns successfully. Tag and record details can also be accessed like so:
// Printing tag info
public void printTagInfo() {
NFCTag tag = m_NFC.getTag();
System.out.println("Tag Type: " + tag.getForumType());
System.out.println("Serial Number: " + tag.getSerialNumber());
System.out.println("Size: " + tag.getSize() + " / " + tag.getMaxSize() + " Bytes");
System.out.println("Writable: " + tag.getWritable());
}
// Printing record info
public void printRecordInfo() {
int recordNum = 1;
for (NDEFRecord record : m_NFC.getRecords()) {
System.out.println("Record " + recordNum + " Info");
System.out.println("TNF: " + record.getRecordTypeFormat());
System.out.println("Record Type: " + record.getRecordType());
System.out.println("Record Payload: " + record.getPayload());
System.out.println();
recordNum++;
}
}
Parsing Records
As mentioned, the Record event provides the raw components of each NDEF record detected on a tag. An NDEF (NFC Data Exchange Format) record is the standard way NFC tags encode and store data. Each record contains four key parts exposed by the Record event.
Typically, the RecordTypeFormat will be checked first. This parameter represents the TNF (Type Name Format) associated with the record. Possible values for this parameter are:
Value | Name | Description |
---|---|---|
0x00 | Empty | Empty record (padding or placeholder) |
0x01 | Well-Known | Standard types like text (T) and URI (U) |
0x02 | MIME Media Type | Type is a MIME type (e.g., text/plain, image/jpeg) |
0x03 | Absolute URI | Type field contains a full URI |
0x04 | External Type | Namespaced external types (e.g., com.example:type) |
0x05 | Unknown | Type is not provided or known |
0x06 | Unchanged | Used for chunked data (not common) |
After checking the value of the RecordTypeFormat, the RecordType can then be interpreted. As an example, if the TNF of a record is 0x01 (Well-Known), the RecordType is usually a single character that identifies the type of record. This value would be "T" to indicate a text record, or "U" to indicate a URI/URL record.
In either case, the mentioned parameters can be used to then determine how to interpret the Payload parameter, which is ultimately left up to the developer. For example, given a RecordTypeFormat of 0x01 and a RecordType of "T" (Well-Known Text Record), the following code can be used to extract the relevant data out of the Payload:
try {
String recordType = new String(record.getRecordType(), "UTF-8");
if (record.getRecordTypeFormat() == NDEFRecord.rtfWellKnown && recordType.equals("T")) {
byte[] payload = record.getPayload();
byte status = payload[0];
boolean isUtf16 = (status & 0x80) != 0;
int langCodeLen = status & 0x3F;
String encoding = isUtf16 ? "UTF-16" : "UTF-8";
String languageCode = new String(payload, 1, langCodeLen, "US-ASCII");
String text = new String(payload, 1 + langCodeLen, payload.length - 1 - langCodeLen, encoding);
System.out.println("Text Record (" + languageCode + "): " + text);
}
} catch (Exception e) {
// Handle exception
}
The format of a text record is defined and standardized, so the payload can be parsed in a predictable way (status byte, language code, then the actual text). Other record types follow similar conventions. For example:
- For a URI record (TNF = 0x01, RecordType = "U"), the first byte of the payload is the URI prefix code (e.g., 0x01 for "http://www.), followed by the rest of the URI string in UTF-8.
- For a MIME Media record (TNF = 0x02), the record type is a MIME type (e.g., "text/plain"), and the payload contains raw data of that type.
In practice, this means that the RecordTypeFormat (TNF) tells you how to interpret the RecordType, which in turn tells you how to parse the Payload. By combining these, you can effectively parse any NDEF record type.
Writing to a Tag
To write records to a tag, the Records collection should first be populated with any records that are to be written to the tag. To do so, the API exposes three methods:
- AddTextRecord
- AddURLRecord
- AddRecord
The AddTextRecord method can be used to add a well-formed text record to the Records collection. This is typically used to encode human-readable text. This method takes two parameters: the actual text to store on the tag, and the language code that specifies the language of the text (for example, "en" for English).
The AddURLRecord method can be used to add a URI or URL record to the Records collection. This method takes a single parameter, which is the URL string to store on the tag.
For all other types of records, including custom or MIME-based records, the AddRecord method may be used. This method requires specifying the TNF (Type Name Format), the record type, an optional record ID, and the record payload. As there are many possible types of records, this method is intended to give developers full control over record creation. While AddTextRecord and AddURLRecord cover some common scenarios, AddRecord allows developers to handle any custom, proprietary, or less common NDEF records without introducing additional specialized methods. As an example, you could create a MIME Media record like containing JSON like so:
try {
int recordTypeFormat = 0x02;
byte[] recordType = "application/json".getBytes(Charset.forName("US-ASCII"));
byte[] id = new byte[0];
// Prepare JSON
JSONObject jsonObject = new JSONObject();
jsonObject.put("name", "NFC Example");
jsonObject.put("version", "1.0");
jsonObject.put("data", "some_json_data");
byte[] payload = jsonObject.toString().getBytes(Charset.forName("UTF-8"));
m_NFC.addRecord(recordTypeFormat, recordType, payload, id);
} catch (Exception ex) {
System.out.println("Failed to add JSON MIME record: " + ex.getMessage());
}
Once the desired records are added to the Records collection, the Write method should be called to write the records to the tag. If multiple records are added, they will be written together as a single NDEF message. Please note that the Write method is blocking. To prevent the UI from freezing, the method can be called from a separate thread like so:
new Thread(() -> {
try {
m_NFC.write();
runOnUiThread(() -> {
dialog.dismiss();
Toast.makeText(this, "Write finished successfully.", Toast.LENGTH_SHORT).show();
});
} catch (IPWorksNFCException e) {
runOnUiThread(() -> {
dialog.dismiss();
Toast.makeText(this, "Write error: " + e.getMessage(), Toast.LENGTH_SHORT).show();
});
}
}).start();
Note that if you previously read a tag, the Records collection will likely be populated. Unless you are preserving the existing records from the prior read operation, you can clear the records from the collection like so:
m_NFC.getRecords().clear();
After clearing the records, you can add new records to write using the methods described previously.
We appreciate your feedback. If you have any questions, comments, or suggestions about this article please contact our support team at support@nsoftware.com.