Schedule local notifications on Android

Avatar
Josip Žitković
9 min read
28.09.2020
Schedule local notifications on Android

Story behind notifications

In my opinion, one of the best parts of the Android OS is notifications. Users are used to interacting with their phones through notifications. Some notifications let you do a simple action without opening the app, unlike in some other platforms, where notifications are mostly ignored and stacked somewhere in the drawer. For example, when you get a Whatsapp message from someone, you can reply directly from the notification, or if the message contains some link, you can open that link without opening the app. If you receive a notification from Google to leave feedback on some location that you visited for the first time, you can leave a rating or a comment through the notification. 

Notifications can also be used to remind users to return to your app. At least it was something we had to deal with before writing this article. In our company, we often build apps that help users build some kind of habits. To build habits, we often need to remind the user to do something. That means we need to schedule notifications because users often forget to open the app.

Android OS limitations

When developing mobile apps, sometimes we don't have a back-end to support us with push notifications. In that case, we will need to have local notifications. On Android OS, this can be a little tricky because of multiple reasons. NotificationManager, which handles everything related to notifications, cannot schedule a notification, so we need to find another way to send notifications at some point in the future. We want to send notifications when the user is not in our app, which means that the app is in the background or killed. If the app is in the background or killed, you need a Service that does some work before showing a notification. Services are not that reliable as they used to be. Android OS is more strict about stuff in the background, and different manufacturers have their own view of when background work can be executed. You can check this to see how other OEM manufacturers handle background work. That means that your notification will probably not be shown to the user.  So what is a solution? There are multiple ways to tackle this problem, depending on what your needs are. If you need a reliable way to schedule notifications, in this article, you can see how we did it using WorkManager.

WorkManager overview

WorkManager is part of the Android Jetpack. It is a library for scheduling deferrable background tasks (work). We use it for long-running background tasks like uploading a video or making sure that some work gets done when some constraints are satisfied. For example, uploading logs to the server can be done when the device is charging and connected to WiFi. WorkManager helps you to schedule some work. This work can be one-time, or it can be repeating work. Scheduled work is then stored in an internally managed SQLite database, and WorkManager takes care of ensuring that work persists and is rescheduled across device reboots. It is also closely connected to the Doze mode. In Doze mode, the system suspends all background work by restricting apps access to the network and CPU. This way, it saves battery by deferring background work to a recurring maintenance window. During this maintenance window, the system will run all the background work.

Figure 1.  Doze provides a recurring maintenance window for apps to use the network and handle pending activities.

Scheduling notification with WorkManager

So why should we care about all of that? Because that means that our scheduled notifications will be shown during this small maintenance window and not at the precise time that we scheduled it. For some types of apps, this is a problem. For example, alarm apps. They need to be 100% precise when showing notifications and informing the user that something is going on. There are few more cases, but for every other app it is not that important to be precise.  There are a few more cases, but it is not that important for every other app to be precise.It is much more critical important that notifications are shown even if the app was not active for some time, and the device was rebooted. In that case, scheduling notifications with WorkManager is the way to go.

One-time notification 

First, we need to create a worker that will execute some work in the background. Here we can implement our logic before a user sees the notification.  In this example, we will just use NotificationManager to deliver a straightforward notification.

class OneTimeScheduleWorker(
    val context: Context,
    workerParams: WorkerParameters
) : Worker(context, workerParams) {

    override fun doWork(): Result {

        val builder = NotificationCompat.Builder(context, CHANNEL_ID)
            .setSmallIcon(R.drawable.notification_icon)
            .setContentTitle("Scheduled notification")
            .setContentText("Hello from one-time worker!")
            .setPriority(NotificationCompat.PRIORITY_DEFAULT)

        with(NotificationManagerCompat.from(context)) {
            notify(Random.nextInt(), builder.build())
        }

        return Result.success()
    }

}

After creating a worker that will show the notification, the next step is to schedule it. We will use the setInitialDelay function that will postpone the execution of our work. Here we can also add the constraints that need to be satisfied to execute work. We will keep it simple and schedule work without any constraints.

fun scheduleOneTimeNotification(initialDelay: Long) {
        val work =
            OneTimeWorkRequestBuilder<OneTimeScheduleWorker>()
                .setInitialDelay(initialDelay, TimeUnit.MILLISECONDS)
                .addTag(WORK_TAG)
                .build()

        WorkManager.getInstance(context).enqueue(work)
}

And that's it. A user will be able to see your notification even if your app is not active. If the system is in Doze mode, there might be some delays, but it will save the device battery, and your app will be battery friendly

Periodic notification (weekly)

For this example, I will show you how to send different notifications every other week. First, we need a worker that will have logic for showing notifications. This worker will receive some input data. In the input data, we will put a DateTime when work was scheduled to calculate in which week we are. We will then change the title and message of notification every week.

class PeriodicScheduleWorker(
    val context: Context,
    workerParams: WorkerParameters
) : Worker(context, workerParams) {

    override fun doWork(): Result {
        val timeOfLastUsage = LocalDateTime
        	.parse(inputData.getString(TIME_SINCE_LAST_USAGE))
        val now = LocalDateTime.now()

        val notificationTitle: String
        val notificationText: String
        
        when (ChronoUnit.WEEKS.between(timeOfLastUsage, now) % 2) {
            1L -> {
                notificationTitle = "Week one"
                notificationText = "This is first of two notifications"
            }
            2L -> {
                notificationTitle = "Week two"
                notificationText = "This is second of two notifications"
            }
        }
        
        val builder = NotificationCompat.Builder(context, CHANNEL_ID)
            .setContentTitle(notificationTitle)
            .setContentText(notificationText)
            .setPriority(NotificationCompat.PRIORITY_DEFAULT)

        with(NotificationManagerCompat.from(context)) {
            notify(Random.nextInt(), builder.build())
        }
        
        return Result.success()
    }
}

After creating a worker, we need to execute it every week. To do that, we will use PeriodicWorkRequestBuilder, where we can set a repeat interval. We also need to specify input data that will be the current time to use it in the worker. Here we need to be careful not to create the same work every time. WorkManager has multiple policies for handling existing work. In this example, we will use ExistingPeriodicWorkPolicy.REPLACE to have only one active work, and if there is existing work, that work will be replaced with the new one.

 fun schedulePeriodicNotifications() {
        val lastUsageTime = LocalDateTime
            .now()
            .format(DateTimeFormatter.ISO_DATE_TIME)

        val data = Data.Builder()
            .putString(
                DATA_REMINDER,
                lastUsageTime
            )
            .build()

        val periodicWork =
            PeriodicWorkRequestBuilder<ReminderWorker>(
                7, TimeUnit.DAYS
            )
                .addTag(WORK_TAG)
                .setInputData(data)
                .build()

        WorkManager.getInstance(context)
            .enqueueUniquePeriodicWork(
                WORK_NAME,
                ExistingPeriodicWorkPolicy.REPLACE,
                periodicWork
            )
}

When creating periodic work requests, you can also set a flex-time interval. That means that your work will be executed once within the flex period of every repeat interval. The shorter the flex period is, the more precise your notification will be.The minimum flex time is 5 minutes.

Conclusion

If you need a reliable way to schedule some future notifications and don't have back-end support, you can use WorkManager to do all the work. Sometimes notifications won't be shown at the exact time you set, but that is not important in most cases. If you need something more precise, you will need to use AlarmManager. With AlarmManager, it is much harder to keep track of scheduled notifications, and you will need to do much more work around it. 

Join us!

Like what you’ve read? Check out our open positions and join the Martian team!

See openings
Martian team

Newsletter

By subscribing to Martian Mantra I agree to the Martian & Machine Privacy Policy.