How to make a periodic background work (15 mins periodic interval) to run indefinitely on Oreo and later?

1k views Asked by At

After Doze Mode was introduced in Android 8, Android has enforced many restrictions on background work which makes the app behave in an unintended way.

In one of my apps , I used to fetch Battery Level every 15 mins and fire an alarm if battery level was above the desired battery level set by the user in my app. I used the following code to accomplish this task.

set up an repeating alarm using AlarmManagerApi :

public static void setAlarm(final Context context) {
    final AlarmManager manager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
    if (manager != null) {
        manager.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, 0, ALARM_INTERVAL,
                getAlarmPendingIntent(context));
    }
}

where getAlarmPendingIntent method is as follows:

private static PendingIntent getAlarmPendingIntent(final Context context) {
        return PendingIntent.getBroadcast(context, REQUEST_CODE, new Intent(context, AlarmReceiver.class), PendingIntent.FLAG_UPDATE_CURRENT);
    }

Setup a BroadcastReciever to recieve alarms: It starts a service in the foreground to do the required background work

public class AlarmReceiver extends BroadcastReceiver {

    @Override
    public void onReceive(final Context context, final Intent intent) {
        if (context == null || intent == null) {
            return;
        }

        if (intent.getAction() == null) {
            Intent monitorIntent = new Intent(context, TaskService.class);
            monitorIntent.putExtra(YourService.BATTERY_UPDATE, true);
            ContextCompat.startForegroundService(context, monitorIntent);
        }
    }

}

TaskService(Service doing some desired task ) is as follows

public class TaskService extends Service {
    private static final String CHANNEL_ID = "channel_01";
    private static final int NOTIFICATION_ID = 12345678;

    @Override
    public void onCreate() {
        super.onCreate();
        createNotificationChannel()
    }

    public void createNotificationChannel(){
        // create notification channel for notifications
        NotificationManager mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            CharSequence name = getString(R.string.app_name);
            NotificationChannel mChannel =
                    new NotificationChannel(CHANNEL_ID, name, NotificationManager.IMPORTANCE_DEFAULT);
            if (mNotificationManager != null) {
                mNotificationManager.createNotificationChannel(mChannel);
            }
        }
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        startForeground(NOTIFICATION_ID, getNotification());    
        BatteryCheckAsync batteryCheckAsync = new BatteryCheckAsync();
        batteryCheckAsync.execute();
        return START_STICKY;
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }


    private Notification getNotification() {
        NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID)
                .setContentText(text)
                .setContentTitle("fetching battery level")
                .setOngoing(true)
                .setPriority(Notification.PRIORITY_LOW)
                .setSmallIcon(R.mipmap.ic_launcher)
                .setWhen(System.currentTimeMillis());

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            builder.setChannelId(CHANNEL_ID);
        }

        return builder.build();
    }


    private class BatteryCheckAsync extends AsyncTask<Void, Void, Void> {

        @Override
        protected Void doInBackground(Void... arg0) {
            IntentFilter ifilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
            Intent batteryStatus = YourService.this.registerReceiver(null, ifilter);
            int level = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
            int scale = batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
            Log.i("Battery Charge Level",String.valueOf((level / (float) scale)));
            return null;

        }

        protected void onPostExecute() {
            YourService.this.stopSelf();
        }
    }
}

BatteryCheckAsync task gets battery level from background and shows current battery level in the logs .

Now, if I run the app and swipe the app off my Recent App List and turn off the screen of the device .

The app prints the logs with battery level in android versions before Android 8 . But after android 8 , this stopped working within minutes of swiping the app off my recent app list and turning off the screen of device. The above behaviour was expected as Doze mode was introduced in android and alarms are deferred until next maintenance window .

=================================================================

From the android documentation on https://developer.android.com/training/monitoring-device-state/doze-standby, I found it documented that

Standard AlarmManager alarms (including setExact() and setWindow()) are deferred to the next maintenance window.

If you need to set alarms that fire while in Doze, use setAndAllowWhileIdle() or setExactAndAllowWhileIdle().

Alarms set with setAlarmClock() continue to fire normally — the system exits Doze shortly before those alarms fire.

So, I modified my code as follows now :

public static void setAlarm(final Context context) {
        final AlarmManager manager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
        if (manager == null) {
            return;
        }

        int SDK_INT = Build.VERSION.SDK_INT;
        if (SDK_INT < Build.VERSION_CODES.KITKAT) {
            manager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, ALARM_INTERVAL, getAlarmPendingIntent(context));
        } else if (SDK_INT < Build.VERSION_CODES.M) {
            manager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, ALARM_INTERVAL, getAlarmPendingIntent(context));
        } else {
            manager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, ALARM_INTERVAL, getAlarmPendingIntent(context));
        }
    }

rather than setting a repeating alarm , I set a One time exact alarm and scheduling the next one myself when handling each alarm delivery in the onRecieve method of broadcast reciever by running the above setAlarm() method again.

Now, if I run the app and swipe the app off my Recent App List on OnePlus 3 and turn off the screen of the device , the app prints logs for about 40-45 mins and then suddenly the alarms and foreground service stops running.

Does any one have a solution for guaranteed execution of periodic background task for indefinite period of time in Android Oreo and above? I am Testing it on OnePlus Devices with Android Oreo or above installed.

2

There are 2 answers

1
ares On

You may also take advantage of WorkManager i.e:

 class SampleWorker : Worker(){
     override fun doWork(): Result {
        // do some stuff 
        return Result.SUCCESS
    }
}

and use it like this:

val recurringWork: PeriodicWorkRequest = 
PeriodicWorkRequest.Builder(SampleWorker::class.java, 15, TimeUnit.MINUTES).build()
WorkManager.getInstance()?.enqueue(recurringWork)

for more info visit https://developer.android.com/topic/libraries/architecture/workmanager/basics

2
a23sokolov On

Try to change

public class AlarmReceiver extends WakefulBroadcastReceiver {
    //logic
}

Also change event for alarm Manager alarmManager.set(AlarmManager.RTC_WAKEUP,...)

As a result you will wake up device from doze mode and execute your work.