How SmsForwarder Implements Lock Screen State Detection and Conditional Forwarding

SmsForwarder uses Android's LockScreenReceiver to monitor screen events, persists the state via TaskUtils SharedPreferences, and evaluates user-defined rules through LockScreenWorker before scheduling delayed forwarding tasks with WorkManager.

The open-source Android application SmsForwarder (pppscn/SmsForwarder) enables sophisticated SMS forwarding workflows triggered by device lock states. By combining broadcast receivers with background work scheduling, the app implements robust lock screen state detection and conditional forwarding that respects user-defined timing constraints.

Listening for Screen Events with LockScreenReceiver

The detection system centers on LockScreenReceiver.kt, which intercepts three critical Android intents: ACTION_SCREEN_OFF, ACTION_SCREEN_ON, and ACTION_USER_PRESENT.

Registering the Broadcast Receiver

According to the source code in App.kt (lines 59-66), the receiver is registered during application initialization to ensure immediate capture of screen state changes:

val lockScreenReceiver = LockScreenReceiver()
val lockScreenFilter = IntentFilter().apply {
    addAction(Intent.ACTION_SCREEN_OFF)
    addAction(Intent.ACTION_SCREEN_ON)
    addAction(Intent.ACTION_USER_PRESENT)
}
registerReceiver(lockScreenReceiver, lockScreenFilter)

Handling Screen State Changes

When a broadcast arrives, the receiver determines if the device is physically locked using KeyguardManager.isDeviceLocked. If the screen turns off while the device is secured, the action string is modified to append "_LOCKED", creating distinct triggers for screen-off versus device-locked states.

override fun onReceive(context: Context?, intent: Intent?) {
    if (context == null) return
    var action = intent?.action ?: return
    if (action == Intent.ACTION_SCREEN_OFF && isDeviceLocked(context)) {
        action += "_LOCKED"
    }
    TaskUtils.lockScreenAction = action
    
    val request = OneTimeWorkRequestBuilder<LockScreenWorker>()
        .setInputData(workDataOf(
            TaskWorker.CONDITION_TYPE to TASK_CONDITION_LOCK_SCREEN,
            TaskWorker.ACTION to action
        ))
        .build()
    WorkManager.getInstance(context).enqueue(request)
}

Persisting Lock Screen State in SharedPreferences

The latest lock screen action is stored in TaskUtils.kt using the preference key SP_LOCK_SCREEN_ACTION. The lockScreenAction property wraps this value, providing global access to the current device state across application components.

This persistence mechanism ensures that background workers can reference the trigger action even if the app process restarts before condition evaluation completes.

Evaluating Conditions with LockScreenWorker

LockScreenWorker.kt performs the core logic for conditional forwarding. It retrieves all tasks configured with condition type TASK_CONDITION_LOCK_SCREEN and validates whether the stored action matches the user's specific configuration.

Parsing LockScreenSetting

Each task's first condition is deserialized from JSON into a LockScreenSetting object using Gson. The worker validates the broadcast action against lockScreenSetting.action before proceeding:

val lockScreenSetting = Gson().fromJson(
    firstCondition.setting, 
    LockScreenSetting::class.java
)
if (action != lockScreenSetting.action) return

Additional constraints are verified through ConditionUtils.checkCondition before the forwarding delay is calculated.

Calculating Forwarding Delays

The worker maps specific actions to their corresponding timing fields, converting user-defined minutes to milliseconds:

val duration = when (action) {
    Intent.ACTION_SCREEN_ON -> lockScreenSetting.timeAfterScreenOn * 60000L
    Intent.ACTION_SCREEN_OFF -> lockScreenSetting.timeAfterScreenOff * 60000L
    Intent.ACTION_USER_PRESENT -> lockScreenSetting.timeAfterScreenUnlocked * 60000L
    else -> lockScreenSetting.timeAfterScreenLocked * 60000L
}

Scheduling the ActionWorker

Once conditions are satisfied, the worker enqueues a delayed OneTimeWorkRequest for ActionWorker, passing the task ID, conditions, and payload data:

val forwardRequest = OneTimeWorkRequestBuilder<ActionWorker>()
    .setInitialDelay(duration, TimeUnit.MILLISECONDS)
    .setInputData(actionData)
    .build()
WorkManager.getInstance().enqueue(forwardRequest)

Configuring Lock Screen Triggers in the UI

Users define these behaviors through LockScreenFragment.kt, which provides the interface for selecting trigger types (screen on/off/locked/unlocked) and setting "time after" values in minutes. The fragment constructs a LockScreenSetting object—defined in LockScreenSetting.kt—that stores the action string and timing parameters as JSON within the task record.

Summary

  • LockScreenReceiver captures ACTION_SCREEN_OFF, ACTION_SCREEN_ON, and ACTION_USER_PRESENT broadcasts in App.kt, appending "_LOCKED" when KeyguardManager.isDeviceLocked returns true.
  • TaskUtils persists the latest action in SharedPreferences using the key SP_LOCK_SCREEN_ACTION, enabling state access across the application lifecycle.
  • LockScreenWorker evaluates tasks matching TASK_CONDITION_LOCK_SCREEN, parses LockScreenSetting JSON with Gson, and verifies conditions via ConditionUtils.checkCondition.
  • WorkManager chains the conditional evaluation to the actual forwarding operation via ActionWorker, respecting calculated delays based on specific screen actions.
  • LockScreenFragment provides the configuration interface for defining triggers and timing constraints, storing settings in the LockScreenSetting data model.

Frequently Asked Questions

How does SmsForwarder distinguish between screen off and device locked?

When ACTION_SCREEN_OFF is received, the receiver checks KeyguardManager.isDeviceLocked. If the device is secured, it appends "_LOCKED" to the action string, creating a distinct SCREEN_OFF_LOCKED state that triggers separate forwarding rules from a simple screen-off event.

What happens if the device reboots before the delayed forwarding executes?

WorkManager persists scheduled OneTimeWorkRequest objects across reboots. The LockScreenWorker passes the action and task data directly to ActionWorker via setInputData, ensuring the forwarding proceeds with the correct context even if the app restarts.

Can users set different delays for screen on versus screen locked events?

Yes. The LockScreenSetting model includes separate integer fields: timeAfterScreenOn, timeAfterScreenOff, timeAfterScreenUnlocked, and timeAfterScreenLocked. The LockScreenWorker maps the specific broadcast action to the corresponding timing field, allowing distinct delays for each state transition.

Where is the LockScreenReceiver registered in the application lifecycle?

The receiver is registered in App.kt during the Application.onCreate() method (lines 59-66), ensuring it captures screen state changes immediately when the app process starts and remains active while the application is running.

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 →