Sunday, May 13, 2012

How to implement Android Beam NFC easily

In this post I will explain how to create an application that can send arbitrary data to the same application using NFC. Especially when it becomes so easy with the introduction of Android Beam in Android 4.0.

I implemented this in 30 minutes in the Bible for Android application. When you view a verse in the app, you may want your friend to see the same verse on his phone. With NFC, it's extremely easy. Just touch the back of your phone to the back of your friend's phone, and the Android Beam animation will be displayed in your phone. Tap the center of it, and instantly your friend's phone shows the same verse, after launching the Bible app if it's not already running.

There are two parts in this tutorial. The first part is how to integrate message sending and receiving into your application's activity, and the second part is how to format the data for sending through NFC.

Part 1 Sending and receiving NFC messages

1. Add the required permission to your app manifest.

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

2. If your app does not require NFC, in order for it to be installed on devices without NFC, make sure you declared it as optional on the app manifest.

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

3. On your app's activity that is able to send NFC messages, initialize NFC:

if (Build.VERSION.SDK_INT >= 14) { // Android Beam is only for 4.0+
nfcAdapter = NfcAdapter.getDefaultAdapter(getApplicationContext());
}

(if it returns null, it means that NFC is not available on the device. So do not call any methods on  nfcAdapter or you'll get a nurupo.)

4. Set a callback, which will be called when you tap on the screen to initiate Android Beam message sending. Here you construct the message to be sent to the other device.

nfcAdapter.setNdefPushMessageCallback(new CreateNdefMessageCallback() {
@Override public NdefMessage createNdefMessage(NfcEvent event) {
NdefMessage msg = ... // described below
return msg;
}
}, this);

5. Create a method to check if an intent is an NFC message from another device, and if it is, parse and do something about it.

private void checkAndProcessBeamIntent(Intent intent) {
String action = intent.getAction();
if (U.equals(action, NfcAdapter.ACTION_NDEF_DISCOVERED)) {
Parcelable[] rawMsgs = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES);
// only one message sent during the beam
if (rawMsgs != null && rawMsgs.length > 0) {
NdefMessage msg = (NdefMessage) rawMsgs[0];
// record 0 contains the MIME type, record 1 is the AAR, if present
NdefRecord[] records = msg.getRecords();
if (records != null && records.length > 0) {
// process records[0].getPayload()
}
}
}
}

6. Enable on your activity the "foreground dispatch", so that your activity will receive directly a NFC message, without having Android OS create a new instance of your activity when getting an Android Beam.

@Override protected void onResume() {
super.onResume();
if (Build.VERSION.SDK_INT >= 14) {
if (nfcAdapter != null) {
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, new Intent(this, getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0);
IntentFilter ndef = new IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED);
   try {
       ndef.addDataType("application/vnd.yuku.yourappname.nfc.beam"); // change to your mime type, can be whatever mime type
   } catch (MalformedMimeTypeException e) {
       throw new RuntimeException("fail mime type", e);
   }
   IntentFilter[] intentFiltersArray = new IntentFilter[] {ndef, };
   nfcAdapter.enableForegroundDispatch(this, pendingIntent, intentFiltersArray, null);
}
}
}

Don't forget to disable it when pausing:

@Override protected void onPause() {
super.onPause();
if (Build.VERSION.SDK_INT >= 14) {
if (nfcAdapter != null) nfcAdapter.disableForegroundDispatch(this);
}
}

This will cause a sent NFC message to be delivered directly to your running activity via onNewIntent instead of having Android OS re-creating the activity. So let's override it:
@Override protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
if (Build.VERSION.SDK_INT >= 14) {
checkAndProcessBeamIntent(intent);
}
}

7. How can we have Android Beam mechanism start our activity (or open Google Play page) while still letting us react to NFC messages by doing something inside the activity? The answer is by adding an intent filter on our app manifest:

<activity android:name="yuku.alkitab.base.IsiActivity">
<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.NDEF_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/vnd.yuku.yourappname.nfc.beam" />
</intent-filter>
</activity>

Remember to use the same mime type as our method above that deals with parsing incoming NFC intent.

8. Our app, when started by Android OS from NFC intent, will have the intent's action set to android.nfc.action.NDEF_DISCOVERED instead of ...MAIN. So don't forget to call the checkAndProcessBeamIntent on onCreate of your app. 

Part 2 How to format the data to be sent over NFC

NFC messages are encapsulated in NdefMessage instances. Each of the NdefMessage contains one or more NdefRecord. For Android Beam to work, it actually sends an "application record" that contains the package name of your application, so that the receiving side knows how to proceed when receiving the message (either give the message to any component that has matching intent filter, open the app whose package name matches the application record, or open the app page on Google Play). This can be done very easily (assuming that we using the callback as the first part of this post describes):

nfcAdapter.setNdefPushMessageCallback(new CreateNdefMessageCallback() {
@Override public NdefMessage createNdefMessage(NfcEvent event) {
byte[] payload = ... // whatever data you want to send
NdefRecord record = new NdefRecord(NdefRecord.TNF_MIME_MEDIA, "application/vnd.yuku.yourappname.beam".getBytes(), new byte[0], payload);
NdefMessage msg = new NdefMessage(new NdefRecord[] { 
record, // your data
NdefRecord.createApplicationRecord(getPackageName()), // the "application record"
});
return msg;
}
}, this);

Actually there are many ways you can have messages formatted, but I think the easiest way is just be defining a custom mime type e.g. "application/vnd.yourcompanyname.yourappname.beam" (make sure it is also used in the intent filter on the app manifest and on the receiving code as mentioned on the first part) and use any array of bytes as the data to be transferred.

There are two records embedded in the NdefMessage. The first record is for your data. The second one is the "application record" described earlier. Android will try to process the first record before falling back to the second record, that's why this works.

That's all for the simple tutorial on Android Beam NFC. Good luck!

1 comment:

  1. Terima kasih sudah memberikan ilmunya secara gratis bang Yuku. Tuhan memberkati. Terus berkarya :))

    ReplyDelete