Background Services in Android Oreo

Or should I say: How to bypass Background Services restrictions in Android O.

If you are like me and have an Android app that relies on a background service, you probably already discover that if you target Android O you cannot start a background service anymore.

I needed my service to run every X minutes, even when the app was in the background (or even not running). Lets be clear, I’m not doing any heavy work on it, but periodicity was important, and that is why I couldn’t trust using a Job. Trust me, I tried GCM & Firebase, and I found times where the Job wasn’t fired for hours. Also, running a Foreground Service for a recurring task it’s not a good idea; almost any user will feel uncomfortably with an ongoing notification saying that your app is ‘always running’ and Android telling you (with another ongoing notification) that app ‘x’ is using the battery, even if the service is not doing any job (Hello new version of Fitbit app)

So, after trying many methods and approaches, I started testing with my old friends AlarmManager and BroadcastReceiver, and this was the only thing that worked for me. (UPDATE: This is almost true, I still find gaps where my task is not fired, but, normally it works pretty well)

Basically what I’m doing is to set the AlarmManager to fire a Broadcast every X minutes. I configured the BroadcastReceiver in my AndroidManifest and inside the onReceive method I just fire a JobIntentService to do the work.

Lets do some code

First, declare the Receiver on the AndroidManifest.xml

<receiver android:name=".AlarmReceiver">
<intent-filter>
<action android:name="com.test.intent.action.ALARM" />
</intent-filter>
</receiver>

Second, extend JobIntentService and write your ‘background’ code

public class MyJobIntentService extends JobIntentService {

/* Give the Job a Unique Id */
private static final int JOB_ID = 1000;
    public static void enqueueWork(Context ctx, Intent intent) {
enqueueWork(ctx, MyJobIntentService.class, JOB_ID, intent);
}

@Override
protected void onHandleWork(@NonNull Intent intent) {
/* your code here */
        /* reset the alarm */
AlarmReceiver.setAlarm(false);
stopSelf();
}

}

Third, extends BroadcastReceiver

public class AlarmReceiver extends BroadcastReceiver {

public static final String CUSTOM_INTENT = "com.test.intent.action.ALARM";


@Override
public void onReceive(Context context, Intent intent) {
/* enqueue the job */
MyJobIntentService.enqueueWork(context, intent);
}
    public static void cancelAlarm() {
AlarmManager alarm = (AlarmManager) ctx.getSystemService(Context.ALARM_SERVICE);

/* cancel any pending alarm */
alarms.cancel(getPendingIntent());
}
    public static void setAlarm(boolean force) {
cancelAlarm();
AlarmManager alarm = (AlarmManager) ctx.getSystemService(Context.ALARM_SERVICE);
        // EVERY X MINUTES
long delay = (1000 * 60 * X);
long when = System.currentTimeMillis();
if (!force) {
when += delay;
}

/* fire the broadcast */
alarms.set(AlarmManager.RTC_WAKEUP, when, getPendingIntent());
}
    private static PendingIntent getPendingIntent() {
Context ctx; /* get the application context */
Intent alarmIntent = new Intent(ctx, AlarmReceiver.class);
alarmIntent.setAction(CUSTOM_INTENT);

return PendingIntent.getBroadcast(ctx, 0, alarmIntent, PendingIntent.FLAG_CANCEL_CURRENT);
}
}

UPDATE: Thanks to @paolop I read more about Doze mode, and even that I couldn’t make the alarm to fire on Doze mode, I think the code for setting the alarm could be updated to

int SDK_INT = Build.VERSION.SDK_INT;
if (SDK_INT < Build.VERSION_CODES.KITKAT)
alarmManager.set(AlarmManager.RTC_WAKEUP, when, pendingIntent);
else if (Build.VERSION_CODES.KITKAT <= SDK_INT && SDK_INT < Build.VERSION_CODES.M)
alarmManager.setExact(AlarmManager.RTC_WAKEUP, when, pendingIntent);
else if (SDK_INT >= Build.VERSION_CODES.M) {
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, when, pendingIntent);
}

That’s it! What you need to do to start this flow is to call

AlarmReceiver.setAlarm(true); /* true will force the broadcast */

This will set an alarm that will broadcast the ‘alarmIntent’ (you can add data to this intent that you will receive in ‘onHandleWork’ from your Job.

The job will recreate the alarm (I chose this path so the job will run ‘X’ minutes after finishing its task)

If you need to cancel the alarm, just call

AlarmReceiver.cancelAlarm();

Remember that Android cancels all the alarms when a device is rebooted. You can follow this to reset your alarm when the device boots:

https://developer.android.com/training/scheduling/alarms.html#boot

Another thing to be aware of if that if the user ‘Force Close’ you app for any reason, then your alarm will not fire anymore until you call setAlarm again.