Android Schedule Daily Repeating/Reminder Alarm at Specific Time With WorkManager

May 14, 2019

AlarmManager vs WorkManager

You could schedule daily repeating alarm at specific time with AlarmManager, but WorkManager does offer a few extra benefit.

  • Ability to set constraints: like only run when network is available, or don’t run when battery is low.
  • Ensures task execution, even if the app or device restarts. Refer this.
  • Support for retry policy, e.g. retry later if network error.

So, as a guideline, WorkManager is intended for tasks that require a guarantee that the system will run them, even if the app exits. It is not intended for background work that requires immediate execution or requires execution at an exact time. If you need your work to execute at an exact time (such as an alarm clock, or event reminder), use AlarmManager. For work that needs to be executed immediately but is long running, you’ll often need to make sure that work is executed when in the foreground; whether that’s by limiting execution to the foreground (in which case the work is not longer true background work) or using a Foreground Service. - Source

For my scenario, I need the reminder to show up around specific time, doesn’t need to be exact.

Daily Repeating Task at Specific Time with WorkManager

You might be thinking of using PeriodicWorkRequest, but it is not suited to run task at specific time.

  • It may run immediately, at the end of the period, or any time in between so long as the other conditions are satisfied at the time - Source. If you setup for a period of one day, it might run at the start of the day or the end of the day, with no gurantee of the time.
  • Another problem is time drift. On Day 1 it might run at 6:00am, then subsequent days it might run on time or later, and it will get later and later as the days goes by (due to the doze and battery saving mechanism). By Day 10, it could be running at 10:00am.

We shall be using OneTimeWorkRequest.Builder, delay excution using setInitialDelay based on the time. After the work for today is executed, we schedule the work for next day. To avoid accidentally setting up multiple alarm/request, we shall utiliaze enqueueUniqueWork.

Dependencies.

depedencies {
    // https://mvnrepository.com/artifact/androidx.work/work-runtime-ktx
    def work_version = '2.0.1'
    // use -ktx for Kotlin+Coroutines
    implementation "androidx.work:work-runtime-ktx:$work_version"
}

Code

class ReminderWorker(appContext: Context, workerParams: WorkerParameters): CoroutineWorker(appContext, workerParams) {

    companion object {
        private const val REMINDER_WORK_NAME = "reminder"
        private const val PARAM_NAME = "name" // optional - send parameter to worker
        // private const val RESULT_ID = "id"

        fun runAt() {
            val workManager = WorkManager.getInstance()

            // trigger at 8:30am
            val alarmTime = LocalTime.of(8, 30)
            var now = LocalDateTime.now().truncatedTo(ChronoUnit.MINUTES)
            val nowTime = now.toLocalTime()
            // if same time, schedule for next day as well
            // if today's time had passed, schedule for next day
            if (nowTime == alarmTime || nowTime.isAfter(alarmTime)) {
                now = now.plusDays(1)
            }
            now = now.withHour(alarmTime.hour).withMinute(alarmTime.minute) // .withSecond(alarmTime.second).withNano(alarmTime.nano)
            val duration = Duration.between(LocalDateTime.now(), now)

            Timber.d("runAt=${duration.seconds}s")

            // optional constraints
            /*
            val constraints = Constraints.Builder()
                .setRequiredNetworkType(NetworkType.CONNECTED)
                .build()
             */

            // optional data
            val data = workDataOf(PARAM_NAME to "Timer 01")

            val workRequest = OneTimeWorkRequestBuilder<ReminderWorker>()
                .setInitialDelay(duration.seconds, TimeUnit.SECONDS)
                // .setConstraints(constraints)
                .setInputData(data) // optional
                .build()

            workManager.enqueueUniqueWork(REMINDER_WORK_NAME, ExistingWorkPolicy.REPLACE, workRequest)
        }

        fun cancel() {
            Timber.d("cancel")
            val workManager = WorkManager.getInstance()
            workManager.cancelUniqueWork(REMINDER_WORK_NAME)
        }
    }


    override suspend fun doWork(): Result = coroutineScope {
        val worker = this@ReminderWorker
        val context = applicationContext

        val name = inputData.getString(PARAM_NAME)
        Timber.d("doWork=$name")

        var isScheduleNext = true
        try {
            // do something

            // possible to return result
            // val data = workDataOf(RESULT_ID to 1)
            // Result.success(data)

            Result.success()
        }
        catch (e: Exception) {
            // only retry 3 times
            if (runAttemptCount > 3) {
                Timber.d("runAttemptCount=$runAttemptCount, stop retry")
                return@coroutineScope Result.success()
            }

            // retry if network failure, else considered failed
            when(e.cause) {
                is SocketException -> {
                    Timber.e(e.toString(), e.message)
                    isScheduleNext = false
                    Result.retry()
                }
                else -> {
                    Timber.e(e)
                    Result.failure()
                }
            }
        }
        finally {
            // only schedule next day if not retry, else it will overwrite the retry attempt
            // - because we use uniqueName with ExistingWorkPolicy.REPLACE
            if (isScheduleNext) {
                runAt() // schedule for next day
            }
        }
    }
}

NOTE: CoroutineWorker is used the example above.

Usage

ReminderWorker.runAt()

References:

This work is licensed under a
Creative Commons Attribution-NonCommercial 4.0 International License.