SmsForwarder Rule Matching Algorithm and Condition Evaluation System Explained

SmsForwarder uses a hierarchical tree-based rule matching system that parses textual conditions into RuleLine objects and evaluates them against message attributes, combined with a separate ConditionUtils subsystem that validates task triggers like cron schedules, network states, and battery levels before execution.

SmsForwarder is an open-source SMS forwarding application for Android that filters incoming messages and triggers actions based on user-defined criteria. The system relies on two distinct but complementary subsystems: the rule matching engine that evaluates message content against filtering criteria, and the condition evaluation system that manages task execution logic. This analysis examines the actual Kotlin implementation found in the pppscn/SmsForwarder repository to explain how these components process data and make routing decisions.

How Rule Matching Works in SmsForwarder

The rule matching subsystem determines whether an incoming message satisfies the filtering criteria defined by users. It handles both simple single-field comparisons and complex multi-line logical expressions.

Rule Definition and Data Structure

Rules are stored as Rule entities in app/src/main/kotlin/cn/ppps/forwarder/database/entity/Rule.kt. Each rule contains three key fields that define the matching logic:

  • filed – Specifies which message attribute to examine (e.g., phone number, content).
  • check – Defines the comparison operator (e.g., "contain", "equal", "regex").
  • value – Contains the reference value or complex rule text.

For simple rules, these fields directly describe a condition such as "phone number contains 10086". For complex rules, the filed value equals FILED_MULTI_MATCH, and the actual rule logic is stored as structured text in the value field.

Parsing Multi-Line Rules into a Tree Structure

When Rule.checkMsg() encounters a FILED_MULTI_MATCH field at line 236 of Rule.kt, it delegates parsing to RuleLineUtils.checkRuleLines in app/src/main/kotlin/cn/ppps/forwarder/utils/RuleLineUtils.kt. This static utility parses the raw rule text into a hierarchical tree of RuleLine objects.

The parser splits each line into four tokens:

  1. Conjunction – Logical operator (并且/或者 mapping to CONJUNCTION_AND or CONJUNCTION_OR).
  2. Sure – Confirmation flag (是/否).
  3. Field – Message attribute to check (phone, content, UID).
  4. Check operator – Comparison type (is, contain, not contain, start with, end with, regex).

Indentation determines hierarchy in the tree structure:

  • Same indentation creates a sibling node.
  • Deeper indentation creates a child node.
  • Shallower indentation climbs back to a parent's sibling.

The parser throws an exception if any line does not contain exactly four tokens, ensuring strict validation of rule syntax.

Tree Traversal and Logical Evaluation

The RuleLineUtils.checkRuleTree function traverses the parsed tree recursively to evaluate the message. The algorithm follows this pattern:

// Conceptual flow from RuleLineUtils.kt
result = current.checkMsg(msg)
if (current.child != null) {
    result = combine(result, child.checkMsg(msg), child.conjunction)
}
if (current.next != null) {
    result = combine(result, next.checkMsg(msg), next.conjunction)
}

Each RuleLine.checkMsg call extracts the appropriate attribute from the incoming MsgInfo object (e.g., msg.from, msg.content, msg.uid) and passes it to RuleLine.checkValue.

Value expressions support composite logic through && and || operators. The method splits values into OR-groups (delimited by ||) and then into AND-groups (delimited by &&), evaluating each atomic condition with evaluateCondition. CONJUNCTION_AND behaves like logical &&, while CONJUNCTION_OR behaves like ||. The final boolean result determines whether the entire rule matches the message.

Task Condition Evaluation System

While rules filter message content, the condition evaluation system determines whether a task should execute based on device state and timing constraints. The core logic resides in ConditionUtils at app/src/main/kotlin/cn/ppps/forwarder/utils/task/ConditionUtils.kt.

Condition List Architecture

Each task stores a list of TaskSetting objects representing its conditions. The first element in the list serves as the trigger (e.g., TASK_CONDITION_SIM for SIM readiness). Subsequent elements act as filters that must all evaluate to true for the task to run.

The main entry point ConditionUtils.checkCondition(taskId, conditionList, beginIndex = 1) iterates over filters starting at index 1 (after the trigger). If any filter returns false, the function immediately returns false. If the list is empty or beginIndex exceeds the list size, it returns true.

Supported Condition Types

A when block in ConditionUtils dispatches to type-specific handlers:

  • Cron – Parses CRON expressions using CronExpression, aligns the current time to whole seconds, and compares the next valid time against the current timestamp.
  • Location – Supports distance and address checks (enter/leave) by comparing TaskUtils.locationInfo values against previous states.
  • Network – Validates Wi-Fi SSID or mobile data SIM slot configurations.
  • SIM – Matches the current SIM state against TaskUtils.simState.
  • Battery / Charge – Uses TaskUtils.batteryLevel, batteryStatus, and batteryPlugged to apply BatterySetting or ChargeSetting constraints.
  • Lock Screen – Compares the current lock-screen action against TaskUtils.lockScreenAction.
  • Bluetooth – Handles state changes, device connections, and discovery results.
  • SMS / Call / App – Deserializes stored JSON into Rule objects and tests incoming messages against the rule criteria.

Execution Flow and Delay Handling

When the trigger involves SIM readiness or network changes, the system introduces a 5-second delay (DELAY_TIME_AFTER_SIM_READY) to allow the system to stabilize before evaluating remaining conditions. This prevents premature execution during transient state changes.

Workers such as SendWorker.kt, NetworkWorker.kt, and BluetoothWorker.kt invoke ConditionUtils.checkCondition(task.id, conditionList) to validate execution requirements before proceeding with message forwarding.

Implementation Examples

Matching a Simple Phone Number Rule

import cn.ppps.forwarder.database.entity.Rule
import cn.ppps.forwarder.database.entity.MsgInfo
import java.util.Date

// Define a simple rule
val rule = Rule(
    id = 0L,
    type = "sms",
    filed = Rule.FILED_PHONE_NUM,
    check = Rule.CHECK_CONTAIN,
    value = "10086",
    senderId = 0L,
    senderList = emptyList(),
    senderLogic = Rule.SENDER_LOGIC_UNTIL_SUCCESS
)

// Incoming message
val msg = MsgInfo(
    type = "sms",
    from = "15810086186",
    content = "Hello",
    time = Date(),
    uid = "12345"
)

// Evaluate
val matches = rule.checkMsg(msg)  // Returns true

Evaluating Complex Multi-Line Rules

import cn.ppps.forwarder.database.entity.Rule
import cn.ppps.forwarder.utils.RuleLineUtils

val multiRuleText = """
    并且 是 手机号 相等 10086
    或者 是 内容 包含 test
"""

val rule = Rule(
    id = 0,
    type = "sms",
    filed = Rule.FILED_MULTI_MATCH,
    check = Rule.CHECK_IS,
    value = multiRuleText
)

// RuleLineUtils.checkRuleLines parses the text and builds the tree
val matches = RuleLineUtils.checkRuleLines(msg, rule.value)

Validating Cron-Based Task Conditions

import cn.ppps.forwarder.utils.task.ConditionUtils
import cn.ppps.forwarder.database.entity.TaskSetting
import com.google.gson.Gson

// Cron expression: every 5 minutes
val cronSetting = CronSetting(expression = "0 0/5 * * * ?")
val condition = TaskSetting(
    type = TaskSetting.TASK_CONDITION_CRON,
    setting = Gson().toJson(cronSetting)
)

val canRun = ConditionUtils.checkCondition(
    taskId = 42L,
    conditionList = mutableListOf(condition)
)

Summary

  • Rule Matching operates through RuleLineUtils.checkRuleLines which parses textual rule definitions into hierarchical trees of RuleLine objects, supporting logical operators (&&, ||) and comparison operators (contain, regex, etc.).
  • Condition Evaluation uses ConditionUtils.checkCondition to validate task execution requirements across multiple dimension types including cron schedules, location, network, battery, and SIM state.
  • Indentation-based parsing in RuleLineUtils creates parent-child relationships between rule conditions, enabling complex nested logic without requiring parentheses.
  • Source Files: Core logic resides in Rule.kt (entity definition), RuleLine.kt (node representation), RuleLineUtils.kt (tree parsing), and ConditionUtils.kt (task validation).

Frequently Asked Questions

How does SmsForwarder handle nested logical operators in rule definitions?

SmsForwarder parses multi-line rule text into a tree structure where indentation determines nesting level, not parentheses. Each line specifies a conjunction (并且/或者 for AND/OR) and the tree traversal in RuleLineUtils.checkRuleTree evaluates children recursively. Within individual values, the system supports && (AND) and || (OR) operators by splitting values into OR-groups first, then AND-groups, evaluating each atomic condition separately.

What is the difference between a rule and a condition in SmsForwarder?

A rule filters message content (SMS text, phone numbers, app data) and resides in the Rule entity class, evaluated through Rule.checkMsg(). A condition controls task execution timing and device state requirements (cron schedules, battery level, network connectivity) and is evaluated through ConditionUtils.checkCondition(). Tasks use conditions to decide when to run, then apply rules to decide which messages to forward.

Which message attributes can rules evaluate against?

According to RuleLine.checkMsg in RuleLine.kt, rules can evaluate the from field (phone number), content field (message body), and uid field (unique identifier). The filed token in the rule definition selects which attribute to examine, supporting operators like contain, start with, end with, regex, and exact matching.

Why does SmsForwarder delay condition evaluation after SIM or network changes?

The system implements a 5-second delay (DELAY_TIME_AFTER_SIM_READY) when the trigger condition involves SIM readiness or network state changes. This delay, handled in ConditionUtils, allows the Android system to stabilize its internal state before evaluating subsequent conditions, preventing race conditions where the device might report transient states that don't reflect stable connectivity.

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 →