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 AsyncTask
s 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:
- When the application starts, we need to check to see if the database is set up.
- If it isn’t, we need to copy the data to where we can access it.
- This may take a while, so:
- The copying should be done on a background thread.
- The screen should provide the user with progress information.
Simple, right? Now let’s talk about what might go wrong.
- 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. - Because Android can kill a non-visible
Activity
whenever it likes, ourActivity
may not even exist (from Android’s point of view) when the copy completes. - 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.
- 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:
- 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 ourApplication
class. OurApplication
class instance will exist for as long as our app is alive in any way, so this is safe. - 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. - 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. - 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 Runnable
s on the UI queue which will call the appropriate methods. These Runnable
s 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:
- Installing a
Listener
- Displaying a progress indication
- Updating the progress indication when the listener's
progress
method was called - 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.
- If the UI thread has gotten to process the
DatabaseSetupTask
onPostExecute
call before the splash screen callsgetState
, theDatabaseSetupManager
will have set its internalstate
variable toREADY
, and the splash screen will proceed. - If the splash screen manages to sneak in first, it will get an
IN_PROGRESS
return fromgetState
. Thus, it will install its listener. As soon as the splash screen setup is done, however, the UI thread will now execute theRunnable
that callsonProgressComplete
, which means that the listener'scomplete
method will get called, allowing the app to continue. (In this case, of course, you wouldn't get any intervening calls to theListener
'sprogress
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.