CategoriesBackground Task

Guide to background Task handling – Part 5 (AlarmManger)

If you have been following this series you may have noticed already we haven’t found any Andoird component that can handle a background task to execute at the exact given time. The closest we come to achieving something like that is by using PeriodicWorkRequestBuilder in WorkManager . Even when using WorkManager that it won’t wake your phone from Doze mode. AlarmManager is the suitable component that can be used to execute a background task in a fixed time in the future.

How does AlarmManager work?

AlarmManager allows developers to schedule tasks that must be executed at a specific time or after a specific interval. It is a system service that runs in the background and handles all the scheduled tasks. When an alarm is triggered, the AlarmManager sends a broadcast intent to the system, which starts the specified service or activity. The AlarmManager Itself is not capable of running a background task but rather capable of triggering a component that is capable of executing a background task. commonly uses Service with AlarmManager for executing background tasks.

One of the most significant advantages of using AlarmManager is that it allows developers to schedule tasks even when the application is not running. The only downside is that AlarmManager will get reset if the device is restarted. But there is a workaround for that which we will discuss in this article.

Coding Example

I have developed a simple example to demonstrate the use of AlarmManager . In this example, the user can set up a time when he/she wants to get a notification to remind them to take medicine. This is the very basic concept of every reminder application. When the correct future time comes a notification will show up for the user to let them know that it is the time to take medicine. Let’s take a look at the code.

The AlarmManager will trigger a BroadcastReceiver when the setup time matches with the current time. Let’s take a look at the implemented BroadcastReceiver.

private const val ALARM_MANAGER_NOTIFICATION_ID = 5
private const val CHANNEL_ID = "alarm_manager_channel"
class AlarmManagerTrigger: BroadcastReceiver() {

    override fun onReceive(context: Context, intent: Intent) {

        val sharedPreferences = context
            .getSharedPreferences("com.chathurangashan.backgroundtasks",Context.MODE_PRIVATE)

        val notificationManager = getSystemService(context, NotificationManager::class.java)

        val notification = NotificationCompat.Builder(context, CHANNEL_ID)
            .setContentTitle(context.getText(R.string.medicine_reminder_notification_title))
            .setContentText(context.getText(R.string.medicine_reminder_notification_description))
            .setSmallIcon(android.R.drawable.ic_dialog_alert)
            .build()

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val notificationChannel = NotificationChannel(
                CHANNEL_ID,
                "Alarm manager notification",
                NotificationManager.IMPORTANCE_DEFAULT
            )
            notificationChannel.description = "Alarm manager notification"
            notificationManager?.createNotificationChannel(notificationChannel)
        }

        notificationManager?.notify(ALARM_MANAGER_NOTIFICATION_ID, notification)

        with (sharedPreferences.edit()) {
            remove(context.getString(R.string.alarm_manger_reminder_time))
            apply()
        }
    }

}

There is nothing much complicated here. In our user case, we only need to show a notification for the user. I have initiated a notification inside onReceive function to show the notification. If there is a need to execute a background task a Service can be started inside this onReceive function. I have used shared preferences to keep track of the data this will be used to re-initialize the AlarmManager when the device gets restarted. Here that value is removed since BroadcastReceiver is already completed its task.

<receiver android:name=".broadcastreceiver.AlarmManagerTrigger" />

Finally, add the receiver to the application AndroidManifest. Then in the UI layer when the user clicks on the save button AlarmManager is initialized like below.

fun onClickSave(
    context: Context,
    navController: NavController,
    selectedLocalDateTime: LocalDateTime
) {

    val sharedPreferences = context
        .getSharedPreferences("com.chathurangashan.backgroundtasks",Context.MODE_PRIVATE)
    val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
    val intent = Intent(context, AlarmManagerTrigger::class.java)
    val pending = PendingIntent.getBroadcast(
        context,
        0,
        intent,
        PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
    )

    val milliseconds = selectedLocalDateTime.atZone(ZoneId.systemDefault()).toEpochSecond() * 1000

    with (sharedPreferences.edit()) {
        putLong(context.getString(R.string.alarm_manger_reminder_time), milliseconds)
        apply()
    }

    alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, milliseconds, pending)

    navController.navigateUp()
}

First, an AlarmManager instance is created and using an intent the broadcast receiver that needs to trigger is provided. Next, a PendingIntent needs to be created. This is the intent that will get executed in the future when the AlarmManager triggers. Then I have added that time value into the SharedPreferences for future use. Finally setExactAndAllowWhileIdle() function sets the AlarmManager to trigger on the milliseconds value input by the user.

Several methods can be called on AlarmManager to have an alarm that behaves differently. setExactAndAllowWhileIdle() will set the trigger our component at the exact time even when the device is on low-power idle modes. If there is no need to trigger the alarm at the precise moment but still want to wake up from low-power idle modes the setAndAllowWhileIdle() can be used. Above mentioned two functions are more like an extension of set() and setExact().

set() schedules an alarm that is expected to be executed at the specified time, but Android may adjust the exact time to minimize the number of wakeups and therefore minimize battery usage. For example, if you schedule an alarm to go off in 5 minutes using set(), the actual time the alarm goes off might be slightly earlier or later than the requested time, depending on how the system is trying to optimize power usage.

setExact(), on the other hand, schedules an alarm to be executed precisely at the specified time, without any optimization by the system. This method guarantees that the alarm will go off at the exact time specified, but may consume more battery power than set() as it requires the system to keep the device awake until the alarm goes off.

Apart from the above functions we have two functions to use when we need to implement repeating alarms. The function setRepeating() method schedules a repeating alarm that will execute at fixed intervals, where the developer specifies the interval. The function setInexactRepeating() method also schedules a repeating alarm but allows the system to adjust the exact triggering time of the alarm to minimize the number of wakeups and therefore minimize battery usage.

Let’s take a look at how to re-initialize the alarm in case of device restart. In the above examples, I have used shared preferences to save the alarm setup millisecond value. And I have removed that value from the shared preferences when the BroadcastReceiver is triggered. When the application is restarted if there is a value in shared preferences that means we need to re-initialize the AlarmManager. To do that I have used android.intent.action.BOOT_COMPLETED system broadcast to identify the device start time. I have created a BroadcastReceiver named BootReceiver and registered it in AndroidManifest like below.

<receiver
            android:name=".broadcastreceiver.BootReceiver"
            android:enabled="true"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED"/>
            </intent-filter>
        </receiver>

Then the BroadcastReceiver class is defined like below.

class BootReceiver : BroadcastReceiver() {

    override fun onReceive(context: Context, intent: Intent) {

        Log.d(BootReceiver::class.simpleName,intent.action.toString())

        if (intent.action == "android.intent.action.BOOT_COMPLETED") {

            val sharedPreferences = context
                .getSharedPreferences("com.chathurangashan.backgroundtasks",Context.MODE_PRIVATE)

            if(sharedPreferences.contains(context.getString(R.string.alarm_manger_reminder_time))){

                val alarmMilliseconds = sharedPreferences.getLong(
                    context.getString(R.string.alarm_manger_reminder_time),
                    0L
                )

                val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
                val broadcastIntent = Intent(context, AlarmManagerTrigger::class.java)
                val pending = PendingIntent.getBroadcast(
                    context,
                    0,
                    broadcastIntent,
                    PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
                )

                alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, alarmMilliseconds, pending)

            }
        }
    }
}

Inside the onReceive() function I check whether the key value for the milliseconds is still contained. If so get that value and re-initialize the alarm manager just like I have set up before.

You can find the full code in this repository. Refer to the alarm_manager_example branch for the start point of the application.

Published by Chathuranga Shan

I am a mobile application developer who loves to get into new technologies in the Industry. Specialized in Android mobile application development with 5+ years’ experience working for different types of products across many business domains.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.