Building responsive apps with IntentService

The IntentService class is a specialized subclass of Service that implements a background work queue using a single HandlerThread. When work is submitted to an IntentService, it is queued for processing by a HandlerThread, and processed in order of submission.

If the user exits the app before the queued work is completely processed, the IntentService will continue working in the background. When the IntentService has no more work in its queue, it will stop itself to avoid consuming unnecessary resources.

The system may still kill a background app with an active IntentService, if it really needs to (to reclaim enough memory to run the current foreground process), but it will kill lower priority processes first, for example, other non-foreground apps that do not have active services.

The IntentService class gets its name from the way in which we submit work to it: by invoking startService with an Intent.

startService(new Intent(context, MyIntentService.class));

We can call startService as often as we like, which will start the IntentService if it isn't already running, or simply enqueue work to an already running instance if there is one.

If we want to pass some data to a Service, we can do so by supplying a data Uri or extra data fields via an Intent.

Intent intent = new Intent(context, MyIntentService.class);
intent.setData(uri);
intent.putExtra("param", "some value");
startService(intent);

We can create an IntentService subclass by extending android.app.IntentService and implementing the abstract onHandleIntent method. We must invoke the single-argument constructor with a name for its background thread (naming the thread makes debugging and profiling much easier).

public class MyIntentService extends IntentService {
  public MyIntentService() {
    super("thread-name");
  }
  protected void onHandleIntent(Intent intent) {
    // executes on the background HandlerThread.
  }
}

We'll need to register the IntentService in our AndroidManifest file, using a <service> element as follows:

<service android:name="com.mypackage.MyIntentService"/>

If we want our IntentService to only be used by the components of our own application, we can specify that it is not public with an extra attribute:

<service android:name=".MyIntentService"
  android:exported="false"/>

Let's get started by implementing IntentService to do something we're already familiar with—calculating the nth prime:

public class PrimesIntentService extends IntentService {
  public static final String PARAM = "prime_to_find";
    
  public PrimesIntentService() {
    super("primes");
  }
    
  protected void onHandleIntent(Intent intent) {
    int n = intent.getIntExtra(PARAM, -1);
    BigInteger prime = calculateNthPrime(n);
  }
    
  private BigInteger calculateNthPrime(int n) {
    BigInteger prime = BigInteger.valueOf(2);
    for (int i=0; i<n; i++)
      prime = prime.nextProbablePrime();        
    return prime;
  }
}

Notice that we're declaring a public static constant name for the parameter, just to make it easy to use the correct name from any client Activity that wants to invoke the Service. We can invoke this IntentService to calculate the nth prime as follows:

private void triggerIntentService(int primeToFind) {
  Intent intent = new Intent(this, PrimesIntentService.class);
  intent.putExtra(PrimesIntentService.PARAM, primeToFind);
  startService(intent);
}

So far so good, but you've probably noticed that we haven't done anything with the result we calculated. In the next section, we'll look at some of the ways in which we can send results from services to activities or fragments.

Handling results

Any Service—including subclasses of IntentService—can be used to start background work from which the originating Fragment or Activity doesn't expect a response.

However, it is very common to need to return a result or display the result of the background work to the user. We have several options for doing this:

  • Send a PendingIntent to the Service from the originating Activity, allowing the Service to callback to the Activity via its onActivityResult method
  • Post a system notification allowing the user to be informed that the background work was completed, even if the application is no longer in the foreground
  • Send a Message to a Handler in the originating Activity using Messenger
  • Broadcast the result as an Intent, allowing any Fragment or Activity—including the originator—to receive the result of background processing

We'll learn about Messenger and BroadcastReceiver in Chapter 6, Long-running Tasks with Service. In this chapter, we'll return results with PendingIntent, and alert the user with system notifications.

Returning results with PendingIntent

When we invoke an IntentService, it does not automatically have any way to respond to the calling Activity; so if the Activity wants to receive a result, it must provide some means for the IntentService to reply.

Arguably, the easiest way to do that is with PendingIntent, which will be familiar to any Android developer who has worked with multiple activities using the startActivityForResult and onActivityResult methods, as the pattern is essentially the same.

First, we'll add a few static members to PrimesIntentService to ensure that we use consistent values between it and the calling Activity:

public static final String PENDING_RESULT = "pending_result";
public static final String RESULT = "result";
public static final int RESULT_CODE = "nth_prime".hashCode();

We'll also need to define a static member in our Activity for the REQUEST_CODE constant, which we can use to correctly identify the results returned to our onActivityResult method:

private static final int REQUEST_CODE = 0;

Now, when we want to invoke PrimesIntentService from our Activity, we'll create a PendingIntent for the current Activity, which will act as a callback to invoke its onActivityResult method.

We can create a PendingIntent with the createPendingResult method of Activity, which accepts three parameters: an int result code, an Intent to use as the default result, and an int that encodes some configuration for how the PendingIntent can be used (for example, whether it may be used more than once).

PendingIntent pending = createPendingResult(
  REQUEST_CODE, new Intent(),(), 0);

We pass the PendingIntent to the IntentService by adding it as an extra in the Intent we launch the IntentService with:

private void triggerIntentService(int primeToFind) {
  PendingIntent pending = createPendingResult(
    REQUEST_CODE, new Intent(), 0);
  Intent intent = new Intent(this, PrimesIntentService.class);
  intent.putExtra(PrimesIntentService.PARAM, primeToFind);
  intent.putExtra(
    PrimesIntentService.PENDING_RESULT, pending);
  startService(intent);
}

To handle the result that will be returned when this PendingIntent is invoked, we need to implement onActivityResult in the Activity, and check for the result code:

protected void onActivityResult(int req, int res, Intent data) {
  if (req == REQUEST_CODE &&
      res == PrimesIntentService.RESULT_CODE) {
      BigInteger result = (BigInteger) 
        data.getSerializableExtra(PrimesIntentService.RESULT);
      // … update UI with the result        
  }
  super.onActivityResult(requestCode, resultCode, data);
}

The IntentService can now reply to the calling Activity by invoking one of the PendingIntent's send methods with the appropriate request code. Our updated onHandleIntent method looks as follows:

protected void onHandleIntent(Intent intent) {
  int n = intent.getIntExtra(PARAM, -1);
  BigInteger prime = calculateNthPrime(n);
  try {
    Intent result = new Intent();
    result.putExtra(RESULT, prime);
    PendingIntent reply =    
      intent.getParcelableExtra(PENDING_RESULT);
      reply.send(this, RESULT_CODE, result);
  } catch (PendingIntent.CanceledException exc) {
    Log.i(TAG, "reply cancelled", exc);
  }
}

The additional code creates a new Intent object and populates it with our calculated prime, then sends it back to the calling Activity using PendingIntent. We also have to handle the CanceledException, in case the calling Activity decided that it wasn't interested in the result any more and cancelled the PendingIntent. That's all there is to it—our Activity will now be invoked via its onActivityResult method when the IntentService completes its work.

As a bonus, we will still receive the result if the Activity has restarted, for example, due to configuration changes such as a device rotation.

What if the user left the Activity (or even left the application) while the background work was in progress? In the next section, we'll use notifications to provide feedback without interrupting the user's new context.

Posting results as system notifications

System notifications appear initially as an icon in the notification area, normally at the very top of the device screen. Once notified, the user can open the notification drawer to see more details.

Notifications are an ideal way to inform the user of results or status updates from services, particularly when the operation may take a long time to complete and the user is likely to be doing something else in the meantime.

Let's post the result of our prime number calculation as a notification, with a message containing the result that the user can read when they open the notification drawer. We'll use the support library to ensure broad API level compatibility, and add one method to PrimesIntentService as follows:

private void notifyUser(int primeToFind, String result) {
  String msg = String.format(
    "The %sth prime is %s", primeToFind, result);
  NotificationCompat.Builder builder =
    new NotificationCompat.Builder(this)
      .setSmallIcon(R.drawable.prime_notification_icon)
      .setContentTitle(getString(R.string.primes_app))
      .setContentText(msg);
  NotificationManager nm = (NotificationManager)
    getSystemService(Context.NOTIFICATION_SERVICE);
  nm.notify(primeToFind, builder.build());
}

Here we're using NotificationCompat.Builder to build a notification that includes an icon, a title (just the name of our application), and a message containing the result of the calculation.

Each notification has an identifier, which we can use to control whether a new notification is posted or an existing one is reused. The identifier is an int, and is the first parameter to the notify method. Since our primeToFind value is an int, and we would like to be able to post multiple notifications, it makes sense to use primeToFind as the ID for our notifications so that each different request can produce its own separate notification.

To post a notification containing the result of our calculation, we just need to update onHandleIntent to invoke the notifyUser method:

protected void onHandleIntent(Intent intent) {
  int n = intent.getIntExtra(PARAM, -1);
      BigInteger prime = calculateNthPrime(n); 
  notifyUser(n, prime.toString());
}

Now that we've learned the basics of using IntentService, let's consider some real-world applications.

..................Content has been hidden....................

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