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.
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:
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.
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.
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:
JobService
, for doing periodic or scheduled workTileService
, for adding a custom tile to the notification shade of the
deviceChooserTargetService
, for helping provide more specific integration options
with other appsAnd 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.
Just as activities have onCreate()
, onResume()
, onPause()
and kin,
Service
implementations have their own lifecycle methods, such as:
onCreate()
, which, as with activities, is called when the service
is created, by any meansonStartCommand()
, which is called each time the service is sent a command
via startService()
onBind()
, which is called whenever a client binds to the service via
bindService()
onDestroy()
which is called as the service is being shut downAs 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.
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>
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.
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()
:
Intent
extras, to tell the service what
it is supposed to doFor 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:
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()
START_REDELIVER_INTENT
, meaning that the service should be restarted via a
call to onStartCommand()
, supplying the same Intent
as was delivered this
timeSTART_NOT_STICKY
, meaning that the service should remain stopped until
explicitly started by application codeBy 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.
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.
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
.
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.
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;
}
}
}
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.
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>
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);
}
}
}
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.
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.
Principally, a PendingIntent
is a wrapper around an Intent
, identifying a specific
action to be performed on that Intent
:
Mostly, you will use factory methods on the PendingIntent
class, based
on the particular action that you want performed on the underlying Intent
:
PendingIntent.getActivity()
to create a PendingIntent
that will start an activityPendingIntent.getService()
to create a PendingIntent
that will start a servicePendingIntent.getForegroundService()
to create a PendingIntent
that will start a service
that, in turn, will use startForeground()
to become a foreground servicePendingIntent.getBroadcast()
to create a PendingIntent
that will send a broadcastThese methods each take the same basic list of parameters, including:
Context
Intent
to wrapPendingIntent
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.
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.
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:
AlarmManager
LocationManager
or Play Services’ fused location provider
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.
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.
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 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.
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:
Message.obtain()
to get an empty Message
objectMessage
object as needed, with whatever data the service
wishes to pass to the activitysend()
on the Messenger
, supplying the Message
as a parameterThe Handler
will then receive the message via handleMessage()
, on the main
application thread, and so can update the UI or whatever is necessary.
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.
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.
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.
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"
/>
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);
}
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);
}
}
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);
}
WRITE_EXTERNAL_STORAGE
is a static
import, just to cut down on verbosity:
import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
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();
}
}
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);
}
Here, the Intent
we pass over has the URL of the file to download (in this
case, a URL pointing to a PDF).
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);
}
}
}
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:
flush()
the streamsync()
the FileDescriptor
(from getFD()
)close()
the streamThis 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.
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();
}
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();
}
};
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.
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.
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
.
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" />
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);
}
}
}
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" />
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:
Context
Class
object for your JobIntentService
subclass (e.g., Downloader.class
)JobScheduler
to perform
the work, where the job ID needs to be unique within your app compared to anything
else that is using JobScheduler
Intent
akin to the one you would have used with startService()
and IntentService
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);
}
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);
}
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.
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:
JobIntentService
, orServices 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.
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.
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.
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.
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.
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.
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.
Here, “background services” are ones that:
startForeground()
to raise themselves
to foreground importance (with a Notification
), 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.
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:
stopService()
-style
functionality, andstartService()
will failYour 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)
An obvious solution is to use a foreground service. You have three options for doing this:
startService()
,
then have the service call startForeground()
getForegroundService()
method on PendingIntent
startForegroundService()
method on Context
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.