Android is getting mature with every major release and we are heading towards more user-centric and privacy first Operating System.

Android Oreo was released back in 2018, one of the major changes were related to background limitations which changed the way most apps would function. It was one of the turning points for android, as a lot of apps were running in the background even without users knowing about it and this also drained a lot of battery, to prevent that from happening to a certain extent the android team came up with this major change.

As welcoming as it was for users, a lot of developers were affected by this change, with the release of Android R some of the developers still run into issues with AlarmManager API as it doesn’t work the way it was working in previous Android versions (below oreo).

In this article, we’ll cover

  1. Basics of AlarmManager and its working
  2. BroadcastReceiver’s role when using AlarmManager
  3. Changes/requirements to make it work seamlessly in Android Oreo and above.

Basics of AlarmManager

AlarmManager is an API that is exposed by the Android framework to schedule your app to run at a specific point in the future.

In simple words, if you want some work (showing notification, making an API call to your servers) in the future irrespective of the fact that if the app is running in the foreground, you can use AlarmManager API.

We’ll use AlarmManagerCompat which is a wrapper class over the existing AlarmManager class to make our life easier.

To set up an alarm using AlarmManagerCompat API (specifically the functions which allow you to set alarm to a very specific time) we require an instance of AlarmManager class, the UTC at which alarm should be triggered, the type of alarm and the pending intent which will fire when its time.

Getting an instance of AlarmManager is fairly simple

val alarmManager = context.getSystemService<AlarmManager>() // Using KTX extensions library

Getting UTC time is based on the logic of your app.

In this example, I am setting it 10 minutes after the current time.

val tenMinsFromNow = System.currentTimeMillis() + (10 * 60 * 1000)

Alarm types are

  1. RTC
  2. RTC_WAKEUP
  3. ELAPSED_REALTIME
  4. ELAPSED_REALTIME_WAKEUP

You can learn when to use which type here in section Alarm types.

PendingIntent

While creating pending intent we’ll use PendingIntent.getBroadcast() function.

// Make sure to put a class which extends BroadcastReceiver as second parameter for Intent
val intent = Intent(context.applicationContext, ExampleBroadcastReceiver::class.java).apply {
                putExtra("id", id) // Any data that you want to pass with intent
                putExtra("name", name)
            }

val pIntent = PendingIntent.getBroadcast(context.applicationContext, id, intent, PendingIntent.FLAG_UPDATE_CURRENT)

Next we set the alarm

AlarmManagerCompat.setExactAndAllowWhileIdle(alarmManager, AlarmManager.RTC_WAKEUP, tenMinsFromNow, pIntent)

Now the functions with setExactXxxx ironically aren’t as exact as we want them to be on Android Oreo and above.

BroadcastReceiver’s role when using AlarmManager

When we use AlarmManager, we pass a PendingIntent which gets fired by the OS when the alarm is triggered in other words when our device’s clock hits the time we set in our alarm request.

Now, our PendingIntent’s Intent, in which we mentioned a BroadcastReceiver class, that class’s onReceive method is called automatically with the same Intent which we passed.

Making AlarmManager work like before

Now if we look into the reason why our AlarmManager is not working the way we expect it to is because of the Battery optimizations set by the android framework and OEMs

There are at least 2 ways we can make it work.

  1. Foreground Service for registering the BroadcastReceiver for our Alarms
  2. Request user to whitelist your app from framework’s Battery Optimization

ForegroundService

It may not be ideal for some apps as it will show a persistent notification in the Notification panel, but it’s a good thing that Android Oreo and above, allow users to hide notifications if they want and it won’t affect the functioning of the app.

While using foreground service we will use its `onStartCommand` method to register our receiver and if due to some reason our service is killed we can do the cleanup in `onDestroy` method.

class ExampleForegroundService() : Service() {
    
    private val exampleReceiver by lazy { ExampleBroadcastReceiver() }
    
    
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        // Create notification
        val notification = NotificationCompat.Builder(this, getString(R.string.channel_desc)).apply {
            // Configure your notification
        }.build()
        
        // Call startForeground service with a ID and notification object
        startForeground(Constants.KEEP_ALIVE_NOTIFICATION_CODE, notification)
        
        // Register the Broadcast Receiver
        registerReceiver(exampleReceiver, IntentFilter())
        
        // Return value
        return START_NOT_STICKY
    }
    
    override fun onDestroy() {
        // Unregister the Broadcast Receiver
        unregisterReceiver(exampleReceiver)
        super.onDestroy()
    }
}

Starting a foreground service is fairly simple.

ContextCompat.startForegroundService(applicationContext, Intent(applicationContext, ExampleForegroundService::class.java))

Using this method we can ensure that our broadcast receiver is always listening.

Whitelisting our android app from Battery Optimization

If you don’t want your app to show a persistent notification, they can use this method but with a lot of caution, this workaround can easily be misused and if your app is doing a lot of heavy lifting in the background including this alarm manager, well then this may not be the ideal for your app.

Before you continue reading further, I would heavily recommend that you read this.

To whitelist our app we need to take the user to battery optimization settings and ask the user to select our app and set its status to “Not optimized” or something along the same lines as this can vary based on android version and device manufacturer.

Note: Be mindful that this is sometimes removed or changed by certain OEMs like “Huawei”, if you execute the Intent given below on a Huawei device you’ll most likely get an ActivityNotFoundException

Use this method at your own risk and test it on multiple devices before you ship it in production.

Always make sure to tell users the reason why you need them to whitelist the app with the steps they might have to follow to complete the process, through a dialog or a note, then if the user accepts/agrees to it, only then fire the intent and wait for users to do its thing.

You can always verify if your app is whitelisted or not using the following piece of code.

private fun isAppBlacklisted(): Boolean {
        val pwrm = requireContext().getSystemService(Context.POWER_SERVICE) as PowerManager
        val name = requireContext().packageName
        return !pwrm.isIgnoringBatteryOptimizations(name)
}

The intent which takes the user to battery optimization settings

startActivity(Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS))

My suggestion would be to only use this method as a last resort.

I hope this article was informative and helped you learn something new, feel free to share your feedback or queries.