How Cron-Based Scheduled Tasks Are Implemented in SmsForwarder: WorkManager and Cron Parser Integration

SmsForwarder implements cron-based scheduled tasks using Android's WorkManager combined with the gatewayapps-crondroid library, where CronWorker handles execution and rescheduling while CronJobScheduler manages the timing and queuing of OneTimeWorkRequest instances.

The SmsForwarder repository (pppscn/SmsForwarder) provides robust automation capabilities through cron-based scheduled tasks that execute even when the app is not actively running. This implementation leverages Android's WorkManager API alongside a specialized cron parsing library to handle reliable background execution with millisecond precision. Understanding this architecture reveals how the application ensures tasks trigger at exact intervals while preventing duplicate or overlapping executions.

Cron Expression Parsing with gatewayapps-crondroid

The foundation of the scheduling system relies on the gatewayapps-crondroid library to parse standard cron expressions. The CronExpression class from gatewayapps.crondroid computes the next valid execution time based on user-provided strings.

In app/src/main/kotlin/cn/ppps/forwarder/fragment/condition/CronFragment.kt, users input cron expressions which are validated and stored as CronSetting objects. These settings are later deserialized in ConditionUtils.kt using Gson to evaluate whether the current time matches the scheduled pattern.

Task Execution Flow in CronWorker

Located in app/src/main/kotlin/cn/ppps/forwarder/workers/CronWorker.kt, the CronWorker class extends CoroutineWorker and serves as the primary execution engine for scheduled tasks.

Retrieving and Validating Tasks

When the worker runs, it extracts the task ID from inputData and loads the corresponding task from the database. Before execution, it validates that all task conditions are satisfied through ConditionUtils.checkCondition:

val taskId = inputData.getLong(TaskWorker.TASK_ID, -1L)
val task = Core.task.getOne(taskId) ?: return Result.failure()

if (!ConditionUtils.checkCondition(task.id, conditionList)) return Result.failure()

Updating Execution Times

Upon successful validation, the worker updates the task's lastExecTime and calculates the next execution window using CronExpression.getNextValidTimeAfter:

val now = Date()
task.lastExecTime = task.nextExecTime
val next = CronExpression(cronSetting.expression)
    .getNextValidTimeAfter(now).apply { time = time / 1000 * 1000 }
task.nextExecTime = next

Enqueueing Action Workers

The actual task actions are handled by ActionWorker, which is queued immediately after the cron condition is met:

val data = Data.Builder()
    .putLong(TaskWorker.TASK_ID, task.id)
    .putString(TaskWorker.TASK_ACTIONS, task.actions)
    .build()
WorkManager.getInstance()
    .enqueue(OneTimeWorkRequestBuilder<ActionWorker>()
        .setInputData(data).build())

Scheduling Mechanism in CronJobScheduler

The CronJobScheduler utility class in app/src/main/kotlin/cn/ppps/forwarder/utils/task/CronJobScheduler.kt manages the creation and cancellation of WorkManager requests.

When scheduling a task, it calculates the delay between the current time and task.nextExecTime, then constructs a OneTimeWorkRequest:

val task: Task = // obtained from database
CronJobScheduler.scheduleTask(task) // creates a delayed OneTimeWorkRequest

The scheduler uses setInitialDelay to defer execution until the exact cron time. If the delay is less than or equal to zero, the work executes immediately. Each request is enqueued using beginUniqueWork with ExistingWorkPolicy.KEEP to prevent duplicate executions:

  • Unique work policy: Ensures only one instance of a specific task runs at a time
  • Immediate execution: Triggered when nextExecTime has already passed
  • Delayed execution: Uses setInitialDelay for future scheduling

Condition Verification with ConditionUtils

Before CronWorker executes the main task logic, ConditionUtils.checkCondition in app/src/main/kotlin/cn/ppps/forwarder/utils/task/ConditionUtils.kt verifies that the cron expression is currently satisfied. This acts as a safety check to ensure WorkManager delays align with the actual cron schedule:

val cronSetting = Gson().fromJson(condition.setting, CronSetting::class.java)
val now = Date()
val previous = Date(now.time - 1000) // one second before
val next = CronExpression(cronSetting.expression)
    .getNextValidTimeAfter(previous).apply { time = time / 1000 * 1000 }

if (now.time != next.time) return false // cron not satisfied

Task Rescheduling and Cancellation

After each execution, CronWorker reschedules the task for its next occurrence. This process involves canceling any existing pending work for that task ID and creating a new schedule:

CronJobScheduler.cancelTask(task.id)
CronJobScheduler.scheduleTask(task)

This pattern ensures that tasks repeat according to their cron expressions without manual intervention. The cancellation mechanism prevents overlapping schedules when a task is updated or when execution times shift.

Summary

  • CronWorker handles the execution of scheduled tasks as a CoroutineWorker, validating conditions and enqueueing ActionWorker instances
  • CronJobScheduler manages WorkManager requests, calculating delays and using OneTimeWorkRequest with unique work policies to prevent duplicates
  • ConditionUtils performs runtime verification of cron expressions to ensure execution occurs only at valid scheduled times
  • CronExpression from the gatewayapps-crondroid library parses expressions and calculates next execution times in milliseconds
  • Tasks automatically reschedule themselves after each execution by calling scheduleTask within the worker's completion handler

Frequently Asked Questions

How does SmsForwarder ensure cron tasks run when the app is closed?

SmsForwarder utilizes Android's WorkManager API, which is designed for deferrable background work that persists across app restarts and device reboots. The CronWorker extends CoroutineWorker, ensuring tasks execute even when the app is not in the foreground by leveraging the system's job scheduling capabilities.

What happens if a scheduled task misses its execution window?

When a task's nextExecTime has already passed (resulting in a delay ≤ 0), CronJobScheduler immediately queues the work for execution rather than skipping it. The ConditionUtils.checkCondition method further validates that the current time matches the cron pattern, ensuring the task only runs if the schedule is still valid.

Can multiple instances of the same cron task run simultaneously?

No. The implementation uses WorkManager.beginUniqueWork with ExistingWorkPolicy.KEEP when scheduling tasks in CronJobScheduler. This policy ensures that if a pending work item for that task already exists, the new request is discarded, preventing duplicate or overlapping executions of the same cron task.

How does the application handle daylight saving time or timezone changes in cron expressions?

The cron parsing relies on java.util.Date and millisecond-based time calculations. The CronExpression.getNextValidTimeAfter method computes the next valid time based on the current system time, automatically adjusting for timezone changes. However, users should verify that their cron expressions account for local time settings as stored in the CronSetting configuration.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →