How SmsForwarder Implements Call Logging and Call Event Forwarding: PhoneUtils and CallReceiver Analysis
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. 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.
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, 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). 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.
// 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), 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.
// 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). 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) queries Android'sCallLog.Callscontent provider with support for pagination, filtering, and manufacturer-specific SIM detection viagetCallInfoList()andgetLastCallInfo(). - CallReceiver (
CallReceiver.kt) extendsPhoneStateReceiverto capture real-time call events through specific callbacks for incoming, outgoing, and missed calls. - SettingUtils (
SettingUtils.kt) stores user-configurable boolean flags (enableCallType1throughenableCallType6) 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) provides an HTTP API at/call/queryfor 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) 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 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.
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 →