Skip to content

remappingBrilliance/SMSAPI

Repository files navigation

SMSAPI — Android SMS/MMS/Call Gateway

A native Android application that acts as a transparent gateway between a physical SIM card and a REST API. It receives incoming SMS/MMS and forwards them to your server, polls your server for outbound messages to send, and logs call metadata (CDR) and call recordings.

Designed to run silently in the background as a persistent foreground service.


Features

  • Receive SMS/MMS — incoming messages are POSTed to your server immediately
  • Send SMS/MMS — polls your server for queued outbound messages and delivers them via the device's SIM
  • Delivery status — reports send success/failure back to your server
  • Call Detail Records (CDR) — logs inbound, outbound, and missed calls with timestamps and duration
  • Call recording upload — uploads call recordings produced by the device's dialer app

Requirements

  • Android device with a SIM card
  • Android 16 (API 36) or higher
  • Your own REST server implementing the API endpoints below

Setup

1. Build configuration

Copy secrets.properties.example to secrets.properties and fill in your server details:

api_host=your-api-host.example.com
api_key=your-api-key-here

These values are injected at build time via BuildConfig and used as the default values in the app's settings screen. They are never committed to source control.

2. Build and install

./gradlew assembleDebug
adb install app/build/outputs/apk/debug/app-debug.apk

Or open in Android Studio and run directly to a device.

3. Grant permissions

Open the app. The main screen shows the status of every required permission:

Permission Purpose
Receive / Read / Send SMS SMS gateway core
Read Phone State + Phone Numbers Device number, call state detection
Read Call Log Outgoing number resolution, CDR
Read Media Audio Access dialer's call recording files
Post Notifications Foreground service notification
Draw Over Other Apps Keeps the service process alive during calls

Tap Request Permissions to prompt for all runtime permissions at once. Tap Grant Overlay to open the system overlay permission screen.

4. Set as default SMS app

For background MMS sending (no user interaction required), this app must be the device's default SMS app. Tap Set as Default SMS App on the main screen.

If not set as default, MMS will fall back to launching the system messaging app, which requires the user to manually tap Send.

5. Configure your server details

Enter your API host and key in the API Settings section. All endpoint paths are configurable and default to the paths documented below.

Tap Save Settings, then Test API Connection to verify connectivity.


How It Works

SMS/MMS receiving

SmsReceiver is a BroadcastReceiver registered for SMS_DELIVER (when default SMS app) and SMS_RECEIVED. When a message arrives, Android delivers all segments of a multipart SMS in a single broadcast as an array of PDUs. The app concatenates them into a single body before uploading, so your server always receives the complete message.

MMS messages are observed via MmsObserver, which watches the system MMS content provider and forwards new messages to the same /sms/receive endpoint with attachments encoded as base64.

SMS/MMS sending

PollingForegroundService runs a continuous polling loop (configurable interval, default 5 seconds). It calls GET /sms/send on your server, which returns a list of queued messages. Each message is dispatched via MessageSender, which routes to either SmsSender (standard Android SMS API) or MmsSender (WAP-209 PDU encoding). Delivery results are reported back via POST /sms/status.

The polling service is a dataSync foreground service that starts on boot and restarts if killed by the system.

Call Detail Records

CallReceiver is a BroadcastReceiver for ACTION_PHONE_STATE_CHANGED. It runs a state machine:

IDLE → RINGING     incoming call arrives, number captured
RINGING → OFFHOOK  call answered
IDLE → OFFHOOK     outgoing call dialled
OFFHOOK → IDLE     call ended → POST CDR
RINGING → IDLE     missed call → POST CDR (state: "missed")

For outgoing calls, the number is not available in the broadcast on Android 10+. The app waits ~2.5 seconds after the call ends and resolves the number from the CallLog.

Call recording

Note: Direct audio capture from phone calls (AudioSource.VOICE_COMMUNICATION) is blocked for non-system apps on stock Android (AOSP, GrapheneOS, Pixel). The Android audio policy manager restricts in-call capture to privileged processes. Any recording made this way will be silent.

The app therefore relies on the device's built-in dialer to produce the recording file. The dialer (a system/privileged app) can record both call legs and saves files to Recordings/CallRecordings/ on shared storage.

Two options for triggering dialer recording:

Option A — Accessibility auto-tap (GrapheneOS dialer, some OEMs): Enable Record call content and Use Accessibility Services to try to press Record on every call in the app settings, then enable the Call Recording Assistant in Android Accessibility settings.

When a call is answered, CallRecordingAccessibilityService monitors the dialer's window events and performs a programmatic click on any button whose content description contains "record" (but not "stop"). This works reliably with the GrapheneOS dialer. The Google dialer does support a similar Record button but has an unavoidable system announcement to both parties ("This call is being recorded") and requires every contact to be individually opted in or "Record all unknown callers" to be enabled — making the accessibility tap less useful there.

Option B — Manual or automatic dialer recording: Configure your dialer to record all calls automatically (where supported), then enable only Record call content in the app settings (leave the accessibility option off). The app will still pick up and upload any recording file it finds in CallRecordings/ after the call ends.

In both cases, after the call ends the app waits ~5 seconds for the dialer to finish writing the file, then queries MediaStore for an audio file in Recordings/CallRecordings/ with a DATE_ADDED timestamp at or after the call was answered. If found, it uploads the file via POST /call/recording and the local copy can be deleted.

Tap Upload Existing Recordings on the main screen to manually upload any files already sitting in CallRecordings/ that were never uploaded (e.g. from before the app was installed).


API Reference

All endpoints use HTTPS. All requests include:

Authorization: Bearer <api_key>

Base URL

https://{api_host}

Endpoint paths are fully configurable in the app. Defaults are shown below.


POST /api/v1/public/sms/receive

Called when the device receives an incoming SMS or MMS.

Request Content-Type: application/json

{
  "message": {
    "from": "+61400000000",
    "to": "+61411111111",
    "body": "Hello, world!",
    "type": "text",
    "timestamp": "Tue Jan 01 12:00:00 GMT+10:00 2008",
    "attachments": [
      {
        "filename": "image.jpg",
        "contentType": "image/jpeg",
        "data": "<base64>"
      }
    ]
  },
  "metadata": {
    "receivedAt": "Tue Jan 01 12:00:00 GMT+10:00 2008",
    "sender": "+61400000000",
    "recipient": "+61411111111"
  }
}
  • type: "text" for SMS, "mms" for MMS
  • attachments: present for MMS only; omitted for plain SMS
  • timestamp: the timestamp from the SMS PDU (sender's carrier time)

Response — any 2xx is treated as success.


GET /api/v1/public/sms/send

Polled by the device on a configurable interval (default every 5 seconds) to retrieve queued outbound messages.

Request — no body; Authorization header only.

Response Content-Type: application/json

[
  {
    "id": "msg_abc123",
    "to": "+61400000000",
    "body": "Your verification code is 1234",
    "type": "sms"
  }
]
  • Return an empty array [] when there are no messages queued.
  • type: "sms" or "mms"
  • For MMS, include an attachments array matching the same structure as the receive payload.
  • id is echoed back in the status update so you can correlate delivery results.

POST /api/v1/public/sms/status

Called after each send attempt to report delivery status.

Request Content-Type: application/json

{
  "id": "msg_abc123",
  "status": "sent",
  "error": null
}
  • status: "sent" on success, "failed" on error
  • error: human-readable error string on failure, null on success

Response — any 2xx is treated as success.


POST /api/v1/public/call/cdr

Called at the end of every call (answered or missed) when CDR logging is enabled.

Request Content-Type: application/json

{
  "call": {
    "state": "ended",
    "direction": "inbound",
    "phoneNumber": "+61400000000",
    "startedAt": 1700000000000,
    "answeredAt": 1700000060000,
    "endedAt": 1700000120000,
    "durationSeconds": 60
  },
  "device": {
    "deviceManufacturer": "google",
    "deviceModel": "Pixel 8",
    "deviceBrand": "google",
    "androidSdkVersion": 36,
    "androidRelease": "16"
  },
  "loggedAt": "2024-11-15T10:41:07Z"
}
Field Notes
state "ended" for answered calls, "missed" for unanswered inbound calls
direction "inbound" or "outbound"
startedAt Epoch ms — RINGING time for inbound, OFFHOOK time for outbound
answeredAt Epoch ms — OFFHOOK time; 0 for missed calls
endedAt Epoch ms
durationSeconds (endedAt - answeredAt) / 1000; 0 for missed calls

Response — any 2xx is treated as success.


POST /api/v1/public/call/recording

Called after an answered call when a recording file is found. Sent as multipart/form-data.

Fields:

Field Type Description
recording File Audio file produced by the dialer (M4A/MP4/AAC)
call String JSON-encoded object containing the same fields as the CDR payload, plus call_id

call JSON structure:

{
  "call_id": "550e8400-e29b-41d4-a716-446655440000",
  "state": "ended",
  "direction": "inbound",
  "phoneNumber": "+61400000000",
  "startedAt": 1700000000000,
  "answeredAt": 1700000060000,
  "endedAt": 1700000120000,
  "durationSeconds": 60,
  "device": {
    "deviceManufacturer": "google",
    "deviceModel": "Pixel 8",
    "deviceBrand": "google",
    "androidSdkVersion": 36,
    "androidRelease": "16"
  }
}

The server should validate the recording file as mimes:mp4,m4a,aac.

Response — any 2xx is treated as success. After a successful upload the local recording file is deleted from the device.


Settings Reference

All settings are persisted in SharedPreferences and configurable from the app's main screen.

Setting Default Description
API Host (from secrets.properties) Hostname only, no scheme or trailing slash
API Key (from secrets.properties) Sent as Authorization: Bearer on every request
Poll Interval 5s How often to check for outbound messages (1s–60s)
Poll for outbound messages On Disable to stop outbound polling without stopping the service
Connect Timeout 10,000 ms HTTP connection timeout
Read Timeout 30,000 ms HTTP read timeout
Max Retries 3 Retry attempts on transient network errors
Receive Endpoint /api/v1/public/sms/receive Path for incoming SMS/MMS
Outbound Endpoint /api/v1/public/sms/send Path for polling outbound queue
Status Endpoint /api/v1/public/sms/status Path for delivery status
Call CDR Endpoint /api/v1/public/call/cdr Path for call metadata
Call Recording Endpoint /api/v1/public/call/recording Path for recording upload
Record call metadata Off Enable CDR logging
Record call content Off Enable recording upload
Use Accessibility Services to press Record Off Auto-tap the dialer's Record button on answer

Architecture Notes

  • The app targets Android 16 (API 36) and does not include legacy version checks.
  • PollingForegroundService is a dataSync foreground service that also holds the microphone service type when started from the foreground (app opened by user). This type is required on API 31+ to start a microphone-capable foreground service — it cannot be started from a background context such as a boot receiver.
  • CallRecordingService.java is kept in the source tree and compiles, but is not used at runtime. All recording orchestration goes through the dialer via the accessibility service.
  • Outgoing call numbers are not available in ACTION_PHONE_STATE_CHANGED broadcasts on API 29+. The app resolves them by querying CallLog.Calls approximately 2.5 seconds after the call ends.
  • MMS sending uses WAP-209 PDU encoding via the android-smsmms library and requires the app to be the default SMS handler for background (silent) delivery.

About

An Android Application which posts call logs, call recordings and turns your phone into a SMS / MMS bidirectional gateway to REST APIs

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages