When it comes to keeping user interfaces “lively,” threads are our friends. But Threads are a real pain in the neck. Such is the life of a programmer.

It’s important that we design applications so that the user interface stays alive. Not only does this improve our user’s experience with our apps, but it keeps operating systems from stepping in and punishing us. Performing long operations on the UI thread is a no-no, and both iOS and Android will terminate an app if it violates this. Thus, we want to push such things off to a background thread and provide the user progress information on the UI thread.

Unfortunately, as soon as we have two threads, there are all kinds of “gotcha’s” we have to worry about. Chief among them is synchronization – we have to worry about what happens if both threads are trying to access the same information at the same time. You’ll find lots of references to this out on the Internet if you fire up your favorite search engine.

Fortunately, in an event-driven system such as Android, there are things that we can do to make life easier. For example, any operations that are performed solely on the UI thread can’t run into synchronization issues, because they are automatically serialized by the operating system – only one such operation is going on at any time. Thus is part of the secret behind what makes AsyncTasks so much easier to use than raw threads.

So here’s the problem we’ve set for ourself. Our wonderful little Android app has a built-in, read-only database worth of pre-configured information. Or perhaps it’s read-write once the app is running, that doesn’t really matter. What matters is that our application includes a file that is supposed to be the initial content of the database, and we need to get it set up when the application is first run. In iOS, a read-only database file can be loaded directly out of the application bundle. In Android, however, we have to copy the data out of our resources (or assets) area and into an appropriate “live” directory.

So:

  1. When the application starts, we need to check to see if the database is set up.
  2. If it isn’t, we need to copy the data to where we can access it.
  3. This may take a while, so:
    1. The copying should be done on a background thread.
    2. The screen should provide the user with progress information.

Simple, right? Now let’s talk about what might go wrong.

  1. We don’t control the user’s actions. There’s nothing to prevent the user from leaving our app (or answering a phone call) while we’re in the process of copying. Thus, our Activity might not always be in the foreground while the copying is going on.
  2. Because Android can kill a non-visible Activity whenever it likes, our Activity may not even exist (from Android’s point of view) when the copy completes.
  3. Worse, we can’t guarantee that the copying process will complete properly. If the user goes off into some other app, Android might kill our application before the copying is complete. While Android will typically try not to do that, it’s still possible. Or the user could power the phone down. Or the battery might die. Thus, we have to be prepared for the situation in which the copying is interrupted.
  4. Finally, it’s possible our copying process might fail because the device might run out of storage space.

So, how do we defend against all this potential badness? Here’s the plan of attack:

  1. We will implement a DatabaseSetupManager class that will handle the gory details. We will (effectively) make this a singleton by making it a member of our Application class. Our Application class instance will exist for as long as our app is alive in any way, so this is safe.
  2. We will use the Android AsyncTask to perform the copying on a background thread. As we will see, AsyncTask will perform some magic for us to push update and completion operations back onto the UI thread.
  3. We will connect the copy operation to the UI by using a “listener” interface. This will allow the UI to connect and disconnect from the background process as required by the Android Activity lifecycle.
  4. When we are doing the actual copy, we will initially copy to a temp file, and will only rename that file to the final “correct” filename once the copy operation is complete. As a result, if the final filename exists, we know that the copy succeeded. If the copy operation is somehow interrupted, we may have a partially-copied temp file to deal with, but we’ll be able to handle that situation.

Here’s the code for the DatabaseSetupManager:

public class DatabaseSetupManager
{
	public static enum State
	{
		UNKNOWN, IN_PROGRESS, READY
	}

	private final Context context;
	private final int resourceId;
	private final int size;
	private final String version;
	private State state;
	private DatabaseSetupTask setupTask;
	private Listener listener;

	public DatabaseSetupManager(Context context, int resourceId, int size, String version)
	{
		this.context = context;
		this.resourceId = resourceId;
		this.size = size;
		this.version = version;

		this.state = State.UNKNOWN;
	}

	public State getState()
	{
		if (state == State.UNKNOWN)
		{
			File path = getDatabaseFile();
			if (path.exists())
			{
				state = State.READY;
			}
			else
			{
				state = State.IN_PROGRESS;

				setupTask = new DatabaseSetupTask(	context,
													getDatabaseFile(),
													resourceId,
													size,
													this);
				setupTask.execute((Void[]) null);
			}
		}

		return state;
	}

	public File getDatabaseFile()
	{
		return context.getDatabasePath(version);
	}
	
	public void setListener(Listener listener)
	{
		this.listener = listener;
	}

	/* package */void progress(Integer completed, Integer total)
	{
		if (listener != null)
		{
			listener.progress(completed.intValue(), total.intValue());
		}
	}

	/* package */void setupComplete(Exception result)
	{
		if (listener != null)
		{
			if (result != null)
			{
				File path = getDatabaseFile();
				path.delete();
				state = State.UNKNOWN;
				listener.complete(false, result);
			}
			else
			{
				state = State.READY;
				listener.complete(true, null);
			}
		}
	}

	public interface Listener
	{
		public void progress(int completed, int total);
		public void complete(boolean success, Exception result);
	}
}

The State enum defined in lines 3-6 provides the rest of the application with an indication as to what is going on when getState (line 26) is called. UNKNOWN is used internally to tell the DatabaseSetupManager that this is the very first time it has been asked – it won’t be returned externally. getState checks to see if the target file already exists. If so, things are ready. Otherwise, it creates a DatabaseSetupTask (we’ll discuss that in a minute), and executes it, setting the state to IN_PROGRESS. The progress and setupComplete methods will be called by the DatabaseSetupTask as things progress and when setup is complete. These handle the interface back to the Listener class.

What is extremely important to understand is that all of the methods of this class will be called on the UI Thread. This means that we don’t have to worry about race conditions between calls to getState and setupComplete, both of which manipulate the state variable. If not for this bit of nice-ness, we’d have to worry about thread synchronization on this variable. The same applies to the listener variable – as long as it’s called from the UI Thread, all is simple.

Now let’s look at the DatabaseSetupTask:

public class DatabaseSetupTask extends AsyncTask<Void, Integer, Exception>
{
	public static final int DATABASE_COPY_BUFFER = 4096;

	private final Context context;
	private final File destination;
	private final int resourceName;
	private final Integer iterations;
	private final DatabaseSetupManager manager;

	public DatabaseSetupTask(	Context context,
								File destination,
								int resourceName,
								int expectedSize,
								DatabaseSetupManager manager)
	{
		this.context = context;
		this.destination = destination;
		this.resourceName = resourceName;
		this.iterations = Integer.valueOf((expectedSize + DATABASE_COPY_BUFFER - 1)
											/ DATABASE_COPY_BUFFER);
		this.manager = manager;
	}

	@Override
	protected Exception doInBackground(Void... params)
	{
		try
		{
			deleteFilesInDestinationDirectory();

			File tempFile = new File(destination.getAbsolutePath() + ".tmp");
			copyDatabaseToTempFile(tempFile);

			tempFile.renameTo(destination);
		}
		catch (Exception e)
		{
			deleteFilesInDestinationDirectory();
			return e;
		}

		return null;
	}

	private void deleteFilesInDestinationDirectory()
	{
		File directory = destination.getParentFile();
		if (directory.exists())
		{
			File[] files = directory.listFiles();
			for (File file : files)
			{
				file.delete();
			}
		}
	}

	private void copyDatabaseToTempFile(File tempFile) throws IOException
	{
		File dir = destination.getParentFile();
		if (!dir.exists())
		{
			dir.mkdirs();
		}

		InputStream input = null;
		FileOutputStream output = null;

		try
		{

			input = context.getResources().openRawResource(resourceName);
			output = new FileOutputStream(tempFile);
			byte[] buffer = new byte[DATABASE_COPY_BUFFER];

			int progress = 0;
			for (;;)
			{
				int nRead = input.read(buffer);
				if (nRead <= 0)
				{
					break;
				}

				output.write(buffer, 0, nRead);
				progress++;

				publishProgress(Integer.valueOf(progress));
			}
		}
		finally
		{
			safeClose(input);
			safeClose(output);
		}
	}

	@Override
	protected void onProgressUpdate(Integer... values)
	{
		manager.progress(values[0], iterations);
	}

	@Override
	protected void onPostExecute(Exception result)
	{
		manager.setupComplete(result);
	}

	private void safeClose(Closeable item)
	{
		try
		{
			if (item != null)
			{
				item.close();
			}
		}
		catch (Exception ignored)
		{
		}
	}
}

This class is derived from Android’s AsyncTask, which handles all the threading magic. The AsyncTask uses Java generics to handle the types of the input parameters, progress values and return values. In this case, we don’t have any input parameters, so we set that to Void. We will return an Exception (which will be null on success) and publish progress as an Integer.

When you call the execute method on the task, Android will arrange for a background thread to be created, and, from that thread, will call the doInBackground method, passing across the list of arguments you passed to execute. In this case, as you see, this method empties out the destination directory (to get rid of any partially-copied file that might be left over from a previous try), copies the database file to a temp file and then, when that’s done, renames the temp file to the destination name.

While your background task is working, you may make periodic calls to publishProgress. Android handles the required cross-thread magic, and arranges for onProgressUpdate to be called on the UI thread with whatever values you passed to publishProgress. As you can see, we simply turn around and call the progress method on the DatabaseSetupManager class which, in turn, passes the notification to any Listener that might be installed there.

Similarly, when your background task is complete, you simply return from doInBackground. Android will take the value you return, again perform some thread magic, and call the classes onPostExecute, again on the UI thread. Here, we again pass that to the DatabaseSetupManager, which passes it to the Listener.

Thus, by using the AsyncTask, all you have to do is to separate the portions of the task that should run in the background from the “progress” and “complete” notifications which should run on the UI thread, and Android handles the inter-thread communication. Behind the scenes, this is done by posting Runnables on the UI queue which will call the appropriate methods. These Runnables are interspersed with others that handle servicing events generated by the user and calling the appropriate Activity methods.

Typically, the way your app could use something like this might be through a "splash screen" which would be the first Activity in your app. It would obtain the DatabaseSetupManager singleton from the app's Application class and call getState. If getState returns READY, the app would immediately proceed to the next step - this would be typical of launches after the first one. On the first launch, of course, getState will return IN_PROGRESS. Your splash screen might react to this by:

  1. Installing a Listener
  2. Displaying a progress indication
  3. Updating the progress indication when the listener's progress method was called
  4. Moving on to the next step in the app when the listener's complete method was called

Let's suppose that, while you're waiting for setup to be complete, the user backs out of your app, so that your splash screen Activity gets torn down. Subsequently, the user comes back into your app, just as the background task is finishing. Let's look at what happens.

  1. If the UI thread has gotten to process the DatabaseSetupTask onPostExecute call before the splash screen calls getState, the DatabaseSetupManager will have set its internal state variable to READY, and the splash screen will proceed.
  2. If the splash screen manages to sneak in first, it will get an IN_PROGRESS return from getState. Thus, it will install its listener. As soon as the splash screen setup is done, however, the UI thread will now execute the Runnable that calls onProgressComplete, which means that the listener's complete method will get called, allowing the app to continue. (In this case, of course, you wouldn't get any intervening calls to the Listener's progress method.)

Because of the UI thread "serialization" of these events, there is no third possibility. Thus, as long as the listener is installed in the same block of code as the call to getState (i.e. the splash screen doesn't return back to the operating system between the two calls), there is no possibility of missing the completion of the background thread setup process, or having two of them running simultaneously.

Note that if the user navigates away from your splash screen, you do need to uninstall the listener so that the Activity instance can be properly cleaned up - you don't want a reference to it to be held via the DatabaseSetupManager singleton. Thus, one method is to test the state, and possibly install the listener, in your activity's onResume and to remove the listener by calling setListener(null) in onPause. The latter will remove the listener regardless of whether the user is leaving your application, or moving on to the next Activity in it.