Mobile
Article

Learn Android NFC Basics by Building a Simple Messenger

By Ethan Damschroder

This article was peer reviewed by Tim Severien. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!


NFC (Near Field Communication) is a short range wireless method of communication between devices or between a device and an NFC ‘tag’. NFC is not limited to Android or mobile devices in general, but this tutorial is specific to the Android implementation of NFC.

By the end of this tutorial you will understand the basic concepts of NFC as well as how to set up basic communication between Android devices. You will need to have an API of 14 or higher to complete this tutorial. Although some functions introduced in API 16 are used, they are convenience functions and not required.

You can find the complete code for this tutorial on GitHub.

Formatting for NFC

NFC has a general standard created by the NFC Forum to make sure that the interface can work across different systems. This format is ‘NDEF’ (NFC Data Exchange Format) and allows us to know how information in tags is likely presented to us, and gives a way to ensure that data we create can be useful to the largest possible number of users.

For now, this is all I’ll say about formatting, but we’ll come back to it.

The Tag Dispatch System

The Android OS handles NFC through its ‘NFC Tag Dispatch System.’ This is a part of the system separate from your application that you have little control over. It’s constantly looking (assuming NFC is not disabled on the device) for NFC devices it can interface with. If the device comes within 4 centimeters of another NFC enabled device or an NFC tag the system will dispatch an intent and this is how we receive data.

Open Android Studio and create a project with a blank activity and we’ll get started.

Filtering for NFC Intents

When filtering for NFC intents you want to be as specific as you can. This is to avoid the chooser dialog appearing for apps that can handle the intent. Normally, the chooser is no problem, and often it’s the preferred behavior to let it (or force it to) show, but this is not the case for NFC.

NFC requires devices to be within centimeters of each other. If we allow the chooser to show, our user will likely move the device back to themselves to look and cancel the interaction.

The tag dispatch system has three actions it can attach to the intent it created in response to finding something it can read, write, or communicate with. The dispatch system will send out several intents, if one intent fails to find an activity to handle it the next in the list is sent.

The actions that the Tag Dispatch System will attach to its intents are:

  1. ACTION_NDEF_DISCOVERED – Sent if the information found is formatted as NDEF.
  2. ACTION_TECH_DISCOVERED – Sent if the first fails, or if the data was formatted in an unfamiliar way
  3. ACTION_TAG_DISCOVERED – The last and most general. Remember, we want to capture the intent before this, as it’s likely we will have multiple activities that have specified something this general.

We’re going to be creating a simple messenger to send and receive a list of strings.

Open AndroidManifest.xml and add the following intent filter to the main activity:

<intent-filter>
       <action android:name="android.nfc.action.NDEF_DISCOVERED" />
       <category android:name="android.intent.category.DEFAULT"/>
       <data android:mimeType="text/plain" />
</intent-filter>

As with any Android project we’re going to have to ask for the appropriate permissions. Add the following permission and feature to AndroidManifest.xml:

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

To make things smoother later, set the launch mode for the main activity to ‘single task’. This will allow us to handle intents sent to our activity without having to recreate the activity, giving a more fluid feel to our user.

<activity
           android:launchMode="singleTask"
           android:name=".MainActivity"
           android:label="@string/app_name" >

Simple Interface

We need a way to add messages to send to an array of strings. You can create your own method, or use the simple interface I have below.

Change activity_main.xml to:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
    android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity">

   <EditText
       android:id="@+id/txtBoxAddMessage"
       android:layout_width="match_parent"
       android:layout_height="wrap_content" />

   <Button
       android:id="@+id/buttonAddMessage"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:onClick="addMessage"
       android:layout_below="@+id/txtBoxAddMessage"
       android:layout_centerHorizontal="true" />

   <TextView
       android:id="@+id/txtMessagesReceived"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:layout_below="@+id/buttonAddMessage"
       android:layout_alignParentEnd="true"
       android:layout_alignParentRight="true"/>


   <TextView
       android:id="@+id/txtMessageToSend"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:layout_alignTop="@+id/txtMessagesReceived"
       android:layout_alignParentLeft="true"
       android:layout_alignParentStart="true"/>

</RelativeLayout>

Update MainActivity.java to the following:

public class MainActivity extends AppCompatActivity {
    //The array lists to hold our messages
    private ArrayList<String> messagesToSendArray = new ArrayList<>();
    private ArrayList<String> messagesReceivedArray = new ArrayList<>();

    //Text boxes to add and display our messages
    private EditText txtBoxAddMessage;
    private TextView txtReceivedMessages;
    private TextView txtMessagesToSend;

    public void addMessage(View view) {
        String newMessage = txtBoxAddMessage.getText().toString();
        messagesToSendArray.add(newMessage);

        txtBoxAddMessage.setText(null);
        updateTextViews();

        Toast.makeText(this, "Added Message", Toast.LENGTH_LONG).show();
    }


    private  void updateTextViews() {
        txtMessagesToSend.setText("Messages To Send:\n");
        //Populate Our list of messages we want to send
        if(messagesToSendArray.size() > 0) {
            for (int i = 0; i < messagesToSendArray.size(); i++) {
                txtMessagesToSend.append(messagesToSendArray.get(i));
                txtMessagesToSend.append("\n");
            }
        }

        txtReceivedMessages.setText("Messages Received:\n");
        //Populate our list of messages we have received
        if (messagesReceivedArray.size() > 0) {
            for (int i = 0; i < messagesReceivedArray.size(); i++) {
                txtReceivedMessages.append(messagesReceivedArray.get(i));
                txtReceivedMessages.append("\n");
            }
        }
    }

    //Save our Array Lists of Messages for if the user navigates away
    @Override
    public void onSaveInstanceState(@NonNull Bundle savedInstanceState) {
        super.onSaveInstanceState(savedInstanceState);
        savedInstanceState.putStringArrayList("messagesToSend", messagesToSendArray);
        savedInstanceState.putStringArrayList("lastMessagesReceived",messagesReceivedArray);
    }

    //Load our Array Lists of Messages for when the user navigates back
    @Override
    public void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
        super.onRestoreInstanceState(savedInstanceState);
        messagesToSendArray = savedInstanceState.getStringArrayList("messagesToSend");
        messagesReceivedArray = savedInstanceState.getStringArrayList("lastMessagesReceived");
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        txtBoxAddMessage = (EditText) findViewById(R.id.txtBoxAddMessage);
        txtMessagesToSend = (TextView) findViewById(R.id.txtMessageToSend);
        txtReceivedMessages = (TextView) findViewById(R.id.txtMessagesReceived);
        Button btnAddMessage = (Button) findViewById(R.id.buttonAddMessage);

        btnAddMessage.setText("Add Message");
        updateTextViews();
    }
}

Checking for NFC Support

In the MainActivity.java onCreate() method add the following to handle when NFC is not supported on the device:

//Check if NFC is available on device
mNfcAdapter = NfcAdapter.getDefaultAdapter(this);
if(mNfcAdapter != null) {
    //Handle some NFC initialization here
  }
  else {
      Toast.makeText(this, "NFC not available on this device",
                Toast.LENGTH_SHORT).show();
  }

Make sure to create the mNfcAdapter variable at the top of the class definition:

private NfcAdapter mNfcAdapter;

Creating Our Message

Android provides useful classes and functions that allow us to package our data. To conform to NDEF, we can create NdefMessages which contain one or more NdefRecords.

To send a message, we have to create it first. There are two main ways to handle this:

  1. Call setNdefPushMessage() in the NfcAdapter class. This will accept an NdefMessage sent when detecting another NFC capable device.
  2. Override callbacks so that our NdefMessage will be created only when it needs to be sent.

Number one is the preferred method if the data will not change. Our data will be changing, so we’ll use option number two.

To handle message sending we need to specify callbacks to respond to NFC events. Since we will be updating our data we want the message to be created only when it needs to be sent.

Update the activity like so:

public class MainActivity extends Activity
       implements NfcAdapter.OnNdefPushCompleteCallback,
                  NfcAdapter.CreateNdefMessageCallback

Override the relevant functions:

@Override
public void onNdefPushComplete(NfcEvent event) {
    //This is called when the system detects that our NdefMessage was
    //Successfully sent.
    messagesToSendArray.clear();
}

@Override
public NdefMessage createNdefMessage(NfcEvent event) {
    //This will be called when another NFC capable device is detected.
    if (messagesToSendArray.size() == 0) {
        return null;
    }
    //We'll write the createRecords() method in just a moment
    NdefRecord[] recordsToAttach = createRecords();
    //When creating an NdefMessage we need to provide an NdefRecord[]
    return new NdefMessage(recordsToAttach);
}

Now make sure to specify these callbacks in the onCreate method:

//Check if NFC is available on device
mNfcAdapter = NfcAdapter.getDefaultAdapter(this);
if(mNfcAdapter != null) {
    //This will refer back to createNdefMessage for what it will send
    mNfcAdapter.setNdefPushMessageCallback(this, this);

    //This will be called if the message is sent successfully
    mNfcAdapter.setOnNdefPushCompleteCallback(this, this);
}

Creating the Records

There are multiple utility functions within the NdefRecord class that can return a properly formatted NdefRecord, but to understand the concept and give us more flexibility we’re going to manually create a NdefRecord first.

In NDEF there are four parts to a record:

  1. A short that specifies the type name of our payload from a list of constants.
  2. A variable length byte[] that gives more detail about our type.
  3. A variable length byte[] used as a unique identifier. This is neither required or often used.
  4. A variable length byte[] that is our actual payload

Add this function to MainActivity.java:

public NdefRecord[] createRecords() {

    NdefRecord[] records = new NdefRecord[messagesToSendArray.size()];

    for (int i = 0; i < messagesToSendArray.size(); i++){

        byte[] payload = messagesToSendArray.get(i).
           getBytes(Charset.forName("UTF-8"));

        NdefRecord record = new NdefRecord(
                NdefRecord.TNF_WELL_KNOWN,  //Our 3-bit Type name format
                NdefRecord.RTD_TEXT,        //Description of our payload
                new byte[0],                //The optional id for our Record
                payload);                   //Our payload for the Record

        records[i] = record;
    }
    return records;
}

Since we’re writing both the sender and receiver we can be specific about how we want our data handled. We can call NdefRecord.createApplicationRecord to attach a specially formatted NdefRecord that will tell the OS which application should handle the data. The system will attempt to open the application to handle the data before any other.

It doesn’t matter where in the NdefRecord[] array we include this record, as long as it’s present anywhere it will work. Make sure to adjust the length of our NdefRecord[] to be one longer to accommodate the additional record and add the following before the return in the createRecords() function.

//Remember to change the size of your array when you instantiate it.
records[messagesToSendArray.size()] =
           NdefRecord.createApplicationRecord(getPackage());

An advantage of creating and attaching an Android Application Record is that if Android cannot find the application it will open a connection to the Google Play store and attempt to download your application (assuming it exists).

Note: This doesn’t make the transaction secure or ensure that your app will be the one to open it. Including the application record only further specifies our preference to the OS. If another activity that is currently in the foreground calls NfcAdapter.enableForegroundDispatch it can catch the intent before it gets to us, there is no way to prevent this except to have our activity in the foreground. Still, this is as close as we can get to ensuring that our application is the one that processes this data.

As mentioned, it’s generally preferred to use the provided utility functions to create the Records. Most of these functions were introduced in API 16, and we are writing for 14 or higher. So we cover all bases, let’s include a check for the API level and create our record in the preferred manner if the function is available to us. Change the createRecords() function to this:

public NdefRecord[] createRecords() {
    NdefRecord[] records = new NdefRecord[messagesToSendArray.size() + 1];
        //To Create Messages Manually if API is less than
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
            for (int i = 0; i < messagesToSendArray.size(); i++){
               byte[] payload = messagesToSendArray.get(i).
                             getBytes(Charset.forName("UTF-8"));
               NdefRecord record = new NdefRecord(
                       NdefRecord.TNF_WELL_KNOWN,      //Our 3-bit Type name format
                       NdefRecord.RTD_TEXT,            //Description of our payload
                       new byte[0],                    //The optional id for our Record
                       payload);                       //Our payload for the Record

               records[i] = record;
           }
       }
       //Api is high enough that we can use createMime, which is preferred.
       else {
           for (int i = 0; i < messagesToSendArray.size(); i++){
               byte[] payload = messagesToSendArray.get(i).
                                getBytes(Charset.forName("UTF-8"));

               NdefRecord record = NdefRecord.createMime("text/plain",payload);
               records[i] = record;
           }
       }
      records[messagesToSendArray.size()] =
       NdefRecord.createApplicationRecord(getPackageName());
       return records;
  }

Processing the Message

The intent received will contain an NdefMessage[] array. Since we know the length, it’s easy to process.

private void handleNfcIntent(Intent NfcIntent) {
    if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(NfcIntent.getAction())) {
            Parcelable[] receivedArray =
                NfcIntent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES);

        if(receivedArray != null) {
            messagesReceivedArray.clear();
            NdefMessage receivedMessage = (NdefMessage) receivedArray[0];
            NdefRecord[] attachedRecords = receivedMessage.getRecords();

            for (NdefRecord record:attachedRecords) {
                String string = new String(record.getPayload());
                //Make sure we don't pass along our AAR (Android Application Record)
                if (string.equals(getPackageName())) { continue; }
                messagesReceivedArray.add(string);
            }
            Toast.makeText(this, "Received " + messagesReceivedArray.size() +
                    " Messages", Toast.LENGTH_LONG).show();
            updateTextViews();
        }
        else {
            Toast.makeText(this, "Received Blank Parcel", Toast.LENGTH_LONG).show();
        }
    }
}


@Override
public void onNewIntent(Intent intent) {
        handleNfcIntent(intent);
}

We are overriding onNewIntent so we can receive and process the message without creating a new activity. It’s not necessary but will help make everything feel fluid. Add a call to handleNfcIntent in the onCreate() and onResume() functions to be sure that all cases are handled.

@Override
  public void onResume() {
      super.onResume();
      updateTextViews();
      handleNfcIntent(getIntent());
  }

That’s it! You should have a simple functioning NFC messenger. Attaching different types of files is as easy as specifying a different mime type and attaching the binary of the file you want to send. For a full list of supported types and their convenience constructors take a look at the NdefMessage and NdefRecord classes in the Android documentation. More complex features are available on Android with NFC such as emulating an NFC tag so that we can passively read, but that is beyond a simple messenger application.

No Reader comments

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in Mobile, once a week, for free.