As you probably
know, starting from API level 21 (Lollipo) the best way to schedule
repeated tasks on Android is using:
- the JobScheduler API (requires API level 21+);
- the Firebase JobDispatcher, which provides a JobScheduler-compatible API that works on all recent versions of Android (API level 9+) that have Google Play services installed.
Running apps in the
background is expensive,
which is especially harmful when they're not actively doing work
that's important to the user.
In recognition of
these issues, the Android framework team created the JobScheduler.
This provides developers a simple way of specifying runtime
constraints on their jobs. Available constraints include network
type, charging state, and idle state.
On the other hand,
the Firebase JobDispatcher
uses the scheduling engine inside Google Play services(formerly the
GCM Network Manager component) to provide a backwards compatible
(back to Gingerbread) JobScheduler-like API.
Problem
However,
both the JobScheduler API and the Firebase JobDispatcher, in order to
save battery life, depend on the mainance
windows of Doze.
In
other words, if
a user leaves a device unplugged and stationary for a period of time,
with the screen off, the device enters Doze mode. In Doze mode, the
system attempts to conserve battery by restricting apps' access to
network and CPU-intensive services. It also prevents apps from
accessing the network and
defers their jobs,
syncs, and standard alarms.Periodically,
the system exits Doze for a brief time to let apps complete their
deferred activities. During thismaintenance
window,
the system runs all pending syncs, jobs, and alarms, and lets apps
access the network.
Doze - mantainance windows |
Warning
In this post we'll focus on scheduling repeated tasks (even offline, whithout an internet connection) that should start at (almost) exact times.
AlarmManager
Although the
aforementioned solution is preferable, because it enables to save
battery life, there might be scenarios in which you want to schedule
a repetead task to run with an exact timing, even
when the device is in Doze mode.
A possible solution is
using the AlarmManager. The exact methods and the pattern to follow
varies depending on the Android version, because:
- Doze was first introduced with Marshmallow (API level 23);
- the AlarmManager methods for scheduling tasks have slightly changed with API leve 19.
The following table shows
the methods to use:
repeated
exact
times
display ON
|
repeated
exact
times
display OFF
|
|
up to API 18
|
setRepeating
RTC or
ELAPSED_REALTIME
(1)
|
setRepeating
RTC_WAKEUP or
ELAPSED_REALTIME_WAKEUP
|
API 19-22
|
setExact with manual
re-scheduling
RTC
ELAPSED_REALTIME
(1) (2)
|
setExact
with manual re-scheduling
RTC_WAKEUP
ELAPSED_REALTIME_WAKEUP
|
API 23+
|
setExact or
setExactAndAllowWhileIdle with manual
re-scheduling
(3)
|
setExactAndAllowWhileIdle
with manual re-scheduling
(3)
|
(1) If an alarm is
delayed (by system sleep, for example, for non _WAKEUP alarm types),
a skipped repeat will be delivered as soon as possible. After that,
future alarms will be delivered according to the original schedule;
they do not drift over time. For example, if you have set a recurring
alarm for the top of every hour but the phone was asleep from 7:45
until 8:45, an alarm will be sent as soon as the phone awakens, then
the next alarm will be sent at 9:00.
(2) as of API 19,
all repeating alarms are inexact. If your application needs precise
delivery times then it must use one-time exact alarms, rescheduling
each time. Legacy applications whose targetSdkVersion is earlier than
API 19 will continue to have all of their alarms, including repeating
alarms, treated as exact. With setExact the alarm will be delivered
as nearly as possible
to the requested trigger time.
(3) If you don't
need exact scheduling of the alarm but still need to execute while
idle, consider using setAndAllowWhileIdle(int, long, PendingIntent).
Frequency
For events less than 60 seconds apart, alarms aren’t the best choice: use the much more efficient Handler (http://goo.gl/CE9vAw) for frequent work.
setExactAndAllowWhileIdle: exact or not?
The method
setExactAndAllowWhileIdle, used to trigger alarms even when the
device is idle, can significantly impact the power use of the device
when idle (and thus cause significant battery blame to the app
scheduling them), so it should be used
with care.
To reduce abuse,
there are restrictions
on how frequently these alarms will go off for a particular
application:
- Under normal system operation, it will not dispatch these alarms more than about every minute (at which point every such pending alarm is dispatched)
- When in low-power idle modes this duration may be significantly longer, such as 15 minutes
Manual rescheduling
As you can see from the above table, starting with
API 19 you have to use setExact or setExactAndAllowWhileIdle the
schedule a repeated task at exact times.
Infact, as of API 19, all repeating alarms are
inexact. If
your application needs precise delivery times then it must use
one-time exact alarms, rescheduling each time as described below.
Legacy applications whose targetSdkVersion is earlier than API 19
will continue to have all of their alarms, including repeating
alarms, treated as exact.
Create an AlarmManager instance:
AlarmManager alarmManager = (AlarmManager) getSystemService(ALARM_SERVICE);
Create an Intent to broadcast to a BroadcastReceiver:
Intent intent = new Intent(MyActivity.this, MyReceiver.class);
Create a PendingIntent that holds the intent:
PendingIntent pendingIntent = PendingIntent.getBroadcast(MyActivity.this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
Create the alarm:
alarmManager.setExact(AlarmManager.RTC_WAKEUP, initialDelay, pendingIntent);
Reschedule the next alarm in the onReceive method of your BroadcastReceiver using the same code described above.
WakefulBroadcastReceiver
The Alarm Manager holds a CPU
wake lock as long as the alarm
receiver's onReceive() method is executing. This guarantees that the
phone will not sleep until you have finished handling the broadcast.
Once onReceive() returns,
the Alarm Manager releases this wake lock. This means that the phone
will in some cases sleep as soon as your onReceive() method
completes. If your alarm receiver called Context.startService(),
it is possible that the phone will sleep before the requested service
is launched. To prevent this, your BroadcastReceiver and Service will
need to implement a separate wake lock policy to ensure that the
phone continues running until the service becomes available.
An easy way to achieve this is by using a
WakefulBroadcastReceiver.
WakefulBroadcastReceiver is a BroadcastReceiver that receives a
device wakeup event and then passes the work off to a Service, while
ensuring that the device does not go back to sleep during the
transition. This class takes care of
creating and managing a partial wake lock for you; you
must request the WAKE_LOCK permission to use it.
public class MyWakefulBroadcastReceiver extends WakefulBroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// Reschedule the alarm. See the code above.
// Create an Intent to start a Service.
Intent service = new Intent(context, MyService.class);
// Start the service, keeping the device awake while it is launching.
startWakefulService(context, service);
}
}
The service (in this example, an IntentService) does some work. When it is finished, it releases the wake lock by calling completeWakefulIntent(intent). The intent it passes as a parameter is the same intent that the WakefulBroadcastReceiver originally passed in.
public class MyService extends IntentService {
public MyService() {
super("MyService");
}
@Override
protected void onHandleIntent(Intent intent) {
// Do some work here.
// At this point SimpleWakefulReceiver is still holding a wake lock
// for us.
// Note that when using this approach you should be aware that if your
// service gets killed and restarted while in the middle of such work
// (so the Intent gets re-delivered to perform the work again), it will
// at that point no longer be holding a wake lock since we are depending
// on SimpleWakefulReceiver to that for us. If this is a concern, you can
// acquire a separate wake lock here.
// Release the wake lock after executing the task.
SimpleWakefulReceiver.completeWakefulIntent(intent);
}
}