Ideal applications for IntentService
include just about any long-running task where the work is not especially tied to the behavior of a Fragment
or Activity
, and particularly when the task must complete its processing regardless of whether the user exits the application.
However, IntentService
is only suitable for situations where a single worker thread is sufficient to handle the workload, since it's work is processed by a single HandlerThread
, and we cannot start more than one instance of the same IntentService
subclass.
A usecase that IntentService
is ideally suited for is uploading data to remote servers. An IntentService
is a perfect fit because:
Let's see how we might implement a very simple IntentService
that uploads images from the MediaStore to a simple web service via HTTP POST.
For this example, we'll create a new Activity, UploadPhotoActivity
, to allow the user to pick an image to upload. We'll start from the code for MediaStoreActivity
that we created in Chapter 4, Asynchronous I/O with Loader.
Our new UploadPhotoActivity
only needs a small modification to add an OnItemClickListener
interface to the GridView
of images, so that tapping an image triggers its upload. We can add the listener as an anonymous inner class in onCreate
as follows:
grid.setOnItemClickListener( new AdapterView.OnItemClickListener()) { public void onItemClick( AdapterView<?> parent, View view, int position, long id) { Cursor cursor = (Cursor)adapter.getItem(position); int mediaId = cursor.getInt( cursor.getColumnIndex(MediaStore.Images.Media._ID)); Uri uri = Uri.withAppendedPath( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, Integer.toString(mediaId)); Intent intent = new Intent( UploadPhotoActivity.this, UploadIntentService.class); intent.setData(uri); startService(intent); } });
This looks like quite a dense chunk of code, but all it really does is use the position of the tapped thumbnail to move the Cursor
to the correct row in its result set, extract the ID of the image that was tapped, create a Uri
for its original file, and then start UploadIntentService
with an Intent
containing that Uri
.
We'll extract the details of the upload into a separate class, so UploadIntentService
itself is just a fairly sparse IntentService
implementation. In onCreate
, we'll set up an instance of our ImageUploader
class, which will be used to process all uploads added to the queue during this lifetime of UploadIntentService
.
public void onCreate() { super.onCreate(); uploader = new ImageUploader(getContentResolver()); }
The onHandleIntent
method just does the coordination duties of notifying the user that an upload is in progress, using an ImageUploader
instance to perform the upload, then notifying the user of success or failure.
protected void onHandleIntent(Intent intent) { Uri data = intent.getData(); int id = Integer.parseInt(data.getLastPathSegment()); sendNotification(id, String.format("Uploading %s.jpg",id)); if (uploader.upload(data, null)) { notifyUser(id, String.format("Completed %s.jpg",id)); } else { notifyUser(id, String.format("Failed %s.jpg",id)); } }
The implementation of ImageUploader
itself is not all that interesting—we just use Java's HTTPURLConnection
class to post the image data to the server. The complete source code is available on the Packt Publishing website, so we'll just list two critical methods—upload
and pump
—and leave out the housekeeping.
public boolean upload(Uri data, ProgressCallack callback) { HttpURLConnection conn = null; try { int len = getContentLength(data); URL destination = new URL(UPLOAD_URL); conn = (HttpURLConnection) destination.openConnection(); conn.setDoInput(true); conn.setDoOutput(true); conn.setFixedLengthStreamingMode(len); conn.setRequestMethod("POST"); conn.setRequestProperty("Content-Type", "image/jpg"); conn.setRequestProperty("Content-Length", len + ""); conn.setRequestProperty( "Filename", data.getLastPathSegment() + ".jpg"); InputStream in = null; OutputStream out = null; try { pump( in = content.openInputStream(data), out = conn.getOutputStream(), callback, len); } finally { close(in); close(out); return ((conn.getResponseCode() >= 200) && (conn.getResponseCode() < 400)); } catch (IOException exc) { Log.e(TAG, "upload failed", exc); return false; } finally { conn.disconnect(); } }
The pump
method just copies 1 KB chunks of data from the InputStream
to the OutputStream
, pumping the data to the server, as follows:
private void pump( InputStream in, OutputStream out, ProgressCallback callback, int len) throws IOException { int length,i=0,size=1024; byte[] buffer = new byte[size]; // 1kb buffer while ((length = in.read(buffer)) > -1) { out.write(buffer, 0, length); out.flush(); if (callback != null) callback.onProgress(len, ++i*size); } }
Each time a 1 KB chunk of data is pushed to the OutputStream
, we invoke the ProgressCallback
method, which we'll use in the next section to report the progress to the user.
For long-running processes, it can be very useful to report progress so that the user can take comfort in knowing that something is actually happening.
To report progress from an IntentService
, we can use the same mechanisms that we use to send results—for example, sending PendingIntents
containing progress information, or posting system notifications with progress updates.
We can also use some techniques that we'll cover in the next chapter: sending messages via Messenger
, or broadcasting intents to registered receivers.
Let's look at an example that displays a progress bar on notifications in the drawer—a use case that the Android development team anticipated and therefore made easy for us with the setProgress
method of NotificationCompat.Builder
:
Builder setProgress(int max, int progress, boolean indeterminate);
Here, max
sets the target value at which our work will be completed, progress
is where we have got to so far, and indeterminate
controls which type of progress bar is shown. When indeterminate
is true
, the notification shows a progress bar that indicates something is in progress without specifying how far through the operation we are, while false
shows the kind of progress bar that we need—one that shows how much work we have done, and how much is left to do.
We'll need to calculate progress and dispatch notifications at appropriate intervals, which we've facilitated through our ProgressCallback
class. Now we need to implement the ProgressCallback
, and hook it up in UploadIntentService
:
private class ProgressNotificationCallback implements ImageUploader.ProgressCallback { private NotificationCompat.Builder builder; private NotificationManager nm; private int id, prev; public ProgressNotificationCallback( Context ctx, int id, String msg) { this.id = id; prev = 0; builder = new NotificationCompat.Builder(ctx) .setSmallIcon(android.R.drawable.stat_sys_upload_done) .setContentTitle(getString(R.string.upload_service)) .setContentText(msg) .setProgress(100,0,false); nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); nm.notify(id, builder.build()); } public void onProgress(int max, int progress) { int percent = (int)((100f*progress)/max); if (percent > (prev + 5)))))))) { builder.setProgress(100, percent, false); nm.notify(id, builder.build()); prev = percent; } } public void onComplete(String msg) { builder.setProgress(0, 0, false); builder.setContentText(msg); nm.notify(id, builder.build()); } }
The constructor of ProgressNotificationCallback
consists of the familiar code to post a notification with a progress bar.
The onProgress
method throttles the rate at which notifications are dispatched, so that we only post an update as each additional 5 percent of the total data is uploaded—in order not to swamp the system with notification updates.
The onComplete
method posts a notification that sets both the integer progress parameters to zero, which removes the progress bar.
The final change is to make onHandleIntent
use ProgressNotificationCallback
as follows:
protected void onHandleIntent(Intent intent) { Uri data = intent.getData(); // unique id per upload, so each has its own notification int id = Integer.parseInt(data.getLastPathSegment()); String msg = String.format("Uploading %s.jpg",id); ProgressNotificationCallback progress = new ProgressNotificationCallback(this, id, msg); if (uploader.upload(data, progress)) { progress.onComplete( String.format("Uploaded %s.jpg", id)); } else { progress.onComplete( String.format("Upload failed %s.jpg", id)); } }
If you download the sample code from the Packt Publishing website, you can try to upload an image from your phone to a simple web service running on Google App Engine at http://devnullupload.appspot.com/upload (don't worry, the web service just consumes the bytes and throws them away, so your images are safe!)
Tap an image to start uploading and you'll see a notification appear. Slide open the notification drawer and watch the progress bar ticking up in 5 percent increments as your image uploads.
35.170.81.33