Applications of IntentService

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:

  • The upload usually must complete, even if the user leaves the application
  • A single upload at a time usually makes best use of the available connection, since bandwidth is often asymmetric (there is much smaller bandwidth for upload than download)
  • A single upload at a time gives us a better chance of completing each individual upload before losing our data connection

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.

HTTP uploads with IntentService

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.

Reporting progress

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.

Tip

Whichever approach we take to report progress, we should be careful not to report progress too frequently, otherwise we'll waste resources updating the progress bar at the expense of completing the work itself!

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.

Reporting progress
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
35.170.81.33