- The Main Thread
- Getting Off the Main Thread
- The AsyncTask
- The IntentService
- Wrapping Up
The IntentService
The IntentService is an excellent way to move large amounts of data around without relying on any specific activity or even application. The AsyncTask will always take over the main thread at least twice (with its pre- and post-execute methods), and it must be owned by an activity that is able to draw to the screen. The IntentService has no such restriction. To demonstrate, I’ll show you how to download the same image, this time from the IntentService rather than the AsyncTask.
Declaring a Service
Services are, essentially, classes that run in the background with no access to the screen. In order for the system to find your service when required, you’ll need to declare it in your manifest, like so:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.haseman.Example"
android:versionCode="1"
android:versionName="1.0">
<application
android:name="MyApplication"
android:icon="@drawable/icon"
android:label="@string/app_name">
<!—Rest of the application declarations go here -->
<service android:name=".ImageIntentService"/>
</application>
</manifest>
At a minimum, you’ll need to have this simple declaration. It will then allow you to (as I showed you earlier with activities) explicitly launch your service. Here’s the code to do exactly that:
Intent i = new Intent(this, ImageIntentService.class); i.putExtra("url", getIntent().getExtras().getString("url")); startService(i);
At this point, the system will construct a new instance of your service, call its onCreate method, and then start firing data at the IntentService’s handleIntent method. The intent service is specifically constructed to handle large amounts of work and processing off the main thread. The service’s onCreate method will be called on the main thread, but subsequent calls to handleIntent are guaranteed by Android to be on a background thread (and this is where you should put your long-running code in any case).
Right, enough gabbing. Let me introduce you to the ImageIntentService. The first thing you’ll need to pay attention to is the constructor:
public class ImageIntentService extends IntentService{ public ImageIntentService() { super("ImageIntentService"); }
Notice that the constructor you must declare has no string as a parameter. The parent’s constructor that you must call, however, must be passed a string. Eclipse will make it seem that you must declare a constructor with a string when, in reality, you must declare it without one. This simple mistake can cause you several hours of intense face-to-desk debugging.
Once your service exists, and before anything else runs, the system will call your onCreate method. onCreate is an excellent time to run any housekeeping chores you’ll need for the rest of the service’s tasks (more on this when I show you the image downloader).
At last, the service can get down to doing some heavy lifting. Once it has been constructed and has had its onCreate method called, it will then receive a call to handleIntent for each time any other activity has called startService.
Fetching Images
The main difference between fetching images and fetching smaller, manageable data is that larger data sets (such as images or larger data retrievals) should not be bundled into a final broadcast intent (another major difference to the AsyncTask). Also, keep in mind that the service has no direct access to any activity, so it cannot ever access the screen on its own. Instead of modifying the screen, the IntentService will send a broadcast intent alerting all listeners that the image download is complete. Further, since the service cannot pass the actual image data along with that intent, you’ll need to save the image to the SD card and include the path to that file in the final completion broadcast.
The Setup
Before you can use the external storage to cache the data, you’ll need to create a cache folder for your application. A good place to check is when the IntentService’s onCreate method is called:
public void onCreate(){ super.onCreate(); String tmpLocation = Environment.getExternalStorageDirectory().getPath() + CACHE_FOLDER; cacheDir = new File(tmpLocation); if(!cacheDir.exists()){ cacheDir.mkdirs(); } }
Using Android’s environment, you can determine the correct prefix for the external file system. Once you know the path to the eventual cache folder, you can then make sure the directory is in place. Yes, I know I told you to avoid file-system contact while on the main thread (and onCreate is called on the main thread), but checking and creating a directory is a small enough task that it should be all right. I’ll leave this as an open question for you as you read through the rest of this chapter: Where might be a better place to put this code?
The Fetch
Now that you’ve got a place to save images as you download them, it’s time to implement the image fetcher. Here’s the handleIntent method:
protected void onHandleIntent(Intent intent) { String remoteUrl = intent.getExtras().getString("url"); String location; String filename = remoteUrl.substring( remoteUrl.lastIndexOf(File.separator)+1); File tmp = new File(cacheDir.getPath() + File.separator +filename); if(tmp.exists()){ location = tmp.getAbsolutePath(); notifyFinished(location, remoteUrl); stopSelf(); return; } try{ URL url = new URL(remoteUrl); HttpURLConnection httpCon = (HttpURLConnection)url.openConnection(); if(httpCon.getResponseCode() != 200) throw new Exception("Failed to connect"); InputStream is = httpCon.getInputStream(); FileOutputStream fos = new FileOutputStream(tmp); writeStream(is, fos); fos.flush(); fos.close(); is.close(); location = tmp.getAbsolutePath(); notifyFinished(location, remoteUrl); }catch(Exception e){ Log.e("Service","Failed!",e); } }
This is a lot of code. Fortunately, most of it is stuff you’ve seen before.
First, you retrieve the URL to be downloaded from the Extras bundle on the intent. Next, you determine a cache file name by taking the last part of the URL. Once you know what the file will eventually be called, you can check to see if it’s already in the cache. If it is, you’re finished, and you can notify the system that the image is available to load into the UI.
If the file isn’t cached, you’ll need to download it. By now you’ve seen the HttpUrlConnection code used to download an image at least once, so I won’t bore you by covering it. Also, if you’ve written any Java code before, you probably know how to write an input stream to disk.
The Cleanup
At this point, you’ve created the cache file, retrieved it from the web, and written it to the aforementioned cache file. It’s time to notify anyone who might be listening that the image is available. Here’s the contents of the notifyFinished method that will tell the system both that the image is finished and where to get it.
public static final String TRANSACTION_DONE = "com.haseman.TRANSACTION_DONE"; private void notifyFinished(String location, String remoteUrl){ Intent i = new Intent(TRANSACTION_DONE); i.putExtra("location", location); i.putExtra("url", remoteUrl); ImageIntentService.this.sendBroadcast(i); }
Anyone listening for the broadcast intent com.haseman.TRANSACTION_DONE will be notified that an image download has finished. They will be able to pull both the URL (so they can tell if it was an image it actually requested) and the location of the cached file.
Rendering the Download
In order to interact with the downloading service, there are two steps you’ll need to take. You’ll need to start the service (with the URL you want it to fetch). Before it starts, however, you’ll need to register a listener for the result broadcast. You can see these two steps in the following code:
public void onCreate(Bundle extras){ super.onCreate(extras); setContentView(R.layout.image_layout); IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(ImageIntentService.TRANSACTION_DONE); registerReceiver(imageReceiver, intentFilter); Intent i = new Intent(this, ImageIntentService.class); i.putExtra("url", getIntent().getExtras().getString("url")); startService(i); pd = ProgressDialog.show(this, "Fetching Image", "Go intent service go!"); }
This code registered a receiver (so you can take action once the download is finished), started the service, and, finally, showed a loading dialog to the user.
Now take a look at what the imageReceiver class looks like:
private BroadcastReceiver imageReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String location = intent.getExtras().getString("location"); if(location == null || location.length() ==0){ Toast.makeText(context, "Failed to download image", Toast.LENGTH_LONG).show(); } File imageFile = new File(location); if(!imageFile.exists()){ pd.dismiss(); Toast.makeText(context, "Unable to Download file :-(", Toast.LENGTH_LONG); return; } Bitmap b = BitmapFactory.decodeFile(location); ImageView iv = (ImageView)findViewById(R.id.remote_image); iv.setImageBitmap(b); pd.dismiss(); } };
This is a custom extension of the BroadcastReceiver class. This is what you’ll need to declare inside your activity in order to correctly process events from the IntentService. Right now, there are two problems with this code. See if you can recognize them.
First, you’ll need to extract the file location from the intent. You do this by looking for the “location” extra. Once you’ve verified that this is indeed a valid file, you’ll pass it over to the BitmapFactory, which will create the image for you. This bitmap can then be passed off to the ImageView for rendering.
Now, to the things done wrong (stop reading if you haven’t found them yet. No cheating!). First, the code is not checking to see if the intent service is broadcasting a completion intent for exactly the image originally asked for (keep in mind that one service can service requests from any number of activities).
Second, the bitmap is loading from the SD card...on the main thread! Exactly one of the things I’ve been warning you NOT to do.
Checking Your Work
Android, in later versions of the SDK tools, has provided a way to check if your application is breaking the rules and running slow tasks on the main thread. You can, in any activity, call StrictMode.enableDefaults, and this will begin to throw warnings when the system spots main thread violations. StrictMode has many different configurations and settings, but enabling the defaults and cleaning up as many errors as you can will work wonders for the speed of your application.