Five user experience tests for your Android application

Localytics user experience test for Android image

As the Android Market starts to pick up steam we are seeing lots of exciting applications come out of the woodwork. Unfortunately, the newness of the platform coupled with the desire to be first to market with a particular idea has also brought about a lot of application bugs which cause otherwise great programs to get some bad ratings in the Android market.

The following is a list of five things, which while certainly not all-inclusive, can be tested in five minutes and will help avoid some of these bugs and the negative feedback they bring.

  1. Try changing the orientation of your device on every activity
  2. Disable data connection and GPS
  3. Interrupt the app by calling the phone or using the home button
  4. Look for leaked connections to the net and GPS after app exits
  5. Test on a real device
  6. Bonus tip: Check your app’s name!

 

1) Try changing the orientation of your device on every activity

This is probably the most common bug we see in the Android market. Many applications will have strange results or even crash if you flip open the keyboard at the right time. This is caused by developers not properly accounting for what happens when their activity is destroyed and recreated mid-run.

Here is an example to illustrate this point. Below we have the code for a game. In it the user starts with 5 lives and every time the player clicks “Remove a life.” they lose a life. When they get below zero lives, the game is over. Not a very fun game, but it makes the point.

package com.Localytics.SimpleBugs;

//Sample of an activity which does not properly handle orientation changes

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.TextView;

public class SimpleBugsActivity extends Activity implements OnClickListener
{
	private Button btnLoseLife;
	private TextView txtLives;
	private int numLives = 5;

	@Override
	public void onCreate(Bundle savedInstanceState)
	{
		super.onCreate(savedInstanceState);
		setContentView(R.layout.main);

		this.txtLives = (TextView)findViewById(R.id.textLives);

		 this.btnLoseLife = new Button(this);
		 this.btnLoseLife.setText("Remove a life.");
		 this.btnLoseLife.setOnClickListener(this);

		 LinearLayout mainLayout = (LinearLayout)findViewById(R.id.mainLayout);
		 mainLayout.addView(this.btnLoseLife);

		 updateDisplay();
	}

	 @Override
	 public void onClick(View v)
	 {
		 if(v == this.btnLoseLife)
		 {
			 this.numLives--;
			 updateDisplay();
		 }
	 }

	 public void updateDisplay()
	 {
		 if(this.numLives < 0)
		 {
			 this.txtLives.setText("Player has no lives left.  Game over");
			 this.btnLoseLife.setEnabled(false);
		 }
		 else
		 {
			 this.txtLives.setText("Player has: " + numLives + " lives left");
		 }
	 }
}

Running the game in either orientation works perfectly. However, if you flip the device open or closed while the game is running the the activity is destroyed and recreated which initializes numLives back to 5. This means the player has a way of getting infinite lives.

It’s not within the scope of this article, but the fix is trivial enough that we can explain it here. Basically, the activity needs to save its state before it is destroyed and restore it after it is created. Conveniently, the methods onSaveInstanceState and onRestoreInstanceState are provided for this scenario. Adding the following code to the above activity fixes it:

	 private static final String KEY_LIVES = "lives";

	 public void onSaveInstanceState(Bundle savedInstanceState)
	 {
		 savedInstanceState.putInt(KEY_LIVES, this.numLives);
		 super.onSaveInstanceState(savedInstanceState);
	 }

	 public void onRestoreInstanceState(Bundle savedInstanceState)
	 {
		 super.onRestoreInstanceState(savedInstanceState);
		 this.numLives = savedInstanceState.getInt(KEY_LIVES);
	 	 updateDisplay();
	 }

It isn’t hard to imagine how having variables reset can cause irregular behavior in an application. This example is benign but plenty of applications hang, or leak threads as a result.

More information about this issue:

 

2) Disable data connection and GPS

 

Localytics user experience test for Android disable_gpsWhenever an application makes use of a resource its behavior should be tested when that resource is unavailable. GPS and data connectivity are the two areas where this bug appears most frequently. These bugs are bad because they usually keep the application from starting.

Another code example - here is an app which displays the user’s location:

package com.Localytics.SimpleBugs;

//Sample of an activity which does not properly handle lack of GPS

import android.app.Activity;
import android.content.Context;
import android.location.Criteria;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.widget.TextView;

public class SimpleBugsActivity extends Activity implements LocationListener
{
	private TextView txtOutput;
	private LocationManager locationManager;

	@Override
	public void onCreate(Bundle savedInstanceState)
	{
		super.onCreate(savedInstanceState);
		setContentView(R.layout.main);

		Criteria criteria = new Criteria();
		criteria.setAccuracy(Criteria.ACCURACY_FINE);
		this.txtOutput = (TextView)findViewById(R.id.textOutput);
		this.locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
		this.locationManager.requestLocationUpdates(
				this.locationManager.getBestProvider(criteria, true),
				0,     // Minimum time interval between updates
				0,     // minimum distance interval between updates
				this); // This class processes the results
	}

	@Override
	public void onLocationChanged(Location location)
	{
		this.txtOutput.setText("Latitude is: " + Double.toString(location.getLatitude()));
	}

	@Override
	public void onProviderDisabled(String arg0) { }

	@Override
	public void onProviderEnabled(String arg0) { }

	@Override
	public void onStatusChanged(String arg0, int arg1, Bundle arg2) { }
}

When fired up in the emulator this application works fine, but what happens when the user decides to disable their GPS because of privacy or battery concerns? If the GPS is disabled, the call to locationManager.getBestProvider returns a null String which causes requestLocationUpdates to throw an IllegalArgumentException and the application doesn’t even start.

This is most common with GPS, but it happens with data connectivity as well. It is amusing to see how many apps crash on startup when run on a G1 with no sim card.

 

3) Interrupt the app by calling the phone or using the home button

This is similar to the first test case listed above. This post on stuffthathappens.com shows the lifecycle impact of an incoming phone call. Much like a screen orientation change, this can happen at any moment and an application that is not aware of this behavior might react poorly to the sudden pause and restart.

.

Plusminus, a very active poster on anddev.org posted a video tutorial of how to simulate an incoming phone call with the emulator which could be used to test this behavior.

The home button is slightly more interesting. It is worth understanding activity launch modes. There are two places in the Android documentation where these are well documented:

  1. Launch Mode section in Application Fundamentals
  2. Activity Manifest Elements

By default, an Android activity is of the ’standard’ launch mode which means it can be instantiated multiple times. If this is the desired behavior for your application (for most applications it is) it is important that you test running the application multiple times. The activities still run within the same Task, so there is enough resource sharing that many applications exhibit weird behavior in these circumstances.

So test this in your emulator by pressing home and relaunching your application several times.

 

4) Look for leaked connections to the net and GPS after app exit

This bug is really aggravating because when an app does this, it is obvious to the user that some application is being bad but there is currently no way to isolate the culprit. Fortunately, the user is getting a better process view in Cupcake but even then it would not be good to be the app everybody has to kill.

Refer back to the threading article linked in the first testing point. In this example a thread is leaked. If that thread happened to be doing a network operation or using the GPS (which are things you would be likely to create a new thread to do) these resources will continue to get consumed after the activity terminates. In the case of GPS, the user will continue to see the GPS icon in the status bar.

Another variation of this comes with audio. A lot of the top rated games are susceptible to this. They play music in the background and pressing back at the right moment causes the game to exit but leaves the music playing. One game I’m particularly fond of takes this one step further by hanging when the app is relaunched after it leaks its sound thread.

So moral of the story: try exiting your application while it is consuming expensive resources.

 

5) Test on a real device!

This is probably the most repeated piece of testing advice around. And with good reason. Google’s Android SDK mentions it but there are still apps on the store which do not launch on a clean device or have other glaring problems. These are some of the reasons why it is a pretty bad idea to release an application without testing on a real device:

  • Actual users interact with the screen using their finger which has much worse resolution than the single pixel mouse cursor used with the emulator
  • Actual users are on spotty data connections with very high latency which causes delays in places you won’t notice with the emulator
  • Actual users have a lot of applications running in the background (hopefully not because of leaked threads). This will cause your app to work slower than in the emulator
  • Actual users are running on a battery which could get used up by an application
  • Actual devices don’t have real keyboards making UI that works in the emulator clunky and difficult to use in reality

It is incredibly easy to run your application on an actual device. It can be done right from Eclipse so there is no excuse for not finding a friend with a G1 and asking to borrow it for 30 minutes.

 

6) Bonus tip: Check your app’s name

Localytics user experience test for Android

After you launch your application, download it yourself to make sure everything looks right on the application download page, and that what you shipped actually worked. Then, once you have done this, go to your application settings and try removing your application. If your application is called “My Awesome App”, but you forgot to give it a proper name so it appears as ‘com.somecompany.app’ in the application list then it will only annoy a user who has already decided they want to delete it. People annoyed in this fashion tend to leave nasty feedback.

 

Conclusion

As stated before, this is not a list of everything to test for when releasing an application. Making sure the application does not consume too much memory, or contain logic errors is also very important. The reason these were singled out is they are glaringly obvious to any end user of an application and they very easy to catch so it would behoove any developer to cover these basics.

Search the blog

Henry is the Co-founder and CTO at Localytics. He loves cars and his dog, Thane.

STAY IN TOUCH