Many applications have the need to get control every so often to do a bit of work. And, many times, those applications need to get control in the background, regardless of what the user may be doing (or not doing) at the time.
The solution, in most cases, is to use AlarmManager
, which is roughly
akin to cron
on Linux and OS X and Scheduled Tasks in Windows. You
teach AlarmManager
when you want to get control back, and AlarmManager
will give you control at that time.
The two main axes to consider with scheduled work are frequency and foreground (vs. background).
If you have an activity that needs to get control every second, the
simplest approach is to use a postDelayed()
loop, scheduling a Runnable
to be invoked after a certain delay, where the Runnable
reschedules itself
to be invoked after the delay in addition to doing some work. We saw this
in the chapter on threads.
This has the advantages of giving you control back on the main application
thread and avoiding the need for any background threads.
On the far other end of the spectrum, you may need to get control on a somewhat
slower frequency (e.g., every 15 minutes), and do so in the background,
even if nothing of your app is presently running. You might need to poll
some Web server for new information, such as downloading updates to an RSS
feed. This is the scenario that AlarmManager
excels at. While postDelayed()
works inside your process (and therefore does not work if you no longer
have a process), AlarmManager
maintains its schedule outside of your
process. Hence, it can arrange to give you control, even if it has to
start up a new process for you along the way.
JobScheduler
, added to Android 5.0, also does this. If your
minSdkVersion
is 21 or higher, JobScheduler
is definitely worth
considering, as it not only takes time into account, but other
environmental factors as well. For example, if you need an Internet
connection to do your work, JobScheduler
will only give you control
if there is an Internet connection. JobScheduler
is covered a bit
later in the book.
There are a variety of things you will be able to configure about your
scheduled alarms with AlarmManager
.
The biggest one is whether or not the scheduled event should wake up the device.
A device goes into a sleep mode shortly after the screen goes dark. During this time, nothing at the application layer will run, until something wakes up the device. Waking up the device does not necessarily turn on the screen — it may just be that the CPU starts running your process again.
If you choose a “wakeup”-style alarm, Android will wake up the device to give you control. This would be appropriate if you need this work to occur even if the user is not actively using the device, such as your app checking for critical email messages in the middle of the night. However, it does drain the battery some.
Alternatively, you can choose an alarm that will not wake up the device. If your desired time arrives and the device is asleep, you will not get control until something else wakes up the device.
You can create a “one-shot” alarm, to get control once at a particular time in the future. Or, you can create an alarm that will give you control periodically, at a fixed period of your choice (e.g., every 15 minutes).
If you need to get control at multiple times, but the schedule is irregular, use a “one-shot” alarm for the nearest time, where you do your work and schedule a “one-shot” alarm for the next-nearest time. This would be appropriate for scenarios like a calendar application, where you need to let the user know about upcoming appointments, but the times for those appointments may not have any fixed schedule.
However, for most polling operations (e.g., checking for new messages every NN minutes), a repeating alarm will typically be the better answer.
If you do choose a repeating alarm, you will have your choice over having (relatively) precise control over the timing of event or not.
If you choose an “inexact” alarm, while you will provide Android with a suggested time for the first event and a period for subsequent events, Android reserves the right to shift your schedule somewhat, so it can process your events and others around the same time. This is particularly important for “wakeup”-style alarms, as it is more power-efficient to wake up the device fewer times, so Android will try to combine multiple apps’ events to be around the same time to minimize the frequency of waking up the device.
However, inexact alarms are annoying to test and debug, simply because you do not have control over when they will be invoked. Hence, during development, you might start with an exact alarm, then switch to inexact alarms once most of your business logic is debugged.
Note that Android 4.4 changes the behavior of AlarmManager
, such that
it is more difficult to actually create an exact-repeating alarm schedule.
This will be examined in greater detail shortly, as we review the various
methods and flags for scheduling AlarmManager
events.
As part of the alarm configuration, you will tell Android when the event is to occur (for one-shot alarms) or when the event is to first occur (for repeating alarms). You can provide that time in one of two ways:
For most polling operations, particularly for periods more frequent than once
per day, specifying the time relative to now is easiest. However, some alarms
may need to tie into “real world time”, such as alarm clocks and calendar
alerts — for those, you will need to use the real-time clock (typically
by means of a Java Calendar
object) to indicate when the event should
occur.
And, of course, you will need to tell Android what to do when each of these
timer events occurs. You will do that in the form of supplying a
PendingIntent
. First mentioned in the chapter on services,
a PendingIntent
is a Parcelable
object, one that indicates an operation
to be performed upon an Intent
:
While the service chapter discussed an Android activity using
createPendingResult()
to craft such a PendingIntent
, that is usually
not very useful for AlarmManager
, as the PendingIntent
will only be
valid so long as the activity is in the foreground. Instead, there are
static factory methods on PendingIntent
that you will use instead
(e.g., getBroadcast()
to create a PendingIntent
that calls sendBroadcast()
on a supplied Intent
). That being said, our next sample will use
createPendingResult()
, to keep the sample as simple as possible.
A trivial sample app using AlarmManager
can be found in
AlarmManager/Simple
.
This application consists of a single activity, SimpleAlarmDemoActivity
,
that will both set up an alarm schedule and respond to alarms:
package com.commonsware.android.alarm;
import android.app.Activity;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Intent;
import android.os.Bundle;
import android.os.SystemClock;
import android.util.Log;
import android.widget.Toast;
public class SimpleAlarmDemoActivity extends Activity {
private static final int ALARM_ID=1337;
private static final int PERIOD=5000;
private PendingIntent pi=null;
private AlarmManager mgr=null;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
mgr=(AlarmManager)getSystemService(ALARM_SERVICE);
pi=createPendingResult(ALARM_ID, new Intent(), 0);
mgr.setRepeating(AlarmManager.ELAPSED_REALTIME,
SystemClock.elapsedRealtime() + PERIOD, PERIOD, pi);
}
@Override
public void onDestroy() {
mgr.cancel(pi);
super.onDestroy();
}
@Override
protected void onActivityResult(int requestCode, int resultCode,
Intent data) {
if (requestCode == ALARM_ID) {
Toast.makeText(this, R.string.toast, Toast.LENGTH_SHORT).show();
Log.d(getClass().getSimpleName(), "I ran!");
}
}
}
In onCreate()
, in addition to setting up the “hello, world”-ish UI, we:
AlarmManager
, by calling getSystemService()
,
asking for the ALARM_SERVICE
, and casting the result to be an AlarmManager
PendingIntent
by calling createPendingResult()
, supplying
an empty Intent
as our “result” (since we do not really need it here)setRepeating()
on AlarmManager
The call to setRepeating()
is a bit complex, taking four parameters:
ELAPSED_REALTIME
, indicating
that we want to use a relative time base for when the first event should
occur (i.e., relative to now) and that we do not need to wake up the device
out of any sleep modePERIOD
) added to “now” as determined
by SystemClock.elapsedRealtime()
(the number of milliseconds since the
device was last rebooted)PendingIntent
to invoke for each of these eventsWhen the event occurs, since we used createPendingResult()
to create
the PendingIntent
, our activity gets control in onActivityResult()
,
where we simply display a Toast
(if the event is for our alarm’s request
ID). This continues until the activity is destroyed (e.g., pressing the
BACK button), at which time we cancel()
the alarm, supplying a
PendingIntent
to indicate which alarm to cancel. While here we use the
same PendingIntent
object as we used for scheduling the alarm, that is
not required — it merely has to be an equivalent PendingIntent
, meaning:
Intent
inside the PendingIntent
matches the scheduled alarm’s
Intent
, in terms of component, action, data (Uri
), MIME type, and
categoriesPendingIntent
(here, ALARM_ID
) must also matchRunning this simply brings up a Toast
every five seconds until you BACK
out of the activity.
There are five methods that you can call on AlarmManager
to establish
an alarm, including the setRepeating()
demonstrated above.
On Android 4.4 (API Level 19) and higher,
setExact()
is used for a one-shot alarm, where you want to get control at
one specific time in the future. This would be used for specific events or
for irregular alarm schedules.
On Android 4.3 and below, and for apps whose targetSdkVersion
is
set to 18 or lower, set()
has the same behavior as setExact()
. However,
on Android 4.4 and above, apps with their targetSdkVersion
set
to be 19 or higher will have different, inexact behavior for set()
.
The time of the event is considered a minimum — your PendingIntent
will not be invoked before your desired time, but it can occur any time
thereafter… and you do not have control over how long that delay will
be. As with all “inexact” schedules, the objective is for Android to be
able to “batch” these events, to do several around the same time, for
greater efficiency, particularly when waking up the device.
On Android 4.4 and higher, you have a setWindow()
option
that is a bit of a hybrid between the new-style set()
and setExact()
.
Here, you specify the time you want the event to occur and an amount
of time that Android can “flex” the actual event. So, for example, you might
set up an event to occur every hour, with a “window” of five minutes, to
allow Android the flexibility to invoke your PendingIntent
within that
five-minute window. This allows for better battery optimization than
with setExact()
, while still giving you some control over how far “off
the mark” the event can occur.
On Android 4.3 and below, and for apps whose targetSdkVersion
is
set to 18 or lower,
setRepeating()
is used for an alarm that should occur at specific points
in time at a specific frequency. In addition to specifying the time of the
first event, you also specify the period for future events. Android will
endeavor to give you control at precisely those times, though since Android
is not a real-time operating system (RTOS), microsecond-level accuracy is
certainly not guaranteed. However, note that as of Android 5.1, your
minimum period is one minute (60000ms) — values less than that will be
rounded up to one minute. This minimum period is enforced regardless
of your targetSdkVersion
value.
setInexactRepeating()
is used for an alarm that should occur on a general
frequency, such as every 15 minutes. In addition to specifying the time of the
first event, you also specify a general frequency, as one of the following
public static data members on AlarmManager
:
INTERVAL_FIFTEEN_MINUTES
INTERVAL_HALF_HOUR
INTERVAL_HOUR
INTERVAL_HALF_DAY
INTERVAL_DAY
Android guarantees that it will give your app control somewhere during that time window, but precisely when within that window is up to Android.
Note that on Android 4.4 and above, for apps with their targetSdkVersion
set
to be 19 or higher, setRepeating()
behaves identically to setInexactRepeating()
–
in other words, all repeating alarms are inexact. The only way to get exact
repeating would be to use setExact()
and to re-schedule the event yourself, rather
than relying upon Android doing that for you automatically. Ideally, you use
setInexactRepeating()
, to help extend battery life.
And, note that on Android 5.1 and higher, alarms must be set to occur at least 5 seconds in the future from now. You cannot trigger an alarm to occur in the future sooner than 5 seconds.
In the above sample, we used ELAPSED_REALTIME
as the type of alarm. There
are three others:
ELAPSED_REALTIME_WAKEUP
RTC
RTC_WAKEUP
Those with _WAKEUP
at the end will wake up a device out of sleep mode
to execute the PendingIntent
— otherwise, the alarm will wait until the
device is awake for other means.
Those that begin with ELAPSED_REALTIME
expect the second parameter
to setRepeating()
to be a timestamp based upon SystemClock.elapsedRealtime()
.
Those that begin with RTC
, however, expect the second parameter to be
based upon System.currentTimeMillis()
, the classic Java “what is the
current time in milliseconds since the Unix epoch” method.
The sample, though, begs a bit of a question: when are we supposed to
set up these alarms? The sample just does so in onCreate()
, but is that
sufficient?
For most apps, the answer is “no”. Here are the three times that you will need to ensure that your alarms get scheduled:
When your app is first installed, none of your alarms are set up, because your code has not yet run to schedule them. There is no means of setting up alarm information in the manifest or something that might automatically kick in.
Hence, you will need to schedule your alarms when the user first runs your app.
As a simplifying measure — and to cover another scenario outlined
below — you might be able to simply get away with scheduling your alarms
every time the user runs your app, as the sample app shown above does.
This works for one-shot alarms (using set()
)
and for alarms with short polling periods,
and it works because setting up a new alarm schedule for an equivalent
PendingIntent
will replace the old schedule. However, for repeating
alarms with slower polling periods, it may excessively delay your events.
For example, suppose you have an alarm set to go off every 24 hours,
and the user happens to run your app 5 minutes before the next event was
to occur — if you blindly reschedule the alarm, instead of going off in
5 minutes, it might not go off for another 24 hours.
There are more sophisticated approaches for this (e.g., using a
SharedPreferences
value to determine if your app has run before or not).
The alarm schedule for alarm manager is wiped clean on a reboot, unlike
cron
or Windows Scheduled Tasks. Hence, you will need to get control
at boot time to re-establish your alarms, if you want them to start up again
after a reboot. We saw how to get control at boot time, via an
ACTION_BOOT_COMPLETED
BroadcastReceiver
, back in
the chapter on broadcasts.
There are other events that could cause your alarms to become unscheduled. The best example of this is if the user goes into the Settings app and presses “Force Stop” for your app. At this point, on Android 3.1+, nothing of your code will run again, until the user manually launches some activity of yours.
If you are rescheduling your alarms every time your app runs, this will be corrected the next time the user launches your app. And, by definition, you cannot do anything until the user runs one of your activities, anyway.
If you are trying to avoid rescheduling your alarms on each run, though, you have a couple of options.
One is to record the time when your alarm-triggered events occur, each time
they occur, such as by updating a SharedPreference
. When the user launches
one of your activities, you check the last-event time — if it was too long
ago (e.g., well over your polling period), you assume that the alarm had been
canceled, and you reschedule it.
Another is to rely on FLAG_NO_CREATE
. You can pass this as a parameter
to any of the PendingIntent
factory methods, to indicate that Android should
only return an existing PendingIntent
if there is one, and not create one
if there is not:
PendingIntent pi=PendingIntent.getBroadcast(ctxt, 0, i, PendingIntent.FLAG_NO_CREATE);
If the PendingIntent
is null
, your alarm has been canceled — otherwise,
Android would already have such a PendingIntent
and would have returned it
to you. This feels a bit like a side-effect, so we cannot rule out the
possibility that, in future versions of Android, this technique could result
in false positives (null
PendingIntent
despite the scheduled alarm) or
false negatives (non-null
PendingIntent
despite a canceled alarm).
The classic AlarmManager
scenario is where you want to do a chunk of
work, in the background, on a periodic basis. This is fairly simple to set
up in Android, though perhaps not quite as simple as you might think.
When an AlarmManager
-triggered event occurs, it is very likely that your
application is not running. This means that the PendingIntent
is going to
have to start up your process to have you do some work. Since everything that
a PendingIntent
can do intrinsically gives you control on your main application
thread, you are going to have to determine how you want to move your work
to a background thread.
One approach is to use a PendingIntent
created by getService()
, and have
it send a command to an IntentService
that you write. Since IntentService
does its work on a background thread, you can take whatever time you need,
without interfering with the behavior of the main application thread. This
is particularly important when:
AlarmManager
-triggered event happens to occur when the user happens
to have one of your activities in the foreground, so you do not freeze the
UI, orAn incrementally-less-trivial sample app using AlarmManager
for the
scheduled service pattern can be found in
AlarmManager/Scheduled
.
This application consists of three components: a BroadcastReceiver
, a
Service
, and an Activity
.
This sample demonstrates scheduling your alarms at two points in your app:
For the boot-time scenario, we need a BroadcastReceiver
set up to receive
the ACTION_BOOT_COMPLETED
broadcast, with the appropriate permission. So,
we set that up, along with our other components, in the manifest:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.commonsware.android.schedsvc"
android:versionCode="1"
android:versionName="1.0">
<uses-sdk
android:minSdkVersion="7"
android:targetSdkVersion="11"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name">
<activity
android:name=".ScheduledServiceDemoActivity"
android:label="@string/app_name"
android:theme="@android:style/Theme.Translucent.NoTitleBar">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<receiver android:name="PollReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
</receiver>
<service android:name="ScheduledService">
</service>
</application>
</manifest>
The PollReceiver
has its onReceive()
method, to be called at boot
time, which delegates its work to a scheduleAlarms()
static method, so that
logic can also be used by our activity:
package com.commonsware.android.schedsvc;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.SystemClock;
public class PollReceiver extends BroadcastReceiver {
private static final int PERIOD=5000;
@Override
public void onReceive(Context ctxt, Intent i) {
scheduleAlarms(ctxt);
}
static void scheduleAlarms(Context ctxt) {
AlarmManager mgr=
(AlarmManager)ctxt.getSystemService(Context.ALARM_SERVICE);
Intent i=new Intent(ctxt, ScheduledService.class);
PendingIntent pi=PendingIntent.getService(ctxt, 0, i, 0);
mgr.setRepeating(AlarmManager.ELAPSED_REALTIME,
SystemClock.elapsedRealtime() + PERIOD, PERIOD, pi);
}
}
The scheduleAlarms()
method retrieves our AlarmManager
, creates a
PendingIntent
designed to call startService()
on our ScheduledService
,
and schedules an exact repeating alarm to have that command be sent every
five seconds.
The ScheduledService
itself is the epitome of “trivial”, simply logging a
message to LogCat on each command:
package com.commonsware.android.schedsvc;
import android.app.IntentService;
import android.content.Intent;
import android.util.Log;
public class ScheduledService extends IntentService {
public ScheduledService() {
super("ScheduledService");
}
@Override
protected void onHandleIntent(Intent intent) {
Log.d(getClass().getSimpleName(), "I ran!");
}
}
That being said, because this is an IntentService
, we could do much more
in onHandleIntent()
and not worry about tying up the main application thread.
Our activity — ScheduledServiceDemoActivity
— is set up with Theme.Translucent.NoTitleBar
in the manifest, never calls setContentView()
, and calls finish()
right
from onCreate()
. As a result, it has no UI. It simply calls scheduleAlarms()
and raises a Toast
to indicate that the alarms are indeed scheduled:
package com.commonsware.android.schedsvc;
import android.app.Activity;
import android.os.Bundle;
import android.widget.Toast;
public class ScheduledServiceDemoActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
PollReceiver.scheduleAlarms(this);
Toast.makeText(this, R.string.alarms_scheduled, Toast.LENGTH_LONG)
.show();
finish();
}
}
On Android 3.1+, we also need this activity to move our application out of
the stopped state and allow that boot-time BroadcastReceiver
to work.
If you run this app on a device or emulator, after seeing the Toast
, messages
will appear in LogCat every five seconds, even though you have no activity
running.
The sample shown above works… most of the time.
However, it has a flaw: the device might fall asleep before our service can complete its work, if we woke it up out of sleep mode to process the event.
To understand where this flaw would appear, and to learn how to address it, we need to think a bit more about the event flows and timing of the code we are executing.
For a _WAKEUP
-style alarm, Android makes precisely one guarantee: if
the PendingIntent
supplied to AlarmManager
for the alarm is one created
by getBroadcast()
to send a broadcast Intent
, Android will ensure that
the device will stay awake long enough for onReceive()
to be completed.
Anything beyond that is not guaranteed.
In the sample shown above, we are not using getBroadcast()
. We are taking
the more straightforward approach of sending the command directly to the service
via a getService()
PendingIntent
. Hence, Android makes no guarantees about
what happens after AlarmManager
wakes up the device, and the device could
fall back asleep before our IntentService
completes processing of
onHandleIntent()
.
For our trivial sample, where we are merely logging to LogCat, we could simply
move that logic out of an IntentService
and into a BroadcastReceiver
. Then,
Android would ensure that the device would stay awake long enough for us to
do our work in onReceive()
.
The problem is that onReceive()
is called on the main application thread, so
we cannot spend much time in that method. And, since our alarm event might
occur when nothing else of our code is running, we need to have our
BroadcastReceiver
registered in the manifest, rather than via registerReceiver()
.
A side effect of this is that we cannot fork threads or do other things
in onReceive()
that might live past onReceive()
yet be “owned” by the
BroadcastReceiver
itself. Besides, Android only ensures that the device will
stay awake until onReceive()
returns, so even if we did fork a thread, the
device might fall asleep before that thread can complete its work.
Enter the WakefulIntentService
.
WakefulIntentService
is a reusable component, published by the author of
this book.
WakefulIntentService
allows you to implement “the handoff pattern”:
WakefulIntentService
to do your background
work, putting that business logic in a doWakefulWork()
method instead of
onHandleIntent()
(though it is still called on a background thread)BroadcastReceiver
of your designBroadcastReceiver
calls sendWakefulWork()
on the WakefulIntentService
class, identifying your own subclass of WakefulIntentService
WAKE_LOCK
permission to your manifestWakefulIntentService
will perform a bit of magic to ensure that the device
will stay awake long enough for your work to complete in doWakefulWork()
.
Hence, we get the best of both worlds: the device will not fall asleep, and
we will not have to worry about tying up the main application thread.
Android Studio users can add a reference to the CommonsWare Maven artifact
repository in their top-level repositories
closure:
repositories {
mavenCentral()
maven {
url "https://repo.commonsware.com.s3.amazonaws.com"
}
}
(here shown alongside an existing mavenCentral()
statement)
Then, adding the WakefulIntentService
is merely a matter of adding a
compile 'com.commonsware.cwac:wakeful:...'
statement to the top-level
dependencies
closure (for some version of the library, denoted by ...
).
WakefulIntentService
is open source, licensed under the Apache License 2.0.
With that in mind, take a peek at the
AlarmManager/Wakeful
sample project. This is a near-clone of the previous sample, with the
primary difference being that we will use WakefulIntentService
.
Android Studio users will pull the AAR from the CommonsWare artifact repository:
repositories {
maven {
url "https://s3.amazonaws.com/repo.commonsware.com"
}
}
dependencies {
compile 'com.commonsware.cwac:wakeful:1.0.+'
}
Our manifest includes the WAKE_LOCK
permission:
<uses-permission android:name="android.permission.WAKE_LOCK"/>
Our PollReceiver
will now serve two roles: handling ACTION_BOOT_COMPLETED
and handling our alarm events. We can detect which of these cases triggered
onReceive()
by inspecting the broadcast Intent
, passed into onReceive()
.
We will use an explicit Intent
for the alarm events, so any Intent
with
an action string must be ACTION_BOOT_COMPLETED
:
package com.commonsware.android.wakesvc;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.SystemClock;
import com.commonsware.cwac.wakeful.WakefulIntentService;
public class PollReceiver extends BroadcastReceiver {
private static final int PERIOD=900000; // 15 minutes
private static final int INITIAL_DELAY=5000; // 5 seconds
@Override
public void onReceive(Context ctxt, Intent i) {
if (i.getAction() == null) {
WakefulIntentService.sendWakefulWork(ctxt, ScheduledService.class);
}
else {
scheduleAlarms(ctxt);
}
}
static void scheduleAlarms(Context ctxt) {
AlarmManager mgr=
(AlarmManager)ctxt.getSystemService(Context.ALARM_SERVICE);
Intent i=new Intent(ctxt, PollReceiver.class);
PendingIntent pi=PendingIntent.getBroadcast(ctxt, 0, i, 0);
mgr.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime() + INITIAL_DELAY,
PERIOD, pi);
}
}
If the Intent
is our explicit Intent
, we call sendWakefulWork()
on
WakefulIntentService
, identifying our ScheduledService
class as being
the service that contains our business logic.
The other changes to PollReceiver
is that we use getBroadcast()
to
create our PendingIntent
, wrapping our explicit Intent
identifying
PollReceiver
itself, and that we use more realistic polling periods
(5 second initial delay, every 15 minutes thereafter).
ScheduledService
has only two changes: it extends WakefulIntentService
and has the LogCat logging in doWakefulWork()
:
package com.commonsware.android.wakesvc;
import android.content.Intent;
import android.util.Log;
import com.commonsware.cwac.wakeful.WakefulIntentService;
public class ScheduledService extends WakefulIntentService {
public ScheduledService() {
super("ScheduledService");
}
@Override
protected void doWakefulWork(Intent intent) {
Log.d(getClass().getSimpleName(), "I ran!");
}
}
A WakefulIntentService
keeps the device awake by using a WakeLock
. A
WakeLock
allows a “userland” (e.g., Android SDK) app to tell the Linux
kernel at the heart of Android to keep the device awake, with the CPU powered
on, indefinitely, until the WakeLock
is released.
This can be a wee bit dangerous, as you can accidentally keep the device
awake much longer than you need to. That is why using a library like
WakefulIntentService
can be useful — to use more-tested code rather than
rolling your own.
Some Android devices take liberties with the way AlarmManager
works, in ways
that may affect your applications.
One example of this today is the SONY Xperia Z. It has a “STAMINA mode” that the user can toggle on via the “Power Management” screen in Settings. This mode will be entered when the device’s screen turns off, if the device is not plugged in and charging. The user can add apps to a whitelist (“Apps active in standby”), where STAMINA mode does not affect those apps’ behavior.
_WAKEUP
style alarms do not wake up the device when it
is in STAMINA mode. The behavior is a bit reminiscent of non-_WAKEUP
alarms. Alarms that occur while the device is asleep are suppressed,
and you get one invocation of your PendingIntent
at the point
the device wakes back up. At that point, the schedule continues as
though the alarms had been going off all along. Apps on the whitelist
are unaffected.
Mostly, you need to be aware of this from a support standpoint. If Xperia Z owners complain that your app behaves oddly, and you determine that your alarms are not going off, see if they have STAMINA mode on, and if they do, ask them to add your app to the whitelist.
If you are using “if my alarm has not gone off in X amount of time, the user perhaps force-stopped me, so let me reschedule my alarms” logic, you should be OK. Before one of your activities gets a chance to make that check, your post-wakeup alarm should have been invoked, so you can update your event log and last-run timestamp. Hence, you should not be tripped up by STAMINA and accidentally reschedule your alarms (potentially causing duplicates, depending upon your alarm-scheduling logic).
Other devices with similar characteristics include Sony’s Xperia P, Xperia U, Xperia sola, and Xperia go.
If you are encountering issues with your alarms, the first thing to do is
to ensure that the alarm schedule in AlarmManager
is what you expect it
to be. To do that, run adb shell dumpsys alarm
from a command prompt.
This will dump a report of all the scheduled alarms, including when they are
set to be invoked next (with portions replaced by vertical ellipses to keep
this listing from being too long):
Current Alarm Manager state:
Realtime wakeup (now=2013-03-09 07:49:51):
RTC_WAKEUP #11: Alarm{429c6028 type 0 com.android.providers.calendar}
type=0 when=+21h40m9s528ms repeatInterval=0 count=0
operation=PendingIntent{42ec2f40: PendingIntentRecord{434fb2f8 com.android.providers.calendar broadcastIntent}}
RTC_WAKEUP #10: Alarm{42e17e28 type 0 com.google.android.gms}
type=0 when=+18h10m8s480ms repeatInterval=86400000 count=1
operation=PendingIntent{42e15d20: PendingIntentRecord{42e0cc28 com.google.android.gms startService}}
.
.
.
Elapsed realtime wakeup (now=+6d15h50m2s672ms):
ELAPSED_WAKEUP #16: Alarm{42cf26f0 type 2 com.google.android.apps.maps}
type=2 when=+999d23h59m59s999ms repeatInterval=0 count=0
operation=PendingIntent{42de2dc0: PendingIntentRecord{42ac73e8 com.google.android.apps.maps broadcastIntent}}
ELAPSED_WAKEUP #15: Alarm{42c4a638 type 2 com.google.android.apps.maps}
type=2 when=+1d18h10m8s894ms repeatInterval=0 count=0
operation=PendingIntent{42ab50c8: PendingIntentRecord{42e2c020 com.google.android.apps.maps broadcastIntent}}
.
.
.
Broadcast ref count: 0
Top Alarms:
+14m24s97ms running, 0 wakeups, 9567 alarms: android
act=android.intent.action.TIME_TICK
+1m15s72ms running, 4890 wakeups, 4890 alarms: com.android.phone
act=com.android.server.sip.SipWakeupTimer@42626830
+1m13s465ms running, 0 wakeups, 320 alarms: android
act=com.android.server.action.NETWORK_STATS_POLL
+45s803ms running, 0 wakeups, 639 alarms: com.google.android.deskclock
act=com.android.deskclock.ON_QUARTER_HOUR
+42s830ms running, 0 wakeups, 19 alarms: com.android.phone
act=com.android.phone.UPDATE_CALLER_INFO_CACHE cmp={com.android.phone/com.android.phone.CallerInfoCacheUpdateReceiver}
+35s479ms running, 0 wakeups, 954 alarms: android
act=com.android.server.ThrottleManager.action.POLL
+14s28ms running, 1609 wakeups, 1609 alarms: com.android.phone
act=com.android.internal.telephony.gprs-data-stall
+11s98ms running, 171 wakeups, 171 alarms: com.android.providers.calendar
act=com.android.providers.calendar.intent.CalendarProvider2
+8s380ms running, 893 wakeups, 893 alarms: android
act=android.content.syncmanager.SYNC_ALARM
+8s353ms running, 569 wakeups, 569 alarms: com.google.android.apps.maps
cmp={com.google.android.apps.maps/com.google.googlenav.prefetch.android.PrefetcherService}
Alarm Stats:
com.google.android.location +120ms running, 12 wakeups:
+73ms 7 wakes 7 alarms: act=com.google.android.location.nlp.ALARM_WAKEUP_CACHE_UPDATER
+47ms 5 wakes 5 alarms: act=com.google.android.location.nlp.ALARM_WAKEUP_LOCATOR
android +15m32s920ms running, 1347 wakeups:
+14m24s97ms 0 wakes 9567 alarms: act=android.intent.action.TIME_TICK
+1m13s465ms 0 wakes 320 alarms: act=com.android.server.action.NETWORK_STATS_POLL
+35s479ms 0 wakes 954 alarms: act=com.android.server.ThrottleManager.action.POLL
+8s380ms 893 wakes 893 alarms: act=android.content.syncmanager.SYNC_ALARM
+7s734ms 159 wakes 159 alarms: act=android.appwidget.action.APPWIDGET_UPDATE cmp={com.guywmustang.silentwidget/com.guywmustang.silentwidgetlib.SilentWidgetProvider}
+1s144ms 151 wakes 151 alarms: act=android.app.backup.intent.RUN
+922ms 0 wakes 6 alarms: act=android.intent.action.DATE_CHANGED
+479ms 66 wakes 66 alarms: act=com.android.server.WifiManager.action.DEVICE_IDLE
+383ms 56 wakes 56 alarms: act=com.android.server.WifiManager.action.DELAYED_DRIVER_STOP
+101ms 14 wakes 14 alarms: act=com.android.server.action.UPDATE_TWILIGHT_STATE
+100ms 7 wakes 7 alarms: act=com.android.internal.policy.impl.PhoneWindowManager.DELAYED_KEYGUARD
+9ms 1 wakes 1 alarms: act=android.net.wifi.DHCP_RENEW
+3ms 0 wakes 1 alarms: act=com.android.server.NetworkTimeUpdateService.action.POLL
com.google.android.apps.maps +14s742ms running, 911 wakeups:
+8s353ms 569 wakes 569 alarms: cmp={com.google.android.apps.maps/com.google.googlenav.prefetch.android.PrefetcherService}
+2s211ms 85 wakes 85 alarms: act=com.google.android.apps.maps.nlp.ALARM_WAKEUP_LOCATOR
+1s206ms 103 wakes 103 alarms: act=com.google.android.apps.maps.nlp.ALARM_WAKEUP_SENSOR_UPLOADER
+807ms 2 wakes 2 alarms: act=com.google.android.apps.maps.nlp.ALARM_WAKEUP_BURST_COLLECTION_TRIGGER
+759ms 56 wakes 56 alarms: act=com.google.android.apps.maps.nlp.ALARM_WAKEUP_S_COLLECTOR
+566ms 10 wakes 10 alarms: act=com.google.android.apps.maps.nlp.ALARM_WAKEUP_CACHE_UPDATER
+385ms 39 wakes 39 alarms: act=com.google.android.apps.maps.nlp.ALARM_WAKEUP_IN_OUT_DOOR_COLLECTOR
+308ms 31 wakes 31 alarms: act=com.google.android.apps.maps.nlp.ALARM_WAKEUP_ACTIVE_COLLECTOR
+77ms 8 wakes 8 alarms: act=com.google.android.apps.maps.nlp.ALARM_WAKEUP_ACTIVITY_DETECTION
+42ms 4 wakes 4 alarms: act=com.google.android.apps.maps.nlp.ALARM_WAKEUP_PASSIVE_COLLECTOR
+28ms 4 wakes 4 alarms: act=com.google.android.apps.maps.nlp.ALARM_WAKEUP_CALIBRATION_COLLECTOR
.
.
.
You are given details of each outstanding alarm, including the all-important
when
value indicating the time the alarm should be invoked next, if it is not
canceled first (e.g., when=+5d15h10m7s782ms
), along with the package requesting
the alarm. You can use this to identify your app’s alarms and see when they should
be invoked next.
You are also given:
Note, though, that for inexact alarms, the when
value may not indicate when the
event will actually occur.
The Android Support package has added a WakefulBroadcastReceiver
, which
offers an alternative to WakefulIntentService
for arranging to do work,
triggered by a broadcast (such as an AlarmManager
event), that may take
a while. WakefulBroadcastReceiver
has its pros and cons compared to
WakefulIntentService
, making it worth considering.
Using WakefulBroadcastReceiver
with AlarmManager
is slightly different
than is using WakefulIntentService
. The
AlarmManager/WakeCast
sample project is a clone of the WakefulIntentService
project, but one
using WakefulBroadcastReceiver
instead.
The activity is unchanged — it simply calls scheduleAlarms()
on
PollReceiver
. scheduleAlarms()
itself is unchanged, as it still uses
setRepeating()
on AlarmManager
to arrange to periodically invoke
a PendingIntent
, targeting the PollReceiver
.
But PollReceiver
itself is now a WakefulBroadcastReceiver
rather than
just an ordinary BroadcastReceiver
. This in turn requires a slightly different
implementation of onReceive()
:
package com.commonsware.android.wakecast;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.SystemClock;
import android.support.v4.content.WakefulBroadcastReceiver;
public class PollReceiver extends WakefulBroadcastReceiver {
private static final int PERIOD=900000; // 15 minutes
private static final int INITIAL_DELAY=5000; // 5 seconds
@Override
public void onReceive(Context ctxt, Intent i) {
if (i.getAction() == null) {
startWakefulService(ctxt,
new Intent(ctxt, ScheduledService.class));
}
else {
scheduleAlarms(ctxt);
}
}
static void scheduleAlarms(Context ctxt) {
AlarmManager mgr=
(AlarmManager)ctxt.getSystemService(Context.ALARM_SERVICE);
Intent i=new Intent(ctxt, PollReceiver.class);
PendingIntent pi=PendingIntent.getBroadcast(ctxt, 0, i, 0);
mgr.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime() + INITIAL_DELAY,
PERIOD, pi);
}
}
Now, when the AlarmManager
broadcast arrives, we call startWakefulService()
,
passing it the Context
supplied to onReceive()
, plus an Intent
identifying
the service to start up. Under the covers, this works much like sendWakefulWork()
on WakefulIntentService
— it starts the identified service, but acquires
a WakeLock
first.
Our ScheduledService
is now a regular IntentService
, instead of a
WakefulIntentService
. This means that our background work moves back to
the standard onHandleIntent()
method, instead of doWakefulWork()
. However,
we have one extra bit of bookkeeping to do: we must call the static
completeWakefulIntent()
method on WakefulBroadcastReceiver
(or, as shown,
on PollReceiver
, as that will point to the same static method):
package com.commonsware.android.wakecast;
import android.app.IntentService;
import android.content.Intent;
import android.util.Log;
public class ScheduledService extends IntentService {
public ScheduledService() {
super("ScheduledService");
}
@Override
protected void onHandleIntent(Intent intent) {
Log.d(getClass().getSimpleName(), "I ran!");
PollReceiver.completeWakefulIntent(intent);
}
}
We pass the Intent
supplied to onHandleIntent()
to completeWakefulIntent()
.
Behind the scenes, completeWakefulIntent()
will release the WakeLock
that
has been keeping our CPU powered on while we do our work.
One might think that WakefulIntentService
would now be obsolete with
the addition of WakefulBroadcastReceiver
. In truth, there are some
advantages to the current implementation of WakefulBroadcastReceiver
:
WakeLock
, one set to auto-release after one
minute, so there is no risk of an app somehow failing to release the
lock and thereby keeping the CPU on indefinitely.WakefulBroadcastReceiver
uses
one WakeLock
per request, rather than the single static WakeLock
that WakefulIntentService
uses, making WakefulBroadcastReceiver
incrementally more resilient in the face of various potential problems.IntentService
,
WakefulBroadcastReceiver
may offer greater flexibility. For example, an
IntentService
is not a good choice if the work you do is intrinsically
asynchronous, such as trying to find the device’s location. Any place
where you find yourself registering a listener from a service, an IntentService
will not work well, as the IntentService
wants to shut down before
your listener has received a result. A regular Service
can work well,
though, in this case, and WakefulBroadcastReceiver
might be of use in
this pattern (though the author has not tried this yet).On the other hand:
WakefulBroadcastReceiver
requires an explicit call to
completeWakefulIntent()
, which a developer can easily forget,
possibly causing the WakeLock
to be leaked. While this is not
disastrous, since the WakeLock
will auto-release after a minute, it
may still represent wasted power. WakefulIntentService
is more
“idiot-proof” and therefore avoids this issue.WakefulBroadcastReceiver
WakeLock
is locked to
being one minute — no more, no less. This offers limited flexibility
and can cause problems if the work you intend to do could easily exceed
a minute. Unfortunately, the implementation of WakefulBroadcastReceiver
offers no easy way to override this one-minute timeout value.WakeLock
, as Android
will be starting the service directly, not via WakefulBroadcastReceiver
.
WakefulIntentService
will suffer the same fate, but it will automatically
grab a WakeLock
for you when it detects this condition. In the case of
WakefulBroadcastReceiver
, your service will run without a WakeLock
, unless
you detect this case yourself (via a custom onStartCommand()
that examines
the passed-in flags
, looking for START_FLAG_REDELIVERY
) and grab your
own WakeLock
.A future generation of WakefulIntentService
will aim to adopt some
of the advantages of WakefulBroadcastReceiver
while avoiding its
disadvantages. As it stands, either component is a reasonable choice
if you are willing to live within their respective constraints.
Android 6.0 introduced some changes to the behavior of AlarmManager
that significantly affect its use on Android 6.0+ devices. These changes
also affect JobScheduler
, and so this topic is covered in grand detail
at the end of the JobScheduler
chapter.
Android 7.0 introduced a curious variant of the existing set()
,
setExact()
, and setWindow()
methods. Rather than taking a PendingIntent
,
they take an implementation of OnAlarmListener
. That listener’s
onAlarm()
method then gets called when the alarm is scheduled to go
off.
These methods are only useful if the app has a process running and that
process is likely to be running when the time for the alarm is to
occur. If the app’s process is terminated after the OnAlarmListener
is registered, the alarms are canceled, as the OnAlarmListener
no
longer exists.
For RTC
and ELAPSED_REALTIME
alarms, it is unclear what value there
is in these AlarmManager
methods over using some other in-process
timing mechanism, such as Java’s ScheduledExecutorService
.
However, for RTC_WAKEUP
and ELAPSED_REALTIME_WAKEUP
alarms, the
new OnAlarmListener
methods may be useful, if you expect the device
to be asleep but the process still running, and you want to get control
to go do something. However, they still only make sense if you only
want to get control if you already have a process running, and if
your process goes away you do not mind the alarms going away.
There may be a set of apps that could use this. The author cannot quite figure out what such an app would be.
To illustrate the use of OnAlarmListener
, we can turn to the
AlarmManager/Listener
sample app. This is reminiscent of the AlarmManager/Simple
app shown
in the beginning of this chapter, where we want an activity to get
control every five seconds to show a Toast
However, in this case,
rather than use setRepeating()
and createPendingResult()
, we will
use setWindow()
and OnAlarmListener
:
package com.commonsware.android.alarm;
import android.app.Activity;
import android.app.AlarmManager;
import android.os.Bundle;
import android.os.SystemClock;
import android.util.Log;
import android.widget.Toast;
public class SimpleAlarmDemoActivity extends Activity
implements AlarmManager.OnAlarmListener {
private static final int PERIOD=5000;
private static final int WINDOW=10000;
private AlarmManager mgr=null;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
mgr=getSystemService(AlarmManager.class);
schedule();
}
@Override
public void onDestroy() {
mgr.cancel(this);
super.onDestroy();
}
@Override
public void onAlarm() {
Toast.makeText(this, R.string.toast, Toast.LENGTH_SHORT).show();
Log.d(getClass().getSimpleName(), "I ran!");
schedule();
}
private void schedule() {
mgr.setWindow(AlarmManager.ELAPSED_REALTIME,
SystemClock.elapsedRealtime()+PERIOD, WINDOW,
getClass().getSimpleName(), this, null);
}
}
onCreate()
gets an AlarmManager
as before. However, since this
version of the app has a minSdkVersion
of 24, we can use
the version of getSystemService()
that takes the desired system service
Class
object as a parameter and returns the system service instance
in the proper data type.
Then, onCreate()
calls schedule()
, which in turn calls setWindow()
.
The version of setWindow()
that we are using takes:
ELAPSED_REALTIME
)PERIOD
milliseconds from now)String
representing some tag to be used for battery usage logging
purposesOnAlarmListener
implementation, which in this case happens to be
the activity itselfnull
, indicating that we want onAlarm()
of the OnAlarmListener
to be called on the main application threadThe alternative to null
for the latter parameter would be a Handler
from a HandlerThread
, indicating that onAlarm()
should be called
on that thread.
Eventually, onAlarm()
is called, where we show a Toast
, log a message
to LogCat,… and call schedule()
again, so our alarm repeats.
Later, when the activity is destroyed, we call cancel()
, passing in
our OnAlarmListener
, so all alarms tied to that listener will be discontinued.