SmsForwarder Battery Monitoring and Charging State Trigger System: A Deep Dive into the Source Code

SmsForwarder monitors battery levels and charging states using Android's BroadcastReceiver and WorkManager architecture to evaluate user-defined conditions and trigger automated actions.

The SmsForwarder repository (pppscn/SmsForwarder) implements a robust background system that tracks device power status to execute user-configured forwarding tasks. This article examines the complete battery monitoring and charging state trigger system as implemented in the Kotlin source code, from system broadcast reception to condition evaluation and HTTP API exposure.

System Architecture Overview

The battery monitoring system follows a decoupled architecture that separates lightweight broadcast handling from heavy task processing. The flow moves from system broadcasts through dedicated receivers, then to WorkManager workers for asynchronous condition evaluation.

The data flow follows this path:


System Broadcast → BatteryReceiver → BatteryWorker → ConditionUtils → ActionWorker

This design ensures reliable trigger execution even when the app is in the background or the device is idle, leveraging Android's WorkManager for guaranteed task scheduling.

Capturing Battery Events with BatteryReceiver

The entry point resides in app/src/main/kotlin/cn/ppps/forwarder/receiver/BatteryReceiver.kt. This BroadcastReceiver registers for the system action ACTION_BATTERY_CHANGED, which Android fires whenever battery level, charge status, or plug type changes.

When triggered, the receiver extracts battery data via BatteryUtils.getBatteryInfo(intent) and immediately schedules a one-off BatteryWorker:

class BatteryReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        val batteryInfo = BatteryUtils.getBatteryInfo(intent).toString()
        val levelNew = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0)
        val levelOld = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
        val statusNew = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1)

        val data = Data.Builder()
            .putInt("status", statusNew)
            .putInt("level_new", levelNew)
            .putInt("level_old", levelOld)
            .build()
        val request = OneTimeWorkRequestBuilder<BatteryWorker>()
            .setInputData(data)
            .build()
        WorkManager.getInstance(context).enqueue(request)
    }
}

The receiver captures both the current level and the previous cached value, enabling the system to detect threshold crossings accurately.

Processing Battery Data with BatteryWorker

Located in app/src/main/kotlin/cn/ppps/forwarder/workers/BatteryWorker.kt, the BatteryWorker class extends CoroutineWorker and handles the heavy lifting of condition evaluation. It receives raw battery values through WorkManager's input Data object.

The worker distinguishes between two condition types:

  1. TASK_CONDITION_BATTERY – Triggered when battery percentage crosses user-defined thresholds
  2. TASK_CONDITION_CHARGE – Triggered when charging state changes (charging, discharging, not-charging, full, unknown)

Handling Battery Level Thresholds

For battery percentage monitoring, the worker retrieves tasks of type TASK_CONDITION_BATTERY and parses the stored JSON condition into a BatterySetting object:

when (inputData.getInt(TaskWorker.CONDITION_TYPE, -1)) {
    TASK_CONDITION_BATTERY -> {
        val status = inputData.getInt("status", -1)
        val levelNew = inputData.getInt("level_new", -1)
        val levelOld = inputData.getInt("level_old", -1)

        Core.task.getByType(TASK_CONDITION_BATTERY).forEach { task ->
            val setting = Gson().fromJson(
                task.conditions, Array<TaskSetting>::class.java
            ).first().setting.let {
                Gson().fromJson(it, BatterySetting::class.java)
            }
            val msg = setting.getMsg(status, levelNew, levelOld, TaskUtils.batteryInfo)
            if (msg.isNotEmpty() && ConditionUtils.checkCondition(task.id, ...)) {
                // Enqueue ActionWorker to execute user tasks
            }
        }
        Result.success()
    }
}

The BatterySetting class (defined in app/src/main/kotlin/cn/ppps/forwarder/entity/condition/BatterySetting.kt) encapsulates properties such as status, levelMin, levelMax, and keepReminding.

Detecting Charging State Changes

For charging state triggers, the worker evaluates TASK_CONDITION_CHARGE tasks against the current BatteryManager status constants. The ChargeSetting data class tracks the specific charging states (plugged in, unplugged, full) that should activate the trigger.

Condition Evaluation Logic

The ConditionUtils class in app/src/main/kotlin/cn/ppps/forwarder/utils/ConditionUtils.kt contains the core comparison logic. It validates whether the current TaskUtils.batteryInfo satisfies the user-defined constraints by comparing:

  • Current percentage against BatterySetting.levelMin and BatterySetting.levelMax
  • Current status against BatterySetting.status
  • Threshold crossing direction (using levelOld vs levelNew)

When conditions match, the utility builds a MsgInfo object and enqueues an ActionWorker to perform the configured actions, such as sending HTTP requests, notifications, or SMS messages.

Optional HTTP API for Battery Queries

When the configuration flag enableApiBatteryQuery is enabled, SmsForwarder exposes an HTTP endpoint through its internal server. The BatteryController in app/src/main/kotlin/cn/ppps/forwarder/server/controller/BatteryController.kt handles POST requests to /battery/query:

@RestController
class BatteryController {
    @PostMapping("/battery/query")
    fun query(@RequestBody bean: BaseRequest<EmptyData>): BatteryInfo {
        val intent = /* get last broadcast Intent stored by BatteryReceiver */
        return BatteryUtils.getBatteryInfo(intent)
    }
}

This endpoint returns the latest BatteryInfo obtained from BatteryUtils, allowing external systems to query the device power status without accessing Android APIs directly.

Configuration and Data Models

The system uses several key data classes to persist user settings:

  • BatterySetting: Defines percentage thresholds (levelMin, levelMax), required status, and reminder flags
  • ChargeSetting: Specifies target charging states and plug types
  • BatteryInfo: Runtime data class holding current level, voltage, temperature, and status

Tasks are stored in the local database via the Core.task repository, with conditions serialized as JSON arrays containing the setting objects.

Summary

  • SmsForwarder uses BatteryReceiver to capture Android's ACTION_BATTERY_CHANGED broadcasts and extract battery data via BatteryUtils.
  • WorkManager decouples event reception from processing through BatteryWorker, ensuring reliable background execution.
  • The system supports two trigger types: percentage thresholds (TASK_CONDITION_BATTERY) and charging state changes (TASK_CONDITION_CHARGE).
  • ConditionUtils evaluates user-defined constraints against real-time battery data before dispatching to ActionWorker.
  • An optional HTTP API (/battery/query) exposes battery information when enableApiBatteryQuery is enabled in settings.

Frequently Asked Questions

How does SmsForwarder detect battery level changes without draining the battery?

SmsForwarder uses Android's standard BroadcastReceiver mechanism rather than polling. The BatteryReceiver only activates when the system broadcasts ACTION_BATTERY_CHANGED, which occurs when the battery level changes by 1% or when charging state changes. This passive approach consumes negligible power compared to active monitoring loops.

What is the difference between battery level triggers and charging state triggers?

Battery level triggers (TASK_CONDITION_BATTERY) fire when the percentage crosses user-defined minimum or maximum thresholds (for example, dropping below 15%). Charging state triggers (TASK_CONDITION_CHARGE) fire when the power connection status changes (such as plugging in, unplugging, or reaching full charge). The BatteryWorker handles these as separate condition types with distinct evaluation logic.

Can I query the current battery status via HTTP when using the API server?

Yes. When the enableApiBatteryQuery configuration flag is enabled, the internal HTTP server exposes a POST endpoint at /battery/query. The BatteryController returns the latest BatteryInfo object extracted from the most recent battery broadcast, allowing remote monitoring of device power levels.

Where does SmsForwarder store the battery condition settings?

User-defined battery conditions are stored as JSON within the conditions field of the TaskSetting database entity. The BatteryWorker deserializes this JSON into BatterySetting or ChargeSetting objects using Gson during task evaluation. These settings include thresholds, status requirements, and the keepReminding flag for persistent alerts.

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 →