# How SmsForwarder Implements Call Logging and Call Event Forwarding: PhoneUtils and CallReceiver Analysis

> Discover how SmsForwarder implements call logging and event forwarding using PhoneUtils and CallReceiver. Learn to track call details and forward events efficiently.

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

---

**SmsForwarder records call details by querying Android's `CallLog.Calls` content provider through the `PhoneUtils` utility class, captures real-time call state changes via the `CallReceiver` broadcast receiver, and forwards events through background `SendWorker` tasks based on per-type user settings in `SettingUtils`.**

SmsForwarder (pppscn/SmsForwarder) is an open-source Android application that automatically forwards SMS and call notifications to multiple channels. The call handling pipeline combines content provider queries with broadcast receivers to capture both historical call logs and real-time telephony events. This article examines the implementation details found in the Kotlin source code, including the specific utility classes and worker threads that handle message dispatch.

## Querying Android's Call Log with PhoneUtils

The foundation of call logging in SmsForwarder relies on the **PhoneUtils** class located in [`app/src/main/kotlin/cn/ppps/forwarder/utils/PhoneUtils.kt`](https://github.com/pppscn/SmsForwarder/blob/main/app/src/main/kotlin/cn/ppps/forwarder/utils/PhoneUtils.kt). This utility encapsulates all interactions with Android's `CallLog.Calls` content provider, abstracting the cursor management and column mapping required to extract meaningful call data.

### Retrieving Call History with getCallInfoList()

The `getCallInfoList()` method constructs dynamic SQL-style queries to filter by call type, phone number, and pagination parameters. It iterates over the returned cursor, mapping each row to a **CallInfo** data object that includes timestamps, duration, and number information. For scenarios requiring only the most recent entry, `getLastCallInfo()` provides a convenience wrapper that returns the latest record matching specific criteria such as call type or phone number.

```kotlin
val outgoingCalls = PhoneUtils.getCallInfoList(
    type = CallLog.Calls.OUTGOING_TYPE, // 2 = outgoing
    limit = 20,
    offset = 0,
    phoneNumber = null
)
outgoingCalls.forEach { Log.d("Call", it.toString()) }

```

### Detecting SIM Slot and Manufacturer Details

Beyond basic call metadata, `PhoneUtils` detects **SIM slot IDs** and distinguishes between device manufacturers like Xiaomi and Huawei through manufacturer-specific logic. The utility also captures whether the call was forwarded via the `forward` column in the call log database, enriching the data available for downstream processing.

## Capturing Real-Time Call Events

While `PhoneUtils` handles historical data, real-time event detection depends on Android's telephony broadcast system. The application registers a **CallReceiver** that extends `PhoneStateReceiver` to intercept call state changes as they occur.

### The CallReceiver Broadcast Handler

Located in [`app/src/main/kotlin/cn/ppps/forwarder/receiver/CallReceiver.kt`](https://github.com/pppscn/SmsForwarder/blob/main/app/src/main/kotlin/cn/ppps/forwarder/receiver/CallReceiver.kt), this component receives Android broadcast intents for incoming, outgoing, answered, ended, and missed calls. The class overrides specific callbacks including `onIncomingCallReceived`, `onIncomingCallAnswered`, `onIncomingCallEnded`, `onOutgoingCallStarted`, `onOutgoingCallEnded`, and `onMissedCall`. Each callback logs the event and initiates the forwarding decision process.

### Filtering Events by Type

Forwarding decisions respect user preferences stored in **SettingUtils** ([`app/src/main/kotlin/cn/ppps/forwarder/utils/SettingUtils.kt`](https://github.com/pppscn/SmsForwarder/blob/main/app/src/main/kotlin/cn/ppps/forwarder/utils/SettingUtils.kt)). The application maintains six boolean flags (`enableCallType1` through `enableCallType6`) that correspond to different call event categories. Before triggering any forwarding action, `CallReceiver` checks the appropriate flag to determine whether the specific event type should be processed.

```kotlin
// Turn on missed-call forwarding (type 3) from UI or API
SettingUtils.enableCallType3 = true

```

## Forwarding Call Notifications

Once a call event passes the filtering stage, SmsForwarder processes it through two distinct pathways: immediate notices for active calls and detailed records for completed calls.

### Sending Immediate Call Notices

For active call events, the `sendNotice()` method builds a short text summary combining the contact name and call type. This content wraps into a **MsgInfo** object with `type="call"`. The system schedules a `OneTimeWorkRequest` that executes **SendWorker** ([`app/src/main/kotlin/cn/ppps/forwarder/workers/SendWorker.kt`](https://github.com/pppscn/SmsForwarder/blob/main/app/src/main/kotlin/cn/ppps/forwarder/workers/SendWorker.kt)), which serializes the message to JSON using Gson and dispatches it through configured channels such as Telegram, ServerChan, or Bark.

### Processing Detailed Call Records

For completed calls, the `sendCallMsg()` method introduces a brief delay to ensure the call log entry is written to the database. It then retrieves the latest `CallInfo` via `PhoneUtils.getLastCallInfo()`, enriches it with SIM slot information and contact names, and constructs a detailed `MsgInfo` containing call duration and SIM-specific data. Like notices, these records queue through `SendWorker` for asynchronous delivery.

```kotlin
// Inside a Worker or Service
val recent = PhoneUtils.getLastCallInfo(
    callType = CallLog.Calls.INCOMING_TYPE,   // 1 = incoming
    phoneNumber = null                        // null = any number
)

recent?.let { info ->
    val msg = PhoneUtils.getCallMsg(info)   // formats a human-readable string
    val msgInfo = MsgInfo(
        type = "call",
        address = info.number,
        content = msg,
        time = Date(),
        simInfo = "",                         // optional SIM tag
        simSlot = info.simId,
        subId = info.subId,
        callType = info.type
    )
    val work = OneTimeWorkRequestBuilder<SendWorker>()
        .setInputData(workDataOf(Worker.SEND_MSG_INFO to Gson().toJson(msgInfo)))
        .build()
    WorkManager.getInstance(context).enqueue(work)
}

```

## HTTP API for External Call Queries

SmsForwarder exposes internal call log functionality through an HTTP interface implemented in **CallController.kt** ([`app/src/main/kotlin/cn/ppps/forwarder/server/controller/CallController.kt`](https://github.com/pppscn/SmsForwarder/blob/main/app/src/main/kotlin/cn/ppps/forwarder/server/controller/CallController.kt)). The `/call/query` endpoint allows external applications to retrieve paginated call history by internally invoking `PhoneUtils.getCallInfoList`, enabling integration with third-party automation tools.

## Summary

- **PhoneUtils** ([`PhoneUtils.kt`](https://github.com/pppscn/SmsForwarder/blob/main/PhoneUtils.kt)) queries Android's `CallLog.Calls` content provider with support for pagination, filtering, and manufacturer-specific SIM detection via `getCallInfoList()` and `getLastCallInfo()`.
- **CallReceiver** ([`CallReceiver.kt`](https://github.com/pppscn/SmsForwarder/blob/main/CallReceiver.kt)) extends `PhoneStateReceiver` to capture real-time call events through specific callbacks for incoming, outgoing, and missed calls.
- **SettingUtils** ([`SettingUtils.kt`](https://github.com/pppscn/SmsForwarder/blob/main/SettingUtils.kt)) stores user-configurable boolean flags (`enableCallType1` through `enableCallType6`) that control which call event types trigger forwarding.
- Messages are encapsulated in **MsgInfo** objects and processed asynchronously through **SendWorker**, which handles JSON serialization and channel-specific dispatch to Telegram, WeWork, Bark, and other senders.
- The **CallController** ([`CallController.kt`](https://github.com/pppscn/SmsForwarder/blob/main/CallController.kt)) provides an HTTP API at `/call/query` for external retrieval of call logs.

## Frequently Asked Questions

### How does SmsForwarder access call history without root permissions?

SmsForwarder reads call history through Android's standard `CallLog.Calls` content provider, which requires the `READ_CALL_LOG` permission but does not need root access. The `PhoneUtils` class manages the cursor iteration and column mapping required to extract call details, numbers, timestamps, and duration from the system's public database.

### What is the difference between call notices and call records in SmsForwarder?

Call notices are generated immediately when a call event occurs (incoming, outgoing, or missed) and contain basic information like contact name and call type. Call records are processed after a brief delay to ensure the system has written the complete call log entry, containing detailed metadata including exact duration, SIM slot ID, and whether the call was forwarded.

### Where does SmsForwarder store the settings for which call types to forward?

The per-call-type enable flags are stored in **SettingUtils** ([`app/src/main/kotlin/cn/ppps/forwarder/utils/SettingUtils.kt`](https://github.com/pppscn/SmsForwarder/blob/main/app/src/main/kotlin/cn/ppps/forwarder/utils/SettingUtils.kt)) as boolean properties `enableCallType1` through `enableCallType6`. These correspond to different telephony events (incoming, outgoing, missed, etc.) and are checked by `CallReceiver` before initiating any forwarding action.

### Can external applications query SmsForwarder's call log database?

Yes, the [`CallController.kt`](https://github.com/pppscn/SmsForwarder/blob/main/CallController.kt) exposes an HTTP endpoint at `/call/query` that allows authorized external requests to retrieve call history. This endpoint internally calls `PhoneUtils.getCallInfoList`, supporting the same pagination and filtering parameters available to the internal UI, enabling integration with automation platforms or external monitoring systems.