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!

Thursday, March 15, 2012

Putting simple transition effect to fragments

Nowadays it's recommended to use Fragments, even when developing non-tablet apps.

One of the first thing I notice when moving between fragments is that the user immediately see the new fragments without any transition animation. We want to add some kind of animation, unfortunately the FragmentTransaction documentation is very scarce.

After experiments, this works even when using v4 Support Library:

It happens that we need to consider several things:
  1. setTransition and setTransitionStyle is confusing and useless. Forget them.
  2. setCustomAnimations is the way to go. But you have to call it before you call add/remove/hide or similar operations.
  3. The resource ids passed to setCustomAnimations will be decoded using AnimationUtils.loadAnimation(). So you can pass anim resources (the old-style animations). I haven't tested whether the new animator resources work.
In order to "push" a fragment as if you were starting a new Activity using startActivity, call the following:


public static void moveForward(FragmentManager fm, Fragment current, Fragment newer, String backstackName, boolean allowStateLoss) {
 FragmentTransaction ft = fm.beginTransaction();
 ft.setCustomAnimations(android.R.anim.fade_in, 0, android.R.anim.fade_in, 0);
 ft.hide(current);
 ft.add(android.R.id.content, newer);
 ft.addToBackStack(backstackName);
 if (allowStateLoss) {
  ft.commitAllowingStateLoss();
 } else {
  ft.commit();
 }
}

For example, from the current Fragment, you want to transition to the AnotherFragment, call the method above as follows:

moveForward(getFragmentManager(), this, new AnotherFragment(), "another", false)

Bonus: If you call the above method after returning from other applications (for example inside onActivityResult), make sure to pass true to allow state loss, or your app will crash. It's not discussed here why =).

Tuesday, January 10, 2012

Convert RGB to HSL and vice-versa in Java

I wanted to add colored labels to my Android application and I wanted to make it such that the users need only to select one color, the background color, and the foreground color would be the same color, only lighter or darker.

I thought I needed a RGB to HSL (Hue, Saturation, Lightness) converter, but Android SDK only provides RGB to HSV value, which has some similarity but different.

Based on the above Wikipedia article, I wrote the Java code to convert between RGB and HSL. Hopefully this can be useful to your project.
  • rgb is an int (with alpha ignored) 0xrrggbb
  • hsl is an array of 3 floats:
    • [0] is hue, 0..360
    • [1] is saturation, 0..1
    • [2] is lightness, 0..1
public static void rgbToHsl(int rgb, float[] hsl) {
 float r = ((0x00ff0000 & rgb) >> 16) / 255.f;
 float g = ((0x0000ff00 & rgb) >> 8) / 255.f;
 float b = ((0x000000ff & rgb)) / 255.f;
 float max = Math.max(Math.max(r, g), b);
 float min = Math.min(Math.min(r, g), b);
 float c = max - min;
 
 float h_ = 0.f;
 if (c == 0) {
  h_ = 0;
 } else if (max == r) {
  h_ = (float)(g-b) / c;
  if (h_ < 0) h_ += 6.f;
 } else if (max == g) {
  h_ = (float)(b-r) / c + 2.f;
 } else if (max == b) {
  h_ = (float)(r-g) / c + 4.f;
 }
 float h = 60.f * h_;
 
 float l = (max + min) * 0.5f;
 
 float s;
 if (c == 0) {
  s = 0.f;
 } else {
  s = c / (1 - Math.abs(2.f * l - 1.f));
 }
 
 hsl[0] = h;
 hsl[1] = s;
 hsl[2] = l;
}

public static int hslToRgb(float[] hsl) {
 float h = hsl[0];
 float s = hsl[1];
 float l = hsl[2];
 
 float c = (1 - Math.abs(2.f * l - 1.f)) * s;
 float h_ = h / 60.f;
 float h_mod2 = h_;
 if (h_mod2 >= 4.f) h_mod2 -= 4.f;
 else if (h_mod2 >= 2.f) h_mod2 -= 2.f;
 
 float x = c * (1 - Math.abs(h_mod2 - 1));
 float r_, g_, b_;
 if (h_ < 1)      { r_ = c; g_ = x; b_ = 0; }
 else if (h_ < 2) { r_ = x; g_ = c; b_ = 0; }
 else if (h_ < 3) { r_ = 0; g_ = c; b_ = x; }
 else if (h_ < 4) { r_ = 0; g_ = x; b_ = c; }
 else if (h_ < 5) { r_ = x; g_ = 0; b_ = c; }
 else             { r_ = c; g_ = 0; b_ = x; }
 
 float m = l - (0.5f * c); 
 int r = (int)((r_ + m) * (255.f) + 0.5f);
 int g = (int)((g_ + m) * (255.f) + 0.5f);
 int b = (int)((b_ + m) * (255.f) + 0.5f);
 return r << 16 | g << 8 | b;
}

Friday, January 6, 2012

Add "screenSize" flag to android:configChanges when targetting API level 13 (Android 3.2) or newer

If you're setting "keyboardHidden|orientation" to android:configChanges in order to prevent activity from restarting itself when the device is rotated, you might be surprised when you find your activity restarts itself when running on Android 3.2 or newer.

The reason is, starting from Android 3.2 (API Level 13), rotated device will also trigger "screenSize" change. From attrs_manifest.xml:



        <!-- The current available screen size has changed.  If applications don't
             target at least {@link android.os.Build.VERSION_CODES#HONEYCOMB_MR2}
             then the activity will always handle this itself (the change
             will not result in a restart).  This represents a change in the
             currently available size, so will change when the user switches
             between landscape and portrait. -->
        <flag name="screenSize" value="0x0400" />


It means, if your android:targetSdkVersion is 13 or more, your application will receive "screenSize" config change. Therefore, to prevent your activity from restarting, add "screenSize". E.g.

    android:configChanges="keyboardHidden|orientation|screenSize"

Hope this helps you.