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
LockScreenReceivercapturesACTION_SCREEN_OFF,ACTION_SCREEN_ON, andACTION_USER_PRESENTbroadcasts inApp.kt, appending "_LOCKED" whenKeyguardManager.isDeviceLockedreturns true.TaskUtilspersists the latest action in SharedPreferences using the keySP_LOCK_SCREEN_ACTION, enabling state access across the application lifecycle.LockScreenWorkerevaluates tasks matchingTASK_CONDITION_LOCK_SCREEN, parsesLockScreenSettingJSON with Gson, and verifies conditions viaConditionUtils.checkCondition.- WorkManager chains the conditional evaluation to the actual forwarding operation via
ActionWorker, respecting calculated delays based on specific screen actions. LockScreenFragmentprovides the configuration interface for defining triggers and timing constraints, storing settings in theLockScreenSettingdata 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →