How SmsForwarder Implements Dual SIM Card Detection and Handling: A Complete Technical Guide

SmsForwarder detects and manages dual SIM cards through a three-layer architecture involving system broadcast receivers, shared preference tracking, and rule-based filtering to ensure messages are routed through the correct SIM slot.

The open-source SmsForwarder repository (pppscn/SmsForwarder) provides a robust SMS forwarding solution for Android devices with multiple SIM slots. Understanding its dual SIM card detection and handling mechanism reveals how the app maintains reliable message processing across different hardware configurations. This implementation combines Android system broadcasts, centralized state management, and user-configurable rules to handle the complexities of dual SIM operations.

System-Level SIM State Detection

The first layer of detection relies on SimStateReceiver, which listens for the Android system broadcast android.intent.action.SIM_STATE_CHANGED. When the system notifies the app of a SIM state change, the receiver performs several critical operations.

It reads the current state from TelephonyManager, checking for states like SIM_STATE_READY or SIM_STATE_ABSENT to determine physical SIM presence and readiness. According to the source code in SimStateReceiver.kt (lines 31-52), the receiver then updates a shared preference flag named simState through TaskUtils, where the value 5 indicates ready status and 1 indicates removed.

When a SIM becomes ready, the receiver enforces a 5-second delay (DELAY_TIME_AFTER_SIM_READY) before triggering any dependent tasks. This delay, implemented in SimStateReceiver.kt (lines 54-66), ensures the SIM is fully initialized and network registration is complete before the app attempts to use the slot for forwarding operations.

Data SIM Slot Tracking

Beyond simple presence detection, SmsForwarder tracks which SIM slot currently carries the active data connection. The TaskUtils class maintains the dataSimSlot value in shared preferences (lines 136-137), storing the index of the SIM currently assigned for mobile data.

This value updates dynamically through NetworkChangeReceiver, which queries the system whenever network connectivity changes. When a new network is detected, the receiver calls TelephonyManager.getDataSimSlotIndex() and persists the result via TaskUtils (as shown in NetworkChangeReceiver.kt, lines 64-71). This creates a real-time mapping of which physical slot serves as the primary data connection, separate from the voice/SMS slot.

Rule-Based SIM Slot Filtering

The final layer applies user-defined rules during message processing. When SendWorker evaluates forwarding tasks, it checks whether the incoming message matches the configured SIM slot requirements defined in Constants.kt (lines 125-127).

The app supports three slot selection constants:

  • CHECK_SIM_SLOT_1 for SIM 1 only
  • CHECK_SIM_SLOT_2 for SIM 2 only
  • CHECK_SIM_SLOT_ALL for any active SIM

During execution, SendWorker constructs a target identifier string by converting the message's simSlot index (adding 1 to convert from 0-based to 1-based) to "SIM1" or "SIM2" format. It compares this against the rule's simSlot field (as implemented in SendWorker.kt, lines 52-53 and 152-154). If the rule specifies a particular slot and the message originates from a different one, the worker skips the task and prevents forwarding through the wrong channel.

Implementation Code Examples

The following Kotlin snippets demonstrate how to implement the same dual SIM detection patterns used throughout the SmsForwarder codebase.

Listening for SIM State Changes

Mirror the SimStateReceiver implementation to detect SIM insertion and removal:

val filter = IntentFilter(TelephonyManager.ACTION_SIM_STATE_CHANGED)
context.registerReceiver(object : BroadcastReceiver() {
    override fun onReceive(ctx: Context, intent: Intent) {
        val tm = ctx.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
        when (tm.simState) {
            TelephonyManager.SIM_STATE_READY -> {
                // SIM is ready - schedule delayed work
                Handler(Looper.getMainLooper()).postDelayed({
                    // Trigger task condition check
                }, 5_000L)   // DELAY_TIME_AFTER_SIM_READY
            }
            TelephonyManager.SIM_STATE_ABSENT -> {
                // SIM removed - update shared prefs
            }
        }
    }
}, filter)

Reading Stored SIM State

Access the centralized state values maintained by TaskUtils:

val simReady = SharedPreference(SP_SIM_STATE, 0).get()   // 5 = ready, 1 = removed
val dataSlot = SharedPreference(SP_DATA_SIM_SLOT, 0).get()

Applying SIM Slot Filters

Validate SIM slot requirements before executing forwarding tasks, matching the logic in SendWorker:

val targetSlot = "SIM${msgInfo.simSlot + 1}"           // e.g., "SIM1"
if (ruleSetting.simSlot != CHECK_SIM_SLOT_ALL && targetSlot != ruleSetting.simSlot) {
    // Skip this message - rule requires different SIM
    return
}
// Proceed to send via the chosen Subscription ID

Summary

SmsForwarder's dual SIM detection and handling mechanism combines three distinct architectural layers to ensure reliable operation:

  • System broadcasts (SimStateReceiver) detect physical SIM changes and enforce initialization delays
  • Shared preference tracking (TaskUtils) maintains current state for both SIM readiness and active data slot selection
  • Rule enforcement (SendWorker) compares message origin against user-configured slot filters using constants from NetworkChangeReceiver and Constants.kt

Frequently Asked Questions

How does SmsForwarder detect when a SIM card is inserted or removed?

The app registers SimStateReceiver to listen for android.intent.action.SIM_STATE_CHANGED broadcasts from the Android system. When triggered, the receiver queries TelephonyManager for the current SIM state and updates shared preferences accordingly, storing 5 for ready states and 1 for absent states.

What is the purpose of the 5-second delay after SIM becomes ready?

The DELAY_TIME_AFTER_SIM_READY delay (5 seconds) ensures the SIM card completes network registration and initialization before SmsForwarder attempts to use it for forwarding operations. This prevents failed sends that would occur if the app tried to transmit immediately when the SIM transitions to ready state.

How does the app determine which SIM slot is currently active for data?

NetworkChangeReceiver calls TelephonyManager.getDataSimSlotIndex() whenever network connectivity changes occur. It stores this 0-based index in shared preferences via TaskUtils.setDataSimSlot(), allowing the system to track which physical slot currently handles mobile data separate from voice/SMS operations.

Can users filter forwarded messages based on specific SIM slots?

Yes. The forwarding rules support CHECK_SIM_SLOT_1, CHECK_SIM_SLOT_2, and CHECK_SIM_SLOT_ALL constants defined in Constants.kt. When SendWorker processes a message, it constructs a slot identifier string from msgInfo.simSlot and compares it against the rule's requirement, skipping the task if the origin slot does not match the configured filter.

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 →