# Implementing Periodic Notifications with WorkManager

<iframe src="https://androidweekly.net/issues/issue-514/badge" height="25px"></iframe>

I had some free time on my hands this morning so I decided to work on an [issue](https://github.com/sanskar10100/Transactions/issues/48) for my app [Transactions](https://play.google.com/store/apps/details?id=dev.sanskar.transactions). The issue was to add a recurring notification feature that would remind the users of the app to log their transactions for the day.

So, I did some research, and decided to use one of two ways:

### 1\. Firebase Cloud Messaging (the easy way)

Firebase Cloud Messaging is a service that allows you to send notifications or data payload to your Android, iOS, or Web app. Sending background notifications (which only work when your app is in the background) is fairly easy. You can either use the firebase `admin-sdk` or the notification composer in the Firebase Console to send a notification with an optional payload (image, or key-value pairs) to a target token, a topic that the client device subscribed to, or to all the users of the app altogether. When the app is in the background, the notification appears in the system tray, and tapping it launches the app. It's extremely simple to set up a recurring notification as well, you just have to change the frequency.

For [Transactions](https://play.google.com/store/apps/details?id=dev.sanskar.transactions), I simply added the SDK dependencies to my `build.gradle` file, ran the app, exited the app (so that it's in the background, foreground notifications work very differently), and used the notification composer to send a test notification. Once that worked, I set up a recurring notification for 10 PM daily.

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1649509821393/QRW5_q3xy.png align="left")

FCM also allows you to modify your notifications on the go, so that's an added advantage. But, I wanted some more features out of this:

* Users should be able to change the default reminder time of 10 PM to a time that they'd prefer
    
* Users should be able to opt out of this feature if they want to.
    

While the second feature could've been implemented by having the app register or deregister from a `daily-reminder` topic in the FCM, the first one would require either cloud functions or a suitable backend, and I wanted to keep this implementation on-device. So, I moved to the second option.

### 2\. WorkManager with Periodic work requests

I'd taken a [codelab](https://developer.android.com/codelabs/android-workmanager) earlier that taught how to set up notifications with [WorkManager](https://developer.android.com/topic/libraries/architecture/workmanager). Albeit it was focused on immediate one-time requests, I was aware that `WorkManager` had the facility of Deferred, Periodic work requests that could execute repeatedly at a fixed interval. I could add some initial delay to this to get the behavior that I wanted.

I started out by setting up a `NotificationHelper` Kotlin singleton object, whose purpose was to create the notification channel (required on Android 8+), the `PendingIntent` that'd launch the app when the notification was tapped, and the notification itself.

#### NotificationHandler

```kotlin
object NotificationHandler {
    private const val CHANNEL_ID = "transactions_reminder_channel"

    fun createReminderNotification(context: Context) {
        //  No back-stack when launched
        val intent = Intent(context, MainActivity::class.java).apply {
            flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
        }
        val pendingIntent = PendingIntent.getActivity(context, 0, intent,
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE
            else PendingIntent.FLAG_UPDATE_CURRENT)

        createNotificationChannel(context) // This won't create a new channel everytime, safe to call

        val builder = NotificationCompat.Builder(context, CHANNEL_ID)
            .setSmallIcon(R.mipmap.ic_launcher_round)
            .setContentTitle("Remember to add your transactions!")
            .setContentText("Logging your transactions daily can help you manage your finances better.")
            .setPriority(NotificationCompat.PRIORITY_HIGH)
            .setContentIntent(pendingIntent) // For launching the MainActivity
            .setAutoCancel(true) // Remove notification when tapped
            .setVisibility(VISIBILITY_PUBLIC) // Show on lock screen

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

    /**
     * Required on Android O+
     */
    private fun createNotificationChannel(context: Context) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val name = "Daily Reminders"
            val descriptionText = "This channel sends daily reminders to add your transactions"
            val importance = NotificationManager.IMPORTANCE_HIGH
            val channel = NotificationChannel(CHANNEL_ID, name, importance).apply {
                description = descriptionText
            }
            // Register the channel with the system
            val notificationManager: NotificationManager =
                context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
            notificationManager.createNotificationChannel(channel)
        }
    }
}
```

I'd planned for the `createReminderNotification` method to be called from the `Worker` when it was invoked. So, I started writing the implementation for the `WorkManager`. Before we look at the code, this is what `WorkManager` is, according to the AndroidX team:

> WorkManager is the recommended solution for persistent work. Work is persistent when it remains scheduled through app restarts and system reboots. Because most background processing is best accomplished through persistent work, WorkManager is the primary recommended API for background processing.

WorkManager abstracts away the myriads of background processing APIs that Android has, including FirebaseJobDispatcher, GcmNetworkManager, and Job Scheduler. It provides one single surface, which then delegates work to these APIs depending on the context and the API level. These are the type of `Work` requests that `WorkManager` supports:

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1649511185251/e4JtvhWyv.png align="left")

Its features cannot be described in a single article, so I hope the code and the comments will be able to convey the meaning of what's happening. To start with `WorkManager`, we add this dependency to our `build.gradle` (for Kotlin):

```kotlin
implementation "androidx.work:work-runtime-ktx:2.7.1"
```

#### ReminderNotificationWorker

These are requests that run at a fixed interval of `TimeUnit`, such as Days, Hours, Minutes, etc. One important thing to note here is that `WorkManager` doesn't guarantee that a `Work` will be executed at the set time. It may be delayed due to battery optimizations and such, but it does guarantee that the `Work` will be executed, sooner or later. I started out by extending the `Worker` class to create a new `Worker` whose `doWork()` method is executed by the `WorkManager` when it's fired.

```kotlin
class ReminderNotificationWorker(private val appContext: Context, workerParameters: WorkerParameters) : Worker(appContext, workerParameters) {
    override fun doWork(): Result {
        NotificationHandler.createReminderNotification(appContext)
        return Result.success()
    }
}
```

Whenever the `ReminderNotificationWorker` is executed, it creates a new reminder notification. I didn't include any error handling here, which I plan on adding some time later.

After that, I defined a static, or as Kotlin likes to call it, `companion object` method, called `schedule`, which takes in a `Context`, and the time at which the notification should be generated daily. This method also handles some cases like when the user selects a time that's before the current time, it delays the first firing until the next day. That may sound easy, but it was surprisingly complex to implement manually, and I resorted to the `Calendar` class in the end.

```kotlin
companion object {

        /**
         * @param hourOfDay the hour at which daily reminder notification should appear [0-23]
         * @param minute the minute at which daily reminder notification should appear [0-59]
         */
        fun schedule(appContext: Context, hourOfDay: Int, minute: Int) {
            log("Reminder scheduling request received for $hourOfDay:$minute")
            val now = Calendar.getInstance()
            val target = Calendar.getInstance().apply {
                set(Calendar.HOUR_OF_DAY, hourOfDay)
                set(Calendar.MINUTE, minute)
            }

            if (target.before(now)) {
                target.add(Calendar.DAY_OF_YEAR, 1)
            }

            log("Scheduling reminder notification for ${target.timeInMillis - System.currentTimeMillis()} ms from now")

            val notificationRequest = PeriodicWorkRequestBuilder<ReminderNotificationWorker>(24, TimeUnit.HOURS)
                .addTag(TAG_REMINDER_WORKER)
                .setInitialDelay(target.timeInMillis - System.currentTimeMillis(), TimeUnit.MILLISECONDS).build()
            WorkManager.getInstance(appContext)
                .enqueueUniquePeriodicWork(
                "reminder_notification_work",
                ExistingPeriodicWorkPolicy.REPLACE,
                notificationRequest
            )
        }
    }
```

We use `UniquePeriodicWork` to ensure that there's ever only one instance of our `ReminderNotificationWorker` in existence since I didn't want to annoy the user with improper notifications. The tag is used later to identify and cancel all the `Work` requests should the user choose to opt out. I realized I'd need to handle the user preferences at this point, so I wrote a `SharedPreferences` helper class that'd help us interact with the `SharedPreferences` key-value store that Android provides.

#### PreferenceStore

```kotlin
class PreferenceStore(context: Context) {
    private val sharedPref: SharedPreferences = context.getSharedPreferences("transactions_shared_pref", Context.MODE_PRIVATE)

    /**
     * @return the hour of the day in which the reminder should be shown, default is 22, if canceled -1
     * @return the minute at which the reminder should be shown, default is 0, if canceled -1
     */
    fun getReminderTime(): Pair<Int, Int> {
        return Pair(
            sharedPref.getInt(SHARED_PREF_REMINDER_HOUR, 22),
            sharedPref.getInt(SHARED_PREF_REMINDER_MINUTE, 0)
        )
    }

    fun setReminderTime(hour: Int, minute: Int) {
        sharedPref.edit {
            putInt(SHARED_PREF_REMINDER_HOUR, hour)
            putInt(SHARED_PREF_REMINDER_MINUTE, minute)
        }
    }

    fun cancelReminder() {
        sharedPref.edit {
            putInt(SHARED_PREF_REMINDER_HOUR, -1)
            putInt(SHARED_PREF_REMINDER_MINUTE, -1)
        }
    }

    fun isDefaultReminderSet() = sharedPref.getBoolean("is_default_reminder_set", false)

    fun saveDefaultReminderIsSet() {
        sharedPref.edit { putBoolean("is_default_reminder_set", true) }
    }
}
```

When the app is launched for the first time, we set the default reminder at 10 PM, which is then customizable by the user. I probably could have removed some redundant methods at the bottom, but couldn't find a feasible solution. Now, let's move to the ViewModel.

#### MainViewModel

```kotlin
class MainViewModel(application: Application) : AndroidViewModel(application) {

    private val app = application
    private val prefStore = PreferenceStore(application)

    fun scheduleReminderNotification(hourOfDay: Int, minute: Int) {
        prefStore.setReminderTime(hourOfDay, minute)
        ReminderNotificationWorker.schedule(app, hourOfDay, minute)
    }

    fun getReminderTime() = prefStore.getReminderTime()

    fun cancelReminderNotification() {
        log("Cancelling reminder notification")
        prefStore.cancelReminder()
        WorkManager.getInstance(app).cancelAllWorkByTag(TAG_REMINDER_WORKER)
    }

    /**
     * This sets the default time at the first launch of the app
     */
    private fun checkAndSetDefaultReminder() {
        if (!prefStore.isDefaultReminderSet()) {
            scheduleReminderNotification(DEFAULT_REMINDER_HOUR, DEFAULT_REMINDER_MINUTE)
            prefStore.saveDefaultReminderIsSet()
        }
    }
```

We extend the `AndroidViewModel` class, since the application instance, or rather the `Context`, is needed for initializing the database instance and the shared preferences store, as well as the `WorkManager`. I plan on adding **Dependency Injection using Hilt** to the app soon, which would eliminate the need for this. In our ViewModel, the `checkAndSetDefaultReminder` method is called every time the ViewModel is initialized. This is alright for now (or is it? :p) since we use only a single ViewModel shared across fragments because of Transactions being a relatively small app, but it'll soon be migrated to a more appropriate place like the Application class. The intent of the other methods should be clear enough by the names. Now, let's look at the `BottomSheetDialogFragment` that allows the user to configure notifications:

#### NotificationsBottomSheet

```kotlin
class NotificationsBottomSheet : BottomSheetDialogFragment() {
    private lateinit var binding: FragmentNotificationsBottomSheetBinding
    private val viewModel by viewModels<MainViewModel>()

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        binding = FragmentNotificationsBottomSheetBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        setInitial()

        binding.checkboxReminder.setOnCheckedChangeListener { buttonView, isChecked ->
            if (isChecked) {
                // Was previously removed by user, now user wants to re-enable.
                viewModel.scheduleReminderNotification(DEFAULT_REMINDER_HOUR, DEFAULT_REMINDER_MINUTE)
                binding.textViewReminderTime.isEnabled = true
                binding.textViewReminderTime.text = get12HourTime(DEFAULT_REMINDER_HOUR, DEFAULT_REMINDER_MINUTE)
            } else {
                viewModel.cancelReminderNotification()
                binding.textViewReminderTime.isEnabled = false
                binding.textViewReminderTime.text = "Not set"
            }
        }

        binding.textViewReminderTime.setOnClickListener {
            val time = viewModel.getReminderTime()

            val picker = MaterialTimePicker.Builder()
                .setTimeFormat(TimeFormat.CLOCK_12H)
                .setHour(time.first)
                .setMinute(time.second)
                .setTitleText("Select Reminder Time")
                .build()
            picker.show(parentFragmentManager, "notification-time-picker")

            picker.addOnPositiveButtonClickListener {
                val hour = picker.hour
                val minute = picker.minute
                viewModel.scheduleReminderNotification(hour, minute)
                val newTime = viewModel.getReminderTime()
                binding.textViewReminderTime.text = get12HourTime(newTime.first, newTime.second)
            }
        }
    }

    private fun setInitial() {
        val setTime = viewModel.getReminderTime()
        if (setTime.first == -1 || setTime.second == -1 ) {
            binding.checkboxReminder.isChecked = false
            binding.textViewReminderTime.isEnabled = false
            binding.textViewReminderTime.text = "Not set"
        } else {
            binding.checkboxReminder.isChecked = true
            binding.textViewReminderTime.isEnabled = true
            binding.textViewReminderTime.text = get12HourTime(setTime.first, setTime.second)
        }
    }
}
```

Initial values are fetched and set from the Preferences. The user can enable or disable the notifications by clicking on the checkbox. Time can be configured by tapping the text and defaulting to the value of 10 PM. We use the Material `TimePicker` widget to allow the user to select time easily, and it's also super simple to implement, compared to a separate `TimerPickerDialog`, which is used elsewhere in the app.

At a fixed interval of every 24 hours, the `ReminderNotificationWorker` is triggered by the `WorkManager`. The worker, in turn, creates a notification (this being the `Work`, tapping which takes the user directly to the app. This allows us to implement the features required above, while also keeping the implementation on the device, by leveraging the awesome AndroidX library that is `WorkManager`.

![Screenshot_1649503731.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1649514397186/jSmbe1HSz.png align="left")

Transactions is available on the [Play Store](https://play.google.com/store/apps/details?id=dev.sanskar.transactions), and the full source code is available on [GitHub](https://github.com/sanskar10100/Transactions). Leave a thumbs up if you liked the article. Cheers!
