Wednesday, July 20, 2011

Android AsyncTask management

Warning: Coding ahead...

Releasing my own Android app has taught me an amazing amount about Java, Android and the state of commercial app libraries in general. I now have a deep and profound respect for what I considered "trivial" apps, just a few short months ago.

My first app, which still isn't in its final release form (though it's on the Android Market in beta form) had one pretty large problem: it loads images, and at times it would get stuck, forever downloading an image. The solution was to actively manage tasks, of course, but I'm new to Java, and I didn't know much about its task management facilities. I had assumed that it was fairly straightforward, and I would simply:

 task.cancel();

Alas, that's not it. as I quickly discovered, the AsyncTask class's cancel method is more of a hint. It tells the task that it should wrap up what it's doing, by setting a semaphore (or whatever the underlying mechanism is) and it's the job of the child task to check in with the isCanceled method and react accordingly.

This presented a great deal of complication for me. When the user presses the "back" button or clicks outside of the progress dialog to cancel a download, I don't want to leave a stranded download going, but the download code isn't mine, it's part of the BitmapFactory class. How can I tell it to stop what it's doing? The solution is fairly complicated, and I hope that I'll refine it or find there's a better way over time. Here's what I arrived at (after the break):



First off, BitmapFactory.decodeStream can take a third argument, which is a set of BitmapFactory.Options. While the decode is running, you can call BitmapFactory.Options.requestCacnelDecode from another thread, and the download will halt (presumably after the next block has been received).

So, I created a new class called BitmapManager, and gave it a BitmapFactory.Options as its only field. All of its methods are synchronized, which are: a getter and setter for the options and a cancel method. The cancel method calls the requestCancelDecode method on the options, if the options are non-null. This BitmapManager is instantiated when my downloader object is instantiated by the parent thread. When the parent invokes the download, creating a new thread, the BitmapManager is set to contain the associated options object. If the parent needs to cancel the download, it calls a cancel method on the downloader (remember, this is executing in the parent) and it calls the cancel method in the BitmapManager. Here's the manager's code:


static class BitmapManager {
private BitmapFactory.Options mBitmapOptions = null;

public synchronized void cancel() {
if (mBitmapOptions == null) {
return;
}
mBitmapOptions.requestCancelDecode();
}


public synchronized void setBitmapOptions(
BitmapFactory.Options bitmapOptions) {
mBitmapOptions = bitmapOptions;
}


public synchronized BitmapFactory.Options getBitmapOptions() {
return mBitmapOptions;
}
}

And here's how I use it inside the AsyncTask:

if (isCancelled()) {
return null;
}
BitmapFactory.Options bitmapOptions = new BitmapFactory.Options();
bitmapOptions.inSampleSize = sampleRatio;
mBitmapManager.setBitmapOptions(bitmapOptions);
Bitmap bitmap = BitmapFactory.decodeStream(in, null, bitmapOptions);
urlStream.close();
mBitmapManager.setBitmapOptions(null);
// What I wouldn't give for a C macro, here...
if (isCancelled()) {
return null;
}
return bitmap;

All I have to do is make sure that when the parent calls cancel on the child, it also calls my cleanup method:

public void cleanup() {
mBitmapManager.cancel();
}

At first, I thought I could override AsyncTask's onCancel method for this, but that method gets invoked in the UI thread, only after the task finishes executing, so it can't force the termination of the BitmapManager.

Now, when I cancel a download, the download actually stops, and a null bitmap is returned.

Overall, I think this is more complex than it needs to be. This is a simple and common need in Android app development. I'd be thrilled to learn that there's already something that does all of this, but if not, I think it's something the Android SDK should add.