Building A Relentless Android Alarm Clock. Part 1 – NFC
Ever feel like falling prey to the well known snooze syndrome in the morning? Do you have a strong resolve in the evening to get up early, and then an even stronger resentment towards actually having to get up? Struggle no more, for the tables are finally turning! The rescue arrives disguised as a relentless Android Alarm Clock that will get you out of bed for good.
We are going to build an Android Alarm Clock App which requires the user to get up, get to the bathroom and tap the smartphone against an NFC tag, glued to your mirror, or wherever you want it, in order to stop ringing. The first part of this blog is going to concentrate around mere NFC technology handling, while the latter is going to make sure it is almost impossible for the user to make the phone stop ringing in any other manner than expected. Now, how cool is that?
Technical background of the NFC technology
We will start with some theoretical background. Near field communication is based off of Radio Frequency Identification, or RFID, which has been invented in the early 1980s, and is widely used to this day, i.e. in real time location tracking systems, libraries and access control systems, to name but a few. Sony and a company called NXP Semiconductors then invented the new NFC technology in 2002. The first Android device to ever incorporate NFC was the Nexus S, manufactured by Samsung.
In time, more and more manufacturers began conforming to this emerging standard, opening many more opportunities that we experience today, i.e. NFC payments, providing contextual information or bonus content in the form of tags built into posters and various adverts, in much the same way that QR codes have been used for several years now.
Alongside the rising popularity of NFC, came the standards of formatting the accessible information stored in tags. The most widely used of these is the NDEF standard, formed by the NFC Forum. This is also the standard we will conform to.
3 types of intent filters within NFC
Android offers many usages of NFC; it is even capable of emulating a payment card. Due to the many various usages and standards within the NFC itself, there was a necessity of defining three types of intent filters, to take on the intents dispatched by the system. These are:
- android.nfc.action.NDEF_DISCOVERED
- android.nfc.action.TECH_DISCOVERED
- android.nfc.action.TAG_DISCOVERED
Our app will incorporate the first and the last of these.
The following chart shows how Android handles incoming NFC intents:
For more thorough information, please take a look at the official documentation.
5 steps to create your Android Alarm Clock with NFC
#1 Setup
Alright, enough of that history and background lesson, let’s get to work.
Let’s dive right into the code. What you are going to need, first and foremost, is to make sure you have a device with an NFC chip on the board. However, there’s no need to fear, as we will also check this during the runtime as well. First off, add these permissions to your AndroidManifest:
1 2 | <uses-permission android:name="android.permission.NFC" /> <uses-feature android:name="android.hardware.nfc" android:required="true" /> |
The latter one, as you may know, will ensure that only devices equipped with the proper technology will be seeing the app in the Play Store.
#2 Initializing the NFC adapter
Afterwards, an NFC adapter has to be initialized in your alarm Activity’s code.
1 2 3 4 5 6 7 | private NfcAdapter nfcAdapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); nfcAdapter = NfcAdapter.getDefaultAdapter(this); startAlarm(); } |
Next, we are going to make sure that the device is equipped with an NFC chip, turned on via the user’s device, and can also act accordingly if it’s not, redirecting him/her to the system settings. We also check if the device is equipped with the NFC technology here, in case the user obtained the app the other way than from the Play Store:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | private void checkForNfcService() { if (nfcAdapter == null) { Toast.makeText(this, "This device doesn`t support NFC", Toast.LENGTH_SHORT).show(); finish(); } else { if (!nfcAdapter.isEnabled()) { Snackbar .make(coordinatorLayout, "NFC service is disabled", Snackbar.LENGTH_INDEFINITE) .setAction("SETTINGS", new View.OnClickListener() { @Override public void onClick(View v) { startActivityForResult(new Intent(Settings.ACTION_NFC_SETTINGS), 0); } }) .setActionTextColor(Color.YELLOW) .show(); } else { Snackbar.make(coordinatorLayout, "NFC service is available!", Snackbar.LENGTH_SHORT).show(); } } } |
#3 Intercepting the NFC tag intent
In order for your application to be able to react to the NFC tag intent dispatched by the system, we have to define the following intent filters for our activity:
1 2 3 4 5 6 7 8 9 10 11 12 13 | <intent-filter> <action android:name="android.nfc.action.NDEF_DISCOVERED" /> <category android:name="android.intent.category.DEFAULT" /> <data android:mimeType="text/plain" /> </intent-filter> <intent-filter> <action android:name="android.nfc.action.TAG_DISCOVERED" /> <category android:name="android.nfc.category.DEFAULT" /> </intent-filter> |
Then the incoming intent, along with the data it carries, can be intercepted by your Activity like this:
1 2 3 4 5 6 | @Override protected void onNewIntent(Intent intent) { if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(intent.getAction())) { handleIncomingNdefDataIntent(intent); } } |
By default, when dispatching the discovered tag, the system will prompt the user to indicate which application should be used to intercept said tag data. This behavior will not make it as seamless an experience as we wish it to be. Fortunately, the Android platform enables us to intercept the incoming intent in the foreground, meaning that the system is informed which activity is specializing in intercepting such intents.
However, there is an impediment to this. The foreground dispatch activity must be in its active state, brought to the foreground for the user, for security purposes. Add the following code to your activity, in order for it to be marked as a prioritized NFC tag intent interceptor:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | @Override protected void onResume() { super.onResume(); final Intent intent = new Intent(this, getClass()); intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); final PendingIntent pendingIntent = PendingIntent.getActivity(this , 0, intent, 0); nfcAdapter.enableForegroundDispatch(this, pendingIntent, null, null); } @Override protected void onPause() { super.onPause(); nfcAdapter.disableForegroundDispatch(this); } |
#4 Reading NFC tag data
After we have tag data intent interception up and running, it`s time to decode it and make use of:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | private void handleIncomingNdefDataIntent(Intent intent) { if ("text/plain".equals(intent.getType())) { Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG); Ndef ndef = Ndef.get(tag); 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)) { if(readNfcTagText(ndefRecord).equals("Your custom tag content")) { Toast.makeText(this, "alarm tag attached", Toast.LENGTH_LONG).show(); stopAlarm(); finish(); } else { Toast.makeText(this, "You are using a wrong tag", Toast.LENGTH_LONG).show(); } } } } else { Toast.makeText(this, "You are using a wrong tag", Toast.LENGTH_LONG).show(); } } |
The method above ensures that the right type of tag, with the proper data, has been attached and stops the alarm if done just so. Now, let’s take a look at the actual decoding method, readNfcTagText() :
1 2 3 4 5 6 | private String readNfcTagText(NdefRecord ndefRecord) { byte[] payload = ndefRecord.getPayload(); // i.e {2, 101, 110, 97, 108, 97, 114, 109} Charset textEncodingCharset = Charset.forName(((payload[0] & 128) == 0) ? "UTF-8" : "UTF-16"); int languageCodeLength = payload[0] & 51; return new String(payload, languageCodeLength + 1, payload.length - languageCodeLength - 1, textEncodingCharset); } |
The bitwise operations make it seem dreadful, though it’s not very complicated and derives directly from how the NDEF data record payload is organized. Having a closer look at the data payload shows us that, in this case, it consists merely out of ASCII characters. ASCII code ‘2’ denotes the start of text, whereas decoding the remainder leaves us with a sequence “enalarm”.
The first two characters are an abbreviation of the language of the data being decoded, while the remaining characters make up the actual message encoded into a tag, which in this case was the word “alarm”. Such a message is then passed to the String constructor, letting us present that data to the user, or use it in another manner.
#5 Writing data to NFC tag
Writing data to a tag comes last, as it’s relatively the most complex topic to cover. We have to handle many cases, such as the technologies supported by tag, its capacity, data format, connection breach during writing and even the fact that the tag might be set to read-only mode.
Writing a plain text data into a tag can be done as follows:
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 | @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); if (NfcAdapter.ACTION_TAG_DISCOVERED.equals(intent.getAction()) || NfcAdapter.ACTION_NDEF_DISCOVERED.equals(intent.getAction())) { Tag detectedTag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG); if (isSupportedTagType(detectedTag.getTechList())) { try { WriteResponse tagWritingResponse = writeTag(createNdefMessage(true), detectedTag); String message = (tagWritingResponse.isSuccessful() ? "Success: " : "Failed: ") + tagWritingResponse.getMessage(); Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); } catch (UnsupportedEncodingException ex) { Toast.makeText(this, "Defined encoding is not supported", Toast.LENGTH_SHORT).show(); } } else { Toast.makeText(this, "This tag type is not supported", Toast.LENGTH_SHORT).show(); } } } public static boolean isSupportedTagType(String[] techs) { boolean ultralight = false; boolean nfcA = false; boolean ndef = false; for (String tech : techs) { switch (tech) { case "android.nfc.tech.MifareUltralight": ultralight = true; break; case "android.nfc.tech.NfcA": nfcA = true; break; case "android.nfc.tech.Ndef": case "android.nfc.tech.NdefFormatable": ndef = true; break; } } return ultralight && nfcA && ndef; } |
The onNewIntent() method above is pretty self-explanatory. This time, we also have to cover cases in which the tag is not NDEF-formatted, hence the additional intercepted intent type applies. Let’s take a look at the actual writing method, since we already know that our tag is compliant to the most popular NFC tag specifications. The writing method compiles all the edge cases we potentially want to have covered:
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 | public WriteResponse writeTag(NdefMessage ndefMessage, Tag tag) { int messageSize = ndefMessage.toByteArray().length; try { Ndef ndef = Ndef.get(tag); if (ndef != null) { //if the tag is already NDEF-formatted ndef.connect(); if (!ndef.isWritable()) { return new WriteResponse(false, "Tag is read-only"); } if (ndef.getMaxSize() < messageSize) { return new WriteResponse(false, "Tag capacity is " + ndef.getMaxSize() + " bytes, message is " + messageSize + " bytes."); } ndef.writeNdefMessage(ndefMessage); return new WriteResponse(true, "Wrote message to pre-formatted tag."); } else { // if we need to format the tag to NDEF standard NdefFormatable format = NdefFormatable.get(tag); if (format != null) { try { format.connect(); format.format(ndefMessage); return new WriteResponse(true, "Formatted tag and wrote message"); } catch (IOException e) { return new WriteResponse(false, "Failed to format tag."); } } else { return new WriteResponse(false, "Tag doesn't support NDEF."); } } } catch (Exception e) { return new WriteResponse(false, "Failed to write tag"); } } |
Now, it’s time to look at the method which creates the actual NDEF message we want to put in the tag. As mentioned earlier, for more thorough information about the protocol, refer to this site. This however, might not be needed, as the applicable knowledge has been laid out earlier in this post.
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 | private NdefMessage createNdefMessage(boolean addAndroidApplicationRecord) throws UnsupportedEncodingException { String text = "alarm"; String lang = "en"; byte[] textBytes = text.getBytes(); byte[] langBytes = lang.getBytes("UTF-8"); byte[] payload = new byte[1 + langBytes.length + textBytes.length]; payload[0] = (byte) langBytes.length; // copy langbytes and textbytes into payload System.arraycopy(langBytes, 0, payload, 1, langBytes.length); System.arraycopy(textBytes, 0, payload, 1 + langBytes.length, textBytes.length); NdefRecord rtdTextRecord = new NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_TEXT, new byte[0], payload); if (addAndroidApplicationRecord) { // add a record to launch an application on tag discovery return new NdefMessage( new NdefRecord[] { rtdTextRecord, NdefRecord.createApplicationRecord("your app package id") } ); } else { return new NdefMessage(new NdefRecord[] { rtdTextRecord }); } } private final class WriteResponse { private final boolean successful; private final String message; WriteResponse(boolean successful, String message) { this.successful = successful; this.message = message; } boolean isSuccessful() { return successful; } String getMessage() { return message; } } |
Wrap up
NFC is not a particularly complex concept to grasp and implement, yet it can add huge value to your product. To my mind, the technology is pure fun, both during implementation and application usage.
However, there are certain limitations, which have to be kept in mind, i.e. NFC tag capacity. Also, for security purposes, Android only permits NFC scanning when the device is unlocked, unless your user has explicitly set an NFC tag to be a trusted device for SmartLock. Nonetheless, the range of possible NFC applications is still mainly limited by your imagination.
Don’t miss the second part of my guide dedicated to Android Alarm Clock. We will dive deeper into Android background to make your alarm almost impossible to be turned off without using NFC tag.
Sign for our newsletter below and stay tuned!
Ready to take your business to the next level with a digital product?
We'll be with you every step of the way, from idea to launch and beyond!