Services and the Command Pattern

As noted previously, Android services are for long-running processes that may need to keep running even when decoupled from any activity. Examples include playing music even if the “player” activity is destroyed, polling the Internet for RSS/Atom feed updates, and maintaining an online chat connection even if the chat client loses focus due to an incoming phone call.

Services are created when manually started (via an API call) or when some activity tries connecting to the service via inter-process communication (IPC). Services will live until specifically shut down, until Android is desperate for RAM and terminates the process, or for a short period of time on Android 8.0+. Running for a long time has its costs, though, so services need to be careful not to use too much CPU or keep radios active too much of the time, lest the service cause the device’s battery to get used up too quickly.

This chapter outlines the basic theory behind creating and consuming services, including a look at the “command pattern” for services.

Why Services?

Services are a “Swiss Army knife” for a wide range of functions that do not require direct access to an activity’s user interface, such as:

  1. Performing operations that need to continue even if the user leaves the application’s activities, like a long download (as seen with the Play Store) or playing music (as seen with Android music apps)
  2. Performing operations that need to exist regardless of activities coming and going, such as maintaining a chat connection in support of a chat application
  3. Providing a local API to remote APIs, such as might be provided by a Web service
  4. Performing periodic work without user intervention, akin to cron jobs or Windows scheduled tasks

Even things like home screen app widgets often involve a service to assist with long-running work.

The primary role of a service is as a flag to the operating system, letting it know that your process is still doing work, despite the fact that it is in the background. This makes it somewhat less likely that Android will terminate your process due to low memory conditions.

Many applications will not need any services. Very few applications will need more than one. However, the service is a powerful tool for an Android developer’s toolbox and is a subject with which any qualified Android developer should be familiar.

Setting Up a Service

Creating a service implementation shares many characteristics with building an activity. You inherit from an Android-supplied base class, override some lifecycle methods, and hook the service into the system via the manifest.

The Service Class

There are many service classes that you might inherit from.

The root of all of them is Service, just as the root of the hierarchy of activity classes is Activity. You may wish to subclass Service, particularly if:

Historically, the next-most-common base class was IntentService. This was good for a “transactional” bit of work, where you need to have a service do something in the background for a bit, then you no longer need the service. The classic example here is a service to download a large file: once the file is downloaded, you no longer need that service to be around. IntentService supplies you with a background thread to use for the network I/O and disk I/O, and it will shut down automatically once the work is complete.

However, IntentService has recently been supplanted by JobIntentService. This has similar characteristics, but it works better on Android 8.0+ devices, due to limitations on what you can do in the background.

We will examine all three of these solutions in this chapter.

Beyond those, there are many specialized service classes. Elsewhere in the book you will find examples of:

And there are plenty of others. While they are all services, and they will all be added to the manifest in the same fashion (more or less), the API that you implement will be very specific to the service that you are extending. The API for TileService looks little like the API for JobService, for example. As such, those services, where we use them, are covered elsewhere in the book.

Lifecycle Methods

Just as activities have onCreate(), onResume(), onPause() and kin, Service implementations have their own lifecycle methods, such as:

As with activities, services initialize whatever they need in onCreate() and clean up those items in onDestroy(). And, as with activities, the onDestroy() method of a service might not be called, if Android terminates the entire application process, such as for emergency RAM reclamation.

The onStartCommand() and onBind() lifecycle methods will be implemented based on your choice of communicating to the client, as will be explained later in this chapter.

Note that Service is an abstract class and onBind() is an abstract method, so even if you are not using bindService(), you will need to implement onBind() in order to successfully compile. A common approach here is to have onBind() simply return null.

Note that these methods may be optional, depending upon your needs and the base class that you extend. So, for example, overriding any of these in a JobIntentService is unusual at best and a bad idea at worst.

Manifest Entry

Finally, you need to add the service to your AndroidManifest.xml file, for it to be recognized as an available service for use. That is simply a matter of adding a <service> element as a child of the application element, providing android:name to reference your service class.

Since the service class is in the same Java namespace as everything else in this application, we can use the shorthand (e.g., "PlayerService") to reference our class.

For example, here is a manifest showing the <service> element:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  package="com.commonsware.android.fakeplayer"
  android:versionCode="1"
  android:versionName="1.0">

  <supports-screens
    android:anyDensity="true"
    android:largeScreens="true"
    android:normalScreens="true"
    android:smallScreens="true"/>

  <uses-sdk
    android:minSdkVersion="14"
    android:targetSdkVersion="14"/>

  <application
    android:icon="@drawable/ic_launcher"
    android:label="@string/app_name"
    android:theme="@android:style/Theme.Holo.Light.DarkActionBar">
    <activity
      android:name="FakePlayer"
      android:label="@string/app_name">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>

        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
    </activity>

    <service android:name="PlayerService"/>
  </application>

</manifest>
(from Service/FakePlayer/app/src/main/AndroidManifest.xml)

Communicating To Services

Clients of services — frequently activities, though not necessarily — have two main ways to send requests or information to a service. One approach is to send a command, which creates no lasting connection to the service. The other approach is to bind to the service, establishing a communications channel that lasts as long as the client needs it.

Sending Commands with startService()

The simplest way to work with a service is to call startService(). The startService() method takes an Intent parameter, much like startActivity() does. In fact, the Intent supplied to startService() has the same two-part role as it does with startActivity():

  1. Identify the service to communicate with
  2. Supply parameters, in the form of Intent extras, to tell the service what it is supposed to do

For a local service — the focus of this chapter — the simplest form of Intent is one that identifies the class that implements the Service (e.g., new Intent(this, MyService.class);).

The call to startService() is asynchronous, so the client will not block. The service will be created if it is not already running, and it will receive the Intent via a call to the onStartCommand() lifecycle method. The service can do whatever it needs to in onStartCommand(), but since onStartCommand() is called on the main application thread, it should do its work very quickly. Anything that might take more than a handful of milliseconds should be delegated to a background thread.

The onStartCommand() method can return one of several values, mostly to indicate to Android what should happen if the service’s process should be killed while it is running. The most likely return values are:

  1. START_STICKY, meaning that the service should be moved back into the started state (as if onStartCommand() had been called), but do not re-deliver the Intent to onStartCommand()
  2. START_REDELIVER_INTENT, meaning that the service should be restarted via a call to onStartCommand(), supplying the same Intent as was delivered this time
  3. START_NOT_STICKY, meaning that the service should remain stopped until explicitly started by application code

By default, calling startService() not only sends the command, but tells Android to keep the service running until something tells it to stop. One way to stop a service is to call stopService(), supplying the same Intent used with startService(), or at least one that is equivalent (e.g., identifies the same class). At that point, the service will stop and will be destroyed. Note that stopService() does not employ any sort of reference counting, so three calls to startService() will result in a single service running, which will be stopped by a call to stopService().

Another possibility for stopping a service is to have the service call stopSelf() on itself. You might do this if you use startService() to have a service begin running and doing some work on a background thread, then having the service stop itself when that background work is completed.

Note that JobIntentService has an API that feels like the command pattern, though it uses a different set of methods, as will be explored later in the chapter.

Binding to Services

Another approach to communicating with a service is to use the binding pattern. Here, instead of packaging commands to be sent via an Intent, you can obtain an actual API from the service, with whatever data types, return values, and so on that you wish. You then invoke that API no different than you would on some local object.

The benefit is the richer API. The cost is that binding is more complex to set up and more complex to maintain, particularly across configuration changes.

We will discuss the binding pattern later in this book.

Scenario: The Music Player

Most audio player applications in Android — for music, audiobooks, or whatever — do not require the user to remain in the player application itself. Rather, the user can go on and do other things with their device, with the audio playing in the background.

The sample project reviewed in this section is Service/FakePlayer.

The Design

We will use startService(), since we want the service to run even when the activity starting it has been destroyed. However, we will use a regular Service, rather than an IntentService or JobIntentService. Those are designed to do work and stop itself, whereas in this case, we want the user to be able to stop the music playback when the user wants to.

Since music playback is outside the scope of this chapter, the service will simply stub out those particular operations.

The Service Implementation

Here is the implementation of this Service, named PlayerService:

package com.commonsware.android.fakeplayer;

import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;

public class PlayerService extends Service {
  public static final String EXTRA_PLAYLIST="EXTRA_PLAYLIST";
  public static final String EXTRA_SHUFFLE="EXTRA_SHUFFLE";
  private boolean isPlaying=false;
  
  @Override
  public int onStartCommand(Intent intent, int flags, int startId) {
    String playlist=intent.getStringExtra(EXTRA_PLAYLIST);
    boolean useShuffle=intent.getBooleanExtra(EXTRA_SHUFFLE, false);

    play(playlist, useShuffle);     
    
    return(START_NOT_STICKY);
  }
  
  @Override
  public void onDestroy() {
    stop();
  }
  
  @Override
  public IBinder onBind(Intent intent) {
    return(null);
  }
  
  private void play(String playlist, boolean useShuffle) {
    if (!isPlaying) {
      Log.w(getClass().getName(), "Got to play()!");
      isPlaying=true;
    }
  }
  
  private void stop() {
    if (isPlaying) {
      Log.w(getClass().getName(), "Got to stop()!");
      isPlaying=false;
    }
  }
}
(from Service/FakePlayer/app/src/main/java/com/commonsware/android/fakeplayer/PlayerService.java)

In this case, we really do not need anything for onCreate(), so that lifecycle method is skipped. On the other hand, we have to implement onBind(), because that is an abstract method on Service.

When the client calls startService(), onStartCommand() is called in PlayerService. Here, we get the Intent and pick out some extras to tell us what to play back (EXTRA_PLAYLIST) and other configuration details (e.g., EXTRA_SHUFFLE). onStartCommand() calls play(), which simply flags that we are playing and logs a message to Logcat — a real music player would use MediaPlayer to start playing the first song in the playlist. onStartCommand() returns START_NOT_STICKY, indicating that if Android terminates the process (e.g., low memory), it should not restart it once conditions improve.

onDestroy() stops the music from playing — theoretically, anyway — by calling a stop() method. Once again, this just logs a message to Logcat, plus updates our internal are-we-playing flag.

Using the Service

The PlayerFragment demonstrating the use of PlayerService has a very elaborate UI, consisting of two large buttons:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:orientation="vertical">

  <Button
    android:id="@+id/start"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_weight="1"
    android:text="@string/start_the_player"/>

  <Button
    android:id="@+id/stop"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_weight="1"
    android:text="@string/stop_the_player"/>

</LinearLayout>
(from Service/FakePlayer/app/src/main/res/layout/main.xml)

The fragment itself is not much more complex:

package com.commonsware.android.fakeplayer;

import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

public class PlayerFragment extends Fragment implements
    View.OnClickListener {
  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup parent,
                           Bundle savedInstanceState) {
    View result=inflater.inflate(R.layout.main, parent, false);

    result.findViewById(R.id.start).setOnClickListener(this);
    result.findViewById(R.id.stop).setOnClickListener(this);

    return(result);
  }

  @Override
  public void onClick(View v) {
    Intent i=new Intent(getActivity(), PlayerService.class);

    if (v.getId() == R.id.start) {
      i.putExtra(PlayerService.EXTRA_PLAYLIST, "main");
      i.putExtra(PlayerService.EXTRA_SHUFFLE, true);

      getActivity().startService(i);
    }
    else {
      getActivity().stopService(i);
    }
  }
}
(from Service/FakePlayer/app/src/main/java/com/commonsware/android/fakeplayer/PlayerFragment.java)

The onCreateView() method merely loads the UI. The onClick() method constructs an Intent with fake values for EXTRA_PLAYLIST and EXTRA_SHUFFLE, then calls startService(). After you press the “Start” button, you will see the corresponding message in Logcat. Similarly, stopPlayer() calls stopService(), triggering the second Logcat message. Notably, you do not need to keep the activity running in between those button clicks — you can exit the activity via BACK and come back later to stop the service.

The Power of the PendingIntent

Right now, we are focused on services that work within a single process. However, later, we will examine services that run in separate processes, possibly even in separate apps. Communicating with such services becomes more of a challenge, as we cannot just pass around arbitrary objects. One thing that we can pass around is anything implementing the Parcelable interface.

One of the key Parcelable objects for inter-process communication is a PendingIntent. And while PendingIntent is designed for use between processes, it can also be used within your own app, if desired.

What Is a PendingIntent?

Principally, a PendingIntent is a wrapper around an Intent, identifying a specific action to be performed on that Intent:

How Do We Create One?

Mostly, you will use factory methods on the PendingIntent class, based on the particular action that you want performed on the underlying Intent:

These methods each take the same basic list of parameters, including:

How Do We Execute the PendingIntent?

If you are the recipient of a PendingIntent, you can execute it by calling send(). This will cause the PendingIntent to perform the requested action on the requested Intent. Note that you do not have the ability to change the action, nor do you have read access to the underlying Intent. You can optionally provide an Intent as input to the send() method, in which case data in your Intent gets blended into data already in the Intent inside of the PendingIntent, with the merged result used to start the activity, start the service, or send the broadcast.

Why Not Just Pass the Intent?

One could argue that a PendingIntent seems unnecessary. Suppose that we instead passed an Intent, in a situation where the recipient of that Intent knew intrinsically what to do with it (e.g., start an activity). What would be the point of wrapping this Intent into a PendingIntent?

For cases where the recipient of the Intent has the rights to perform the desired action, a PendingIntent would indeed be unnecessary. However, more often than not, the recipient of a PendingIntent does not have the rights to perform the desired action on the wrapped Intent directly. Most activities, services, and receivers in Android apps are not “exported”, meaning that they are private to the app. However, from a security standpoint, executing a PendingIntent works as if the action were being performed by whatever app created the PendingIntent originally, rather than the app that is executing the PendingIntent itself.

For example, suppose App A creates an activity PendingIntent, with an Intent that points to one of its activities. That particular activity is a private one for the app — it has no <intent-filter> in the manifest and is not otherwise marked as being exported. App A then passes that PendingIntent to App B. App B, on its own, has no ability to start this activity. However, App B can send() the PendingIntent, and the PendingIntent can start the activity, because that PendingIntent was created by App A, not App B.

In international diplomacy, the plot of land that an embassy sits upon is considered to be the sovereign territory of the country of the embassy, not the country in which the embassy resides. For example, the land around the French Embassy in Washington DC is part of France, not the United States. Similarly, it is as if a PendingIntent is part of the app that creates it, even if that PendingIntent is passed to other apps or to system processes.

Where Will We Use PendingIntents?

You can put a PendingIntent in an Intent extra, since a PendingIntent is Parcelable. Similarly, as we will see in a later chapter, there are other ways of getting data to a bound service, and a PendingIntent will work for those as well. So, one use of PendingIntent objects is to pass them to other services, where those services can execute the PendingIntent objects as needed.

However, beyond that, there are many places in Android where PendingIntent objects get used, including:

Communicating From Services

Sending commands to a service, by default, is a one-way street. Frequently, though, we need to get results from our service back to our activity. There are a few approaches for how to accomplish this, above and beyond passing your own custom PendingIntent as described above.

Broadcast Intents

One approach, first mentioned in the chapter on Intent filters, is to have the service send a broadcast Intent that can be picked up by the activity… assuming the activity is still around and is not paused. The service can call sendBroadcast(), supplying an Intent that identifies the broadcast, designed to be picked up by a BroadcastReceiver. This could be a component-specific broadcast (e.g., new Intent(this, MyReceiver.class)), if the BroadcastReceiver is registered in the manifest. Or, it can be based on some action string, perhaps one even documented and designed for third-party applications to listen for.

The activity, in turn, can register a BroadcastReceiver via registerReceiver(), though this approach will only work for Intent objects specifying some action, not ones identifying a particular component. But, when the activity’s BroadcastReceiver receives the broadcast, it can do what it wants to inform the user or otherwise update itself.

However, for local services, this is not a good choice. System broadcasts like this are intrinsically system-wide; for a local service, you should be using a communications channel that is private to your process.

Pending Results

Your activity can call createPendingResult(). This returns a PendingIntent. In this case, the PendingIntent will cause a result to be delivered to your activity’s implementation of onActivityResult(), just as if another activity had been called with startActivityForResult() and, in turn, called setResult() to send back a result.

Event Buses

Event bus implementations — like LocalBroadcastManager or greenrobot’s EventBus — are a great solution for having a service communicate with objects elsewhere within your process. You can have the service raise events (e.g., NewEmailEvent, UploadCompletedEvent, MartiansHaveLandedEvent), which activities or fragments can listen for and respond to.

Messenger

Yet another possibility is to use a Messenger object. A Messenger sends messages to an activity’s Handler. Within a single activity, a Handler can be used to send messages to itself, as was mentioned briefly in the chapter on threads. However, between components — such as between an activity and a service — you will need a Messenger to serve as the bridge.

As with a PendingIntent, a Messenger is Parcelable, and so can be put into an Intent extra. The activity calling startService() or bindService() would attach a Messenger as an extra on the Intent. The service would obtain that Messenger from the Intent. When it is time to alert the activity of some event, the service would:

  1. Call Message.obtain() to get an empty Message object
  2. Populate that Message object as needed, with whatever data the service wishes to pass to the activity
  3. Call send() on the Messenger, supplying the Message as a parameter

The Handler will then receive the message via handleMessage(), on the main application thread, and so can update the UI or whatever is necessary.

Notifications

Another approach is for the service to let the user know directly about the work that was completed. To do that, a service can raise a Notification — putting an icon in the status bar and optionally shaking or beeping or something. This technique is covered in an upcoming chapter.

We can also combine these techniques, such as using an event bus event and detecting when nothing in the UI layer receives the event, so we know that we need to display a Notification. We will be examining this pattern later in the book as well.

Scenario: The Downloader

If you elect to download something from the Play Store, you are welcome to back out of the Play Store application entirely. This does not cancel the download – the download and installation run to completion, despite no Play Store activity being on-screen.

You may have similar circumstances in your application, from downloading a purchased e-book to downloading a map for a game to downloading a file from some sort of “drop box” file-sharing service. And, perhaps DownloadManager is not going to be a great choice, for any number of reasons (e.g., you want to download the file to internal storage).

The sample project reviewed in this section is Service/Downloader, which implements such a downloading service.

The Design

This sort of situation is a perfect use for the command pattern and either an IntentService or a JobIntentService. Either of those have a background thread, so downloads can take as long as needed. Either of those will automatically shut down when the work is done, so the service will not linger and you do not need to worry about shutting it down yourself. Your activity can simply send a command to tell it to go do the work.

In this sample, we will use an IntentService. A clone of this project that uses a JobIntentService appears later in this chapter.

Admittedly, things get a bit trickier when you want to have the activity find out when the download is complete. This example will show the use of LocalBroadcastManager for this.

Things get even trickier when you want to download to a public location on external storage, such as the Downloads directory. On Android 6.0+ devices, with a targetSdkVersion of 23 or higher, you need to request runtime permissions before you can write to external storage. However, requesting runtime permissions needs to be done by the UI layer — a service cannot request permissions on its own (though it can check to see if the app has permission). The simplest thing to do is to request the permissions, if needed, before starting the service. This sample app demonstrates this.

Using the Service

The DownloadFragment demonstrating the use of Downloader has a trivial UI, consisting of one large button:

<?xml version="1.0" encoding="utf-8"?>
<Button xmlns:android="http://schemas.android.com/apk/res/android"
  android:id="@+id/button"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:text="@string/do_the_download"
/>

(from Service/Downloader/app/src/main/res/layout/main.xml)

That UI is initialized in onCreateView(), as usual:

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup parent,
                           Bundle savedInstanceState) {
    View result=inflater.inflate(R.layout.main, parent, false);

    b=result.findViewById(R.id.button);
    b.setOnClickListener(this);

    return(result);
  }
(from Service/Downloader/app/src/main/java/com/commonsware/android/downloader/DownloadFragment.java)

When the user clicks the button, onClick() is called. What happens now depends on whether we have permission to write to external storage or not:

  @Override
  public void onClick(View v) {
    if (hasPermission(WRITE_EXTERNAL_STORAGE)) {
      doTheDownload();
    }
    else {
      requestPermissions(
        new String[] { WRITE_EXTERNAL_STORAGE }, REQUEST_STORAGE);
    }
  }
(from Service/Downloader/app/src/main/java/com/commonsware/android/downloader/DownloadFragment.java)

The first time the user runs the app, the app will not have permission yet. hasPermission(WRITE_EXTERNAL_STORAGE) will return false, where hasPermission() is a utility method wrapping around ContextCompat.checkSelfPermission():

  private boolean hasPermission(String perm) {
    return(ContextCompat.checkSelfPermission(getActivity(), perm)==
      PackageManager.PERMISSION_GRANTED);
  }
(from Service/Downloader/app/src/main/java/com/commonsware/android/downloader/DownloadFragment.java)

WRITE_EXTERNAL_STORAGE is a static import, just to cut down on verbosity:

import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
(from Service/Downloader/app/src/main/java/com/commonsware/android/downloader/DownloadFragment.java)

If hasPermission() returns false, we call requestPermissions() on FragmentCompat, as we are in a fragment, not an activity. That eventually routes to onRequestPermissionsResult():

  @Override
  public void onRequestPermissionsResult(int requestCode, String[] permissions,
                                         int[] grantResults) {
    if (hasPermission(WRITE_EXTERNAL_STORAGE)) {
      doTheDownload();
    }
  }
(from Service/Downloader/app/src/main/java/com/commonsware/android/downloader/DownloadFragment.java)

Hence, if we either have permission when the user clicks the button, or if we receive permission after asking for it, we call a doTheDownload() method to kick off the download. Specifically, we disable the button (to prevent accidental duplicate downloads) and call startService() to send over a command:

  private void doTheDownload() {
    b.setEnabled(false);

    Intent i=new Intent(getActivity(), Downloader.class);

    i.setData(Uri.parse("https://commonsware.com/Android/Android-1_0-CC.pdf"));

    getActivity().startService(i);
  }
(from Service/Downloader/app/src/main/java/com/commonsware/android/downloader/DownloadFragment.java)

Here, the Intent we pass over has the URL of the file to download (in this case, a URL pointing to a PDF).

The Service Implementation

Here is the implementation of this IntentService, named Downloader:

package com.commonsware.android.downloader;

import android.app.IntentService;
import android.content.Intent;
import android.os.Environment;
import android.support.v4.content.LocalBroadcastManager;
import android.util.Log;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;

public class Downloader extends IntentService {
  public static final String ACTION_COMPLETE=
      "com.commonsware.android.downloader.action.COMPLETE";

  public Downloader() {
    super("Downloader");
  }

  @Override
  public void onHandleIntent(Intent i) {
    try {
      File root=
          Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);

      root.mkdirs();

      File output=new File(root, i.getData().getLastPathSegment());

      if (output.exists()) {
        output.delete();
      }

      URL url=new URL(i.getData().toString());
      HttpURLConnection c=(HttpURLConnection)url.openConnection();

      FileOutputStream fos=new FileOutputStream(output.getPath());
      BufferedOutputStream out=new BufferedOutputStream(fos);

      try {
        InputStream in=c.getInputStream();
        byte[] buffer=new byte[8192];
        int len=0;

        while ((len=in.read(buffer)) >= 0) {
          out.write(buffer, 0, len);
        }

        out.flush();
      }
      finally {
        fos.getFD().sync();
        out.close();
        c.disconnect();
      }

      LocalBroadcastManager.getInstance(this)
                           .sendBroadcast(new Intent(ACTION_COMPLETE));
    }
    catch (IOException e2) {
      Log.e(getClass().getName(), "Exception in download", e2);
    }
  }
}
(from Service/Downloader/app/src/main/java/com/commonsware/android/downloader/Downloader.java)

Our business logic is in onHandleIntent(), which is called on an Android-supplied background thread, so we can take whatever time we need. Also, when onHandleIntent() ends, the IntentService will stop itself automatically… assuming no other requests for downloads occurred while onHandleIntent() was running. In that case, onHandleIntent() is called again for the next download, and so on.

In onHandleIntent(), we first set up a File object pointing to where we want to download the file. We use getExternalStoragePublicDirectory() to find the public folder for downloads. Since this directory may not exist, we need to create it using mkdirs(). We then use the getLastPathSegment() convenience method on Uri, which returns to us the filename portion of a path-style Uri. The result is that our output File object points to a file, named the same as the file we are downloading, in a public folder.

We then go through a typical HttpUrlConnection process to connect to the URL supplied via the Uri in the Intent, streaming the results from the connection (8KB at a time) out to our designated file. Then, we follow the requested recipe to ensure our file is saved:

This recipe was explained back in the chapter on file I/O.

Finally, it would be nice to let somebody know that the download has completed. So, we send a local broadcast Intent, with our own custom action (ACTION_COMPLETE), using LocalBroadcastManager.

Note that, in theory, we could start the service, but the user could revoke our permission before we get a chance to start the download. In practice, this is very unlikely to happen, as our download should start within milliseconds. However, when working with runtime permissions from services, you need to consider the length of time between confirming that you have permission and when you perform the actions secured by that permission. If there may be a significant time gap, double-check the permission before trying the actions (e.g., writing to external storage). It is cleaner to recover from checkSelfPermission() indicating that you do not have permission than from some IOException or SecurityException because you do not have permission and tried the action anyway.

If your service determines that it does not have permission, you cannot call requestPermissions(), as a service is neither an activity nor a fragment. Instead, raise a notification and gracefully exit the service. The notification can direct the user somewhere in the app where you can request the permission (again) and re-try the work to be done by the service.

Receiving the Broadcast

Our DownloadFragment is set up to listen for that local broadcast Intent, by registering a local BroadcastReceiver in onStart() and unregistering it in onStop():

  @Override
  public void onStart() {
    super.onStart();

    IntentFilter f=new IntentFilter(Downloader.ACTION_COMPLETE);

    LocalBroadcastManager.getInstance(getActivity())
                         .registerReceiver(onEvent, f);
  }

  @Override
  public void onStop() {
    LocalBroadcastManager.getInstance(getActivity())
                         .unregisterReceiver(onEvent);

    super.onStop();
  }
(from Service/Downloader/app/src/main/java/com/commonsware/android/downloader/DownloadFragment.java)

The BroadcastReceiver itself re-enables our button, plus displays a Toast indicating that the download is complete:

  private BroadcastReceiver onEvent=new BroadcastReceiver() {
    public void onReceive(Context ctxt, Intent i) {
      b.setEnabled(true);

      Toast.makeText(getActivity(), R.string.download_complete,
                     Toast.LENGTH_LONG).show();
    }
  };
(from Service/Downloader/app/src/main/java/com/commonsware/android/downloader/DownloadFragment.java)

Note that if the user leaves the activity (e.g., BACK, HOME), the broadcast will not be received by the activity. There are other ways of addressing this, particularly combining an ordered broadcast with a Notification, which we will examine later in this book.

Limitation: Time

On Android 7.1 and older, this sample app works fine.

On Android 8.0 and newer, this sample app works fine… because the PDF file that it is trying to download is not that big.

However, on Android 8.0+, a started service like this can run for only one minute. After that, Android will stop the service. That will not terminate our background thread immediately, but it will make it far more likely that Android will terminate our entire process, taking our thread and download with it.

As a result, nowadays, an IntentService is suitable only for scenarios where you are quite certain that the work will get done in less than a minute.

One way to work around this is to make the service be a “foreground service”. In the upcoming chapter on notifications, we will revisit this sample and discuss the use of startForeground() to make a service be a foreground service.

Another workaround is to use JobIntentService, as we will see in the next section.

Limitation: Staying Awake

Even if our work is certain to take less than a minute, we may run into another problem: the device might fall asleep. When the screen turns off, by default, Android will power down the CPU, suspending all running processes. This, in turn, will prevent us from downloading the file anymore.

JobIntentService works around this by way of a “wakelock”: a request to the OS to keep the device awake, even though the screen turns off. You can use a wakelock yourself to keep the device awake, for scenarios where you need that capability but where JobIntentService is not a great option. We will explore wakelocks in greater detail in the chapter on AlarmManager.

JobIntentService

IntentService still works on Android 8.0, but unless you make it be a foreground service, you will be limited to ~1 minute of runtime before your service is stopped abruptly. JobIntentService is a wrapper around a JobService that offers IntentService-style semantics. On Android 8.0 and higher, when you tell a JobIntentService to do some work, it enqueues that work via JobScheduler. On Android 7.1 and earlier, the JobIntentService behaves more like a regular IntentService, though one that supplies a WakeLock for you (akin to the author’s WakefulIntentService).

The Service/JobIntentService sample project is a clone of the IntentService sample, where we use the service to download a PDF file. The revised sample swaps out the IntentService with a JobIntentService, which is a fairly easy conversion to make.

First, we need to defend it with the android.permission.BIND_JOB_SERVICE permission:

    <service
      android:name="Downloader"
      android:permission="android.permission.BIND_JOB_SERVICE" />
(from Service/JobIntentService/app/src/main/AndroidManifest.xml)

This is required because, under the covers, a JobIntentService is really a JobService, and JobService requires this android:permission attribute. We will explore this more in the chapter on JobScheduler.

What had been onHandleIntent() in an IntentService turns into onHandleWork() in a JobIntentService:

package com.commonsware.android.downloader;

import android.content.Context;
import android.content.Intent;
import android.os.Environment;
import android.support.v4.app.JobIntentService;
import android.support.v4.content.LocalBroadcastManager;
import android.util.Log;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;

public class Downloader extends JobIntentService {
  public static final String ACTION_COMPLETE=
      "com.commonsware.android.downloader.action.COMPLETE";
  private static final int UNIQUE_JOB_ID=1337;

  static void enqueueWork(Context ctxt, Intent i) {
    enqueueWork(ctxt, Downloader.class, UNIQUE_JOB_ID, i);
  }

  @Override
  public void onHandleWork(Intent i) {
    try {
      File root=
          Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);

      root.mkdirs();

      File output=new File(root, i.getData().getLastPathSegment());

      if (output.exists()) {
        output.delete();
      }

      URL url=new URL(i.getData().toString());
      HttpURLConnection c=(HttpURLConnection)url.openConnection();

      FileOutputStream fos=new FileOutputStream(output.getPath());
      BufferedOutputStream out=new BufferedOutputStream(fos);

      try {
        InputStream in=c.getInputStream();
        byte[] buffer=new byte[8192];
        int len=0;

        while ((len=in.read(buffer)) >= 0) {
          out.write(buffer, 0, len);
        }

        out.flush();
      }
      finally {
        fos.getFD().sync();
        out.close();
        c.disconnect();
      }

      LocalBroadcastManager.getInstance(this)
                           .sendBroadcast(new Intent(ACTION_COMPLETE));
    }
    catch (IOException e2) {
      Log.e(getClass().getName(), "Exception in download", e2);
    }
  }
}
(from Service/JobIntentService/app/src/main/java/com/commonsware/android/downloader/Downloader.java)

The semantics of onHandleWork() are the same as doWakefulWork() with a WakefulIntentService:

Because our code is using a wakelock — by way of a support library — we need to have the WAKE_LOCK permission in the manifest, along with other permissions needed for our business logic:

  <uses-permission android:name="android.permission.WAKE_LOCK" />
  <uses-permission android:name="android.permission.INTERNET" />
  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
(from Service/JobIntentService/app/src/main/AndroidManifest.xml)

To arrange to have the service perform some work, with IntentService, you would use startService(), with an Intent identifying the service (and, optionally, passing along details of the work to be done). With JobIntentService, you instead call a static enqueueWork() method defined on JobIntentService. This takes four parameters:

Since the second and third parameters are constants, you can create your own enqueueWork() method that calls the JobService implementation, passing along those constant values:

  private static final int UNIQUE_JOB_ID=1337;

  static void enqueueWork(Context ctxt, Intent i) {
    enqueueWork(ctxt, Downloader.class, UNIQUE_JOB_ID, i);
  }
(from Service/JobIntentService/app/src/main/java/com/commonsware/android/downloader/Downloader.java)

Then, your code that wishes to have the work performed calls your enqueueWork() method to do so:

  private void doTheDownload() {
    b.setEnabled(false);

    Intent i=new Intent(getActivity(), Downloader.class);

    i.setData(Uri.parse("https://commonsware.com/Android/Android-1_0-CC.pdf"));

    Downloader.enqueueWork(getActivity(), i);
  }
(from Service/JobIntentService/app/src/main/java/com/commonsware/android/downloader/DownloadFragment.java)

The JobIntentService can spend up to ~10 minutes in onHandleWork() on Android 8.0+, which is a substantial improvement over the ~1 minute a background IntentService has. If, however, there is a substantial chance that the work would exceed 10 minutes, use a foreground IntentService.

However, there is one issue with JobIntentService: on Android 8.0+, it may not do its work right away. On Android 8.0+, JobIntentService schedules its work via JobScheduler, and JobScheduler may elect to postpone that work for a bit. In cases where you really need the work to be started immediately, JobIntentService is not a good choice.

IntentService or JobIntentService?

Use JobIntentService if:

Use a plain IntentService if you are sure that the work will take less than a minute.

Use a foreground IntentService if either:

Services and Configuration Changes

Services are not directly affected by configuration changes the way that activities are. While activities will be destroyed and recreated by default, services continue running if they were created.

Usually, services do not really care about configuration changes. However, if you have a service that does care, you can override onConfigurationChanged() in the service.

This means that you have two choices for dealing with configuration changes: override onConfigurationChanged() or simply re-read in the configuration information as needed. For example, suppose that you need to know the user’s chosen locale, to include as information in a Web service call. If you are checking the locale on each Web service call, your service does not need to know about configuration changes. If, on the other hand, you prefer to cache the locale data, reading it in from the Locale class when the service is created, you will want to override onConfigurationChanged() and update that cache, in case the configuration change was a locale change.

When Do Services End?

One common question with services is: when do services end? If we start them, what causes them to ever stop?

While services are designed to run in the background for a while, they will not run forever. Exactly how long they will run depends on what stops them, and there are several scenarios for that.

Scenario #1: You Stop It

If you call stopSelf() from inside a started service, the service is stopped and destroyed. Or, if you call stopService() from something outside of the service — but supplying an Intent that identifies your service — the service is stopped and destroyed. In both cases, the service is stopped and destroyed asynchronously, so the service will be running for a bit even after the stopSelf() or stopService() call. However, the service will be stopped and destroyed fairly quickly, assuming that you are not tying up the main application thread for some reason.

As we will see in the chapter on binding and remote services, if you unbind from a bound service, and nothing else has bound to it or has started it, Android will destroy the service.

So, you can cause a service to stop by taking some positive action to stop it, much as we did with the stopService() call in the FakePlayer example earlier in this chapter.

Scenario #2: Android Terminates Your Process

The real point behind a service is to elevate your process’ importance within the operating system. Android terminates low-importance processes to free up system RAM for other processes representing other apps.

However, while process importance is the primary criterion for Android to choose processes to terminate, it is not the only criterion. Process age and memory consumption are also taken into account. As a result, your process will not run forever, even with a service running.

Needless to say, when your process is terminated, your service goes with it.

Scenario #3: The User Nukes You From Orbit

Not only can Android terminate your process, but so can the user, by any number of ways:

In the first two scenarios, if you had a running service, and if your onStartCommand() returns something like START_STICKY or START_REDELIVER_INTENT, eventually Android will fork a fresh process for you and restart your service. This also happens if Android terminates your process on its own due to old age.

If the user presses “Force Stop”, nothing of your app will run again, until the user launches your app from the home screen or something else uses an explicit Intent to start one of your components.

Scenario #4: Automatically, After a Minute

All of the previous options have been a possibility for a long time, frequently since Android 1.0.

New to Android 8.0 is a time limit on background services: you can run for a minute, and then you’re done. We will explore this more in the next section.

Background Service Limitations

For apps that have a targetSdkVersion of 26 or higher and are running on Android 8.0, background services are limited. After a short period of time — as low as one minute — any such services will be stopped and you will be unable to start new ones.

Also, even if your targetSdkVersion is 25 or lower, you might still have these limitations applied to your app. If your app appears on the Battery screen in Settings — indicating that it is using above-average power — the user will have the ability to apply these limitations to your app from there.

What Is a Background Service, Exactly?

Here, “background services” are ones that:

That latter scenario covers cases where your service exposes an API that is bound to by other apps or by core OS processes. This includes custom APIs implemented via AIDL and framework-supplied APIs such as those used by JobService, TileService, and so on.

Certain events, such as having your code triggered by a Notification PendingIntent, or by receiving a broadcast, will also give you a fresh window of time when background services behave normally.

What Happens?

The documentation indicates that when your process moves from the foreground to the background, or when one of the other triggers (e.g., receiving a broadcast) occurs, you have “several minutes” of normal operation. Testing suggests that by “several minutes”, Google actually means “a minute or so”.

At that point:

Your process is still running at this point. However, its importance is the same as if you had no service running, meaning that your process is at risk of being terminated at any point to free up system RAM.

Note that while the documentation suggests that startService() will throw an IllegalStateException when your app is ineligible to start a background service, this behavior varies. If a service calls startService(), usually no exception is thrown and the call seems to fail quietly (or perhaps with only a debug LogCat message about the issue). If another Context is used — such as the one handed to onReceive() of a BroadcastReceiver — you will get the exception:


Process: com.commonsware.android.service.ouroboros, PID: 27276
java.lang.RuntimeException: Unable to start receiver com.commonsware.android.service.ouroboros.HackReceiver: java.lang.IllegalStateException: Not allowed to start service Intent { cmp=com.commonsware.android.service.ouroboros/.SecondSillyService (has extras) }: app is in background
   at android.app.ActivityThread.handleReceiver(ActivityThread.java:3159)
   at android.app.ActivityThread.-wrap18(Unknown Source:0)
   at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1621)
   at android.os.Handler.dispatchMessage(Handler.java:102)
   at android.os.Looper.loop(Looper.java:154)
   at android.app.ActivityThread.main(ActivityThread.java:6408)
   at java.lang.reflect.Method.invoke(Native Method)
   at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:232)
   at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:751)
Caused by: java.lang.IllegalStateException: Not allowed to start service Intent { cmp=com.commonsware.android.service.ouroboros/.SecondSillyService (has extras) }: app is in background
   at android.app.ContextImpl.startServiceCommon(ContextImpl.java:1451)
   at android.app.ContextImpl.startService(ContextImpl.java:1405)
   at android.content.ContextWrapper.startService(ContextWrapper.java:630)
   at android.content.ContextWrapper.startService(ContextWrapper.java:630)
   at com.commonsware.android.service.ouroboros.HackReceiver.onReceive(HackReceiver.java:12)
   at android.app.ActivityThread.handleReceiver(ActivityThread.java:3152)
   at android.app.ActivityThread.-wrap18(Unknown Source:0) 
   at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1621) 
   at android.os.Handler.dispatchMessage(Handler.java:102) 
   at android.os.Looper.loop(Looper.java:154) 
   at android.app.ActivityThread.main(ActivityThread.java:6408) 
   at java.lang.reflect.Method.invoke(Native Method) 
   at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:232) 
   at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:751)

What Are the Alternatives?

An obvious solution is to use a foreground service. You have three options for doing this:

The latter two approaches work even when your process no longer has the ability to call startService(). However, those methods are new to Android 8.0. Also, despite their method names, they do not actually start your service as a foreground service. Rather, they give you a short reprieve from the normal service-starting limits — your service needs to call startForeground() shortly after being created, or else your service will be destroyed.

A JobService can spend ~10 minutes processing a job, before Android will consider the service to be broken and reduce the process’ priority to lower levels. Hence, using JobScheduler may be an option for you, particularly if the work you are trying to do is periodic in nature, where your JobService does the work.

As this change only affects apps with a targetSdkVersion higher than 25, keeping your targetSdkVersion at 25 or lower will avoid this behavior change.