# How SmsForwarder Implements Lock Screen State Detection and Conditional Forwarding

> Discover how SmsForwarder detects lock screen state and conditionally forwards messages. Learn about its use of LockScreenReceiver, SharedPreferences, and WorkManager for efficient message management.

- Repository: [pppscn/SmsForwarder](https://github.com/pppscn/SmsForwarder)
- Tags: internals
- Published: 2026-06-22

---

**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`](https://github.com/pppscn/SmsForwarder/blob/main/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`](https://github.com/pppscn/SmsForwarder/blob/main/App.kt) (lines 59-66), the receiver is registered during application initialization to ensure immediate capture of screen state changes:

```kotlin
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.

```kotlin
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`](https://github.com/pppscn/SmsForwarder/blob/main/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`](https://github.com/pppscn/SmsForwarder/blob/main/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:

```kotlin
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:

```kotlin
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:

```kotlin
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`](https://github.com/pppscn/SmsForwarder/blob/main/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`](https://github.com/pppscn/SmsForwarder/blob/main/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`](https://github.com/pppscn/SmsForwarder/blob/main/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`](https://github.com/pppscn/SmsForwarder/blob/main/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.