Skip to content

Repository files navigation

xsms

xsms

Self-hosted SMS ↔ Telegram bridge. Your phone runs everything; there is no server, no account, no cloud sync. Telegram is the UI.

Android Kotlin Release License

xsms main screen

What it does

When someone texts your phone, xsms forwards the message to your private Telegram bot. From Telegram you can reply, browse your full SMS inbox and call log, search across every conversation, manage contacts, mute numbers, and export anything to CSV — all from any device where you're signed into Telegram. SMS sending uses the SIM you pick (handy on dual-SIM phones).

The whole bridge runs on the Android device in your pocket. Nothing leaves the phone except direct, encrypted requests to api.telegram.org (optionally over a SOCKS5 proxy you control).

Features

  • Forward incoming SMS to one or more Telegram bots, with a tap-to-reply button.
  • Replays SMS that arrived while the bridge was offline once it reconnects, so nothing is missed.
  • Auto-reconnects the moment connectivity returns — no need to poke the bot to wake it up.
  • Send SMS back from Telegram. Dual-SIM picker when both are eligible. No country code required.
  • Browse inbox and call log; open any conversation with full pagination + sort.
  • Search across every conversation (body, contact name, address) with a live progress indicator.
  • Per-conversation, inbox-wide, call-log, and contacts CSV export.
  • Contact management — view, edit, create, mute — straight from the bot.
  • Mute a number even when it isn't a saved contact.
  • Full backup & restore — save every setting (bots, users, permissions, filters, mute lists) to one file, from the app or the bot, and restore it on any phone.
  • Per-user permission system: 10 granular permissions × N allowed users per bot.
  • Per-user sender filter — forward to a user from only certain numbers, or everyone except some (pick from contacts, no country code needed).
  • Per-user SIM scoping — limit a user to a SIM and they only see (and send from) that SIM.
  • In-app updates — checks GitHub for a newer release and installs it for you.
  • Per-bot SOCKS5 proxy with username/password and remote DNS (for regions where Telegram is blocked).
  • Foreground service + WorkManager watchdog + boot receiver for survival on aggressive OEMs (Samsung One UI, MIUI, EMUI, OxygenOS, ColorOS, FuntouchOS).
  • True-black OLED dark theme; Material 3 throughout.

Requirements

  • Android 7.0 Nougat (API 24) or later
  • A Telegram account for creating the bot (free, via @BotFather)
  • Optional: a SOCKS5 proxy if api.telegram.org is blocked in your region

Setup

1. Install the app

Open the Releases page on the phone that will host the bridge, download xsms-v1.5.apk, and tap it. Android will ask you to allow installs from unknown sources the first time — that's normal for sideloaded apps. Or build from source — see the technical section below. After this first install, xsms can update itself in-app.

2. Create a Telegram bot

On Telegram, open a chat with @BotFather:

  1. Send /newbot.
  2. Pick a display name and a @username for the bot.
  3. BotFather returns an API token (123456789:ABC-DEF1234...). Keep it safe.

3. Add the bot to xsms

On the main screen tap + Add bot. Give the bot a nickname (just for the xsms UI) and paste the token.

Add bot — Profile / Credentials / Allowed users

Then add at least one Allowed user:

  • Find your own Telegram user ID by chatting with @userinfobot.
  • Type that ID into the User ID field and tap Add.
  • Pick the permissions you want this user to have (Receive SMS, Send SMS, SMS Inbox, Call Log, Contacts, Add Contact, Edit Contact, Mute Contact, Backup, Restore).
  • On dual-SIM phones, choose which SIMs the user is allowed to send from.

Leaving the allowed-users list empty means anyone who finds the bot can use it with full access — only do this for private testing.

Add bot — SIMs / SOCKS5 / Test bot

If your country blocks api.telegram.org, scroll to SOCKS5 proxy and enter your proxy host, port, and (optionally) credentials. Tap Test bot to verify the connection works end-to-end before saving.

4. Grant permissions

Back on the home screen, tap the Permissions tile and grant: SMS (receive/read/send), Call Log, Contacts, and Phone State. The bot also checks these at action time, so you can revoke any of them at the OS level if you change your mind.

5. Enable Anti-Kill (recommended)

Tap the Anti-Kill tile. The wizard walks through five steps:

  1. Allow notifications — so the foreground service can show "xsms is active".
  2. Disable battery optimization for xsms.
  3. Allow auto-start on boot — xsms deep-links to the right OEM page (Samsung Device Care, MIUI Autostart, OPPO Startup Manager, etc.).
  4. Allow background activity.
  5. Lock the app in Recents so swipe-away doesn't kill it.

Toggle Auto-start after boot on if you want xsms to resume bridging automatically after a phone restart.

6. Start the bridge

Tap Start on the main hero card. The status dot goes green, the bot card flips to live, and the foreground notification appears.

7. Talk to your bot

Open Telegram, find the bot you created, send /start. The welcome card appears with the main menu; incoming SMS will start showing up here.

xsms bot in Telegram — /start menu + incoming SMS push

Tap ↩ Reply on any incoming SMS to text back, 🔇 Mute number to silence the sender for this bot, or use the main menu buttons (Send SMS, Inbox, 🔎 Search inbox, Call log, Contacts, Mute list, 💾 Backup, ♻️ Restore, Status) to explore.

8. Back up your setup (recommended)

Once everything works, save a backup. On the home screen tap Backup & Restore → Save backup file to write every setting — all bots and tokens, users, permissions, sender/SIM filters and mute lists, plus your app settings — to a single .json file you can keep anywhere. You can also do this from Telegram: the bot's 💾 Backup button sends you the same file. To move to a new phone (or recover), install xsms and use ♻️ Restore (in the app or the bot) to load the file back — it replaces everything with the backup's contents. Android runtime permissions (SMS, call log, contacts) aren't part of a backup; re-grant them on the Permissions screen afterward.


How it works — technical details

Stack

  • Kotlin 2.0.21, Jetpack Compose, Material 3
  • AGP 8.7.3, Gradle 8.10.2, JDK 21 (Android Studio bundled JBR — system JDK 22+ is too new for Gradle 8.10)
  • compileSdk 36, minSdk 24, targetSdk 36
  • androidx.navigation:navigation-compose for screens
  • androidx.work:work-runtime-ktx for the keep-alive watchdog
  • OkHttp + kotlinx.serialization for the Telegram Bot API
  • Custom SOCKS5 implementation (RFC 1928 + RFC 1929) layered into OkHttp via a SocketFactory

Three orthogonal lifecycles

xsms has three booleans the user toggles independently. None of them imply each other.

Toggle Owned by What it does
Bridge telegram/Bridge.kt + BridgePreferences Runs BotSupervisor, which spawns one BotRouter coroutine per enabled bot. This is what actually polls Telegram and processes SMS.
KeepAlive keepalive/KeepAlive.kt + KeepAlivePreferences A specialUse foreground service that holds the process alive. Paired with WatchdogWorker (WorkManager periodic, 15 min) which restarts the service if Android kills it.
Auto-start on boot KeepAlivePreferences.autoStartOnBoot Gates BootReceiver. When on, restores Bridge and/or KeepAlive after reboot — each independently, based on its own enabled flag.

A bridge running without keep-alive is a legitimate combination (in-foreground polling only, no persistent notification). Bridge off + keep-alive on is also fine (the foreground service is just the process holder).

Application.onCreate deliberately does not restore subsystems — it runs on every process spawn including background-receiver-triggered ones, which would bypass the auto-start toggle. User-initiated restoration is handled by MainActivity.onCreate (guarded by a per-process subsystemsRestored flag so rotation doesn't double-fire). Boot-time restoration is handled by BootReceiver, gated on the toggle.

Bot router

Each enabled bot has its own BotRouter coroutine inside BotSupervisor. Lifecycle:

  1. getMe probe — one-shot liveness check so the UI card flips to live immediately. (The long-polling getUpdates blocks up to 25 seconds with no traffic, which would otherwise delay the first status update.)
  2. setMyCommands — registers /start in the bot's command dock (idempotent).
  3. Long-poll loopgetUpdates with the offset cursor, indefinitely.
  4. Backoff on failure — 5 s → 10 s → 30 s → 60 s capped, reset on the next successful poll. State surfaces as BotStatus.Failing(reason, nextRetryAtMs, totalWaitMs) so the app card can render a draining countdown.
  5. CancellationException always re-throws first — earlier, a generic catch (e: Exception) in TelegramApi.execute was swallowing coroutine cancellation as TelegramResult.Network("...cancelled"), which the router then wrote to botStatus as a phantom "Failing". The fix is to re-throw CancellationException before any IOException / Exception catch.
  6. Connectivity-driven reconnectNetworkMonitor (a default-network callback) fires on the offline→online edge and calls BotSupervisor.kickAll(), which cancels the stale long-poll, evicts dead pooled connections, and relaunches each router on its existing instance (so the getUpdates offset and per-chat sessions survive). The polling read timeout is 35 s (just over the 25 s server long-poll) so a half-open socket is noticed quickly. Without this, a dropped connection could look stuck until something forced a fresh request.
  7. Missed-SMS replay — on first connect and after every reconnect, the router walks the SMS provider for inbox rows newer than a per-bot marker (MissedSmsTracker, capped to the last 24 h) and forwards anything that landed while it was offline, tagged "📩 Missed SMS". The marker advances per message, so a mid-replay failure resumes rather than skips. The marker is seeded to "now" on a bot's first ever connect, so existing history is never dumped.

The router reads BotConfig fresh on every access via AppState.getBot(botId). Edits made through the UI (token, permissions, proxy, muted numbers) reach the running router without a restart.

Per-user permissions

Each bot has a list of BotAllowedUser(userId, nickname?, permissions: Set<BotUserPermission>, allowedSimIds: Set<Int>). Ten permission flags:

Permission What it gates
ReceiveSms Receive push notifications for incoming SMS
SendSms Compose and send SMS (and tap Reply on a push)
SmsInbox Browse the inbox, open conversations, CSV export
CallLog Browse call log, CSV export
Contacts Browse contacts, see details, CSV export
AddContact Create new contacts
EditContact Edit existing contacts
MuteContact Mute/unmute numbers (raw or via contact)
BackupSettings Download a full settings backup file
RestoreSettings Restore all settings from an uploaded backup

Main menu, inline keyboards, and every callback handler all gate per-user. A SIM allowlist scopes a user to specific subscriptions — it filters which SIMs they can send from and which SIMs' incoming SMS they see (allowsSim); an empty set means all SIMs. Each user also has a sender filter (numberFilterMode ∈ Off / Include / Exclude + filterNumbers) so incoming SMS can be limited to — or hidden from — specific numbers per user; matching is fuzzy (PhoneNumberUtils.compare) and entries are normalized to E.164 against the SIM's region, so no country code is needed.

Defense in depth: bot-level perms are checked at the callback handler and the keyboard builder (so a stale button in an old message can't bypass gating), and at the Android-permission layer (requireOsPerm) before touching any repository.

SMS rendering

telegram/Html.kt is a small builder over Telegram-flavored HTML (b, i, u, s, a, code, pre, blockquote, tg-spoiler).

The visual style is blockquote cards — every list item, every message, every detail block lives inside a <blockquote>, which Telegram renders as a left-bar indented card. Headers are emoji + <b>title</b>; subtitles are italic; phone numbers / identifiers / paths use <code>. No heavy ━━━ dividers.

The conversation view packs cards into pages dynamically: each page is filled until the next card would push it over MAX_CONV_PAGE_CHARS = 3600 (Telegram's 4096 limit minus ~400 chars of headroom for the header + page indicator). A page may hold 20 short SMS or a single verbose one. Total page count varies with the data — and with the sort direction, since the packing runs over the sorted list.

A single SMS whose body alone exceeds the budget gets its own truncated page with a — message truncated; use ⬇ Export CSV for the full body — footer. The CSV export always walks every row, so it stays the source of truth.

SOCKS5 proxy

telegram/Socks5SocketFactory.kt implements RFC 1928 (auth-method negotiation + CONNECT) and RFC 1929 (username/password). DNS resolution is delegated to the proxy via ATYP=domain (the paired ProxyDns sends the raw hostname instead of resolving locally) — essential in regions where local DNS is hijacked or blocked.

Per-bot config in data/SocksProxyConfig.kt. TelegramApi.pair(proxy) caches OkHttp clients per proxy config in a HashMap<SocksProxyConfig, ClientPair>, with a separate @Volatile var directPair for the proxy=null case — ConcurrentHashMap rejects null keys, which broke the original cache.

Mute list

Bot-level only. Per-bot mutedNumbers: Set<String> in BotConfig. Inbound SMS dropped at BotRouter.pushIncomingSms via PhoneNumberUtils.compare(a, b) — deprecated but the API 34+ replacement PhoneNumberUtils.areSamePhoneNumber would require raising minSdk. Suppression consolidated in BotConfig.samePhone(a, b).

OS-level SMS blocking via BlockedNumberContract requires xsms to become the default SMS or default dialer app, which was an explicit non-goal. The feature is named "mute" rather than "block" because it only silences the bot's push — the SMS still reaches the Android inbox.

Toggle Mute / Unmute from: a push notification (🔇 Mute number), inside a conversation view, the dedicated Mute list main-menu screen, or a contact detail page. Both raw-number (CB_MUTE_NUM:<address>) and contact-id (CB_CONTACT_MUTE:<contactId>) paths route through AppState.toggleMutedNumbers.

In-app updates

update/UpdateManager.kt reads api.github.com/repos/Sir-MmD/xsms/releases/latest (this is a public repo, so no token is needed), compares the tag to BuildConfig.VERSION_NAME, and — when newer — downloads the release's .apk asset to the cache and hands it to the system package installer via a FileProvider (ACTION_VIEW, REQUEST_INSTALL_PACKAGES; routed through ACTION_MANAGE_UNKNOWN_APP_SOURCES the first time). The check is a direct connection, not routed through a bot's proxy. The download only installs cleanly over the existing app because release APKs are signed with the same key (see build and install). The UpdateCard on the main screen auto-checks once per launch and offers a manual re-check.

Backup & restore

data/SettingsBackup.kt serializes every persisted setting into one JSON document via BackupManager: the full List<BotConfig> (tokens, allowed users, per-user permissions, SIM allowlists, sender filters, muted numbers, proxy, enabled flags), plus theme, Anti-Kill step acknowledgements, banner dismissals, and autoStartOnBoot. app + schema are required, non-defaulted fields so an unrelated JSON fails to decode instead of silently deserializing into an empty backup that a replace-all restore would then use to wipe everything. OS runtime permissions can't be captured (Android grants them), and the live run-state toggles (is the bridge/service running right now) are deliberately excluded as session state rather than config.

Restore is replace-all: BackupManager.apply overwrites AppState.bots and all prefs, then a running bridge is refreshed so the new bot set takes effect. In the app it's Storage-Access-Framework file save/pick (CreateDocument / OpenDocument — no storage permission needed) behind a confirm dialog. In the bot it's two permission-gated menu buttons: 💾 Backup uploads the file via sendDocument; ♻️ Restore downloads an uploaded document (new getFile + file-download plumbing, since the bot previously only sent documents) and requires an explicit Confirm tap. A bot-side restore refreshes the bridge through BotSupervisor.requestRestart, which runs on a scope independent of the (about-to-be-cancelled) router coroutine that triggered it.

Anti-Kill

OEMs aggressively kill background apps. xsms covers the bases:

  • <service android:foregroundServiceType="specialUse" android:stopWithTask="false"> in the manifest, with PROPERTY_SPECIAL_USE_FGS_SUBTYPE declaring the persistent SMS↔Telegram bridge use case.
  • WatchdogWorker (15-min periodic WorkManager) restarts the service if Android schedules it dead.
  • BootReceiver handles BOOT_COMPLETED, LOCKED_BOOT_COMPLETED, MY_PACKAGE_REPLACED, and the OEM-specific QUICKBOOT_POWERON actions.
  • OemHelper deep-links to the right Settings page on Samsung / Xiaomi / Huawei / OnePlus / OPPO / Vivo / Asus for Auto-start, Background activity, and Lock-in-Recents. The OEM steps aren't programmatically auto-detectable, so the user grants each and toggles "I've done this" manually; the wizard advances on tap.

A previous attempt to use the gyf-dev/Cactus library was ditched — abandoned since 2022, all artifacts gone from Maven Central and JitPack.

Persistence

Three SharedPreferences files (intentionally fragmented per subsystem):

File Owner Keys
xsms_app AppState banner dismissals, OEM step toggles (step_AutoStart, step_BackgroundActivity, step_LockInRecents, plus the auto-detected ones)
xsms_bots BotConfigStore the list of BotConfig as JSON; migrations: legacy allowedUserIds: List<Long>allowedUsers: List<BotAllowedUser> (all perms granted); legacy blockedNumbersmutedNumbers; legacy BlockContact perm → MuteContact
keepalive_prefs KeepAlivePreferences enabled, autostart_on_boot
bridge_prefs BridgePreferences enabled

AppState.botStatus is a Compose SnapshotStateMap<String, BotStatus> — writes from BotRouter are observed by the UI without a separate Flow. Cleared on stop / supervisor cancel so stale failures don't linger across enable cycles.

Layout

app/src/main/kotlin/xsms/app/
├── MainActivity.kt              entry, edge-to-edge, theme, user-initiated subsystem restoration
├── XsmsApp.kt                   Application; loads disk state only (never restores)
├── data/
│   ├── AppState.kt              singleton in-memory state (bots, granted steps, theme, bot status)
│   ├── BotConfig.kt             token, nickname, allowed users, proxy, muted numbers
│   ├── BotAllowedUser.kt        per-user perms + nickname + SIM allowlist
│   ├── BotUserPermission.kt     the 10 perms enum
│   ├── BotStatus.kt             Live · Failing(reason, nextRetryAtMs, totalWaitMs)
│   ├── BotConfigStore.kt        JSON SharedPreferences persistence with schema migration
│   ├── SettingsBackup.kt        full-settings backup model + BackupManager (capture/apply)
│   ├── SocksProxyConfig.kt      proxy host/port/credentials
│   ├── AntiKillPermission.kt    the 5 anti-kill steps + OEM-aware rationale
│   └── CorePermission.kt        runtime permission groups (SMS, CallLog, etc.)
├── keepalive/
│   ├── KeepAlive.kt             public API (start/stop)
│   ├── KeepAliveService.kt      specialUse foreground service
│   ├── BootReceiver.kt          restart on BOOT_COMPLETED / package replaced
│   ├── WatchdogWorker.kt        15-min periodic; restarts service if dead
│   ├── KeepAlivePreferences.kt  enabled + autoStartOnBoot
│   └── OemHelper.kt             per-OEM Settings deep-links
├── telegram/
│   ├── Bridge.kt                public API for bot polling lifecycle
│   ├── BridgePreferences.kt     enabled flag
│   ├── BotSupervisor.kt         coroutine scope; one BotRouter per enabled bot
│   ├── BotRouter.kt             long-polling loop + state machine + all command handlers
│   ├── BotRouterBackup.kt       bot-side backup upload + restore download/apply
│   ├── BotSession.kt            per-chat state (Idle, BrowsingConversation, ComposingMessage, ...)
│   ├── BotKeyboards.kt          inline keyboard builders + callback prefixes
│   ├── TelegramApi.kt           OkHttp + serialization wrapper for the Bot API (incl. getFile + download)
│   ├── TelegramTester.kt        getMe + sendMessage probe used by the app's "Test bot" button
│   ├── Socks5SocketFactory.kt   RFC 1928 / 1929 SOCKS5 implementation
│   ├── ProxyDns.kt              ATYP=domain hint for remote DNS
│   ├── StatusReport.kt          /status command body builder
│   └── Html.kt                  Telegram HTML builder (blockquote-card style)
├── phone/                       SMS / call log / contacts / SIMs repositories + SmsReceiver/Sender
├── permissions/PermissionSync.kt unified ON_RESUME re-check
├── navigation/Navigation.kt     NavHost: main, add_bot, anti_kill, permissions, backup
└── ui/
    ├── theme/                   Material 3, true-black OLED palette
    ├── components/              hero card, status pills, bot card, banners, switcher
    └── screens/                 Main, AddBot, AntiKill, Permissions, Backup

Build and install

JAVA_HOME=/opt/android-studio/jbr ANDROID_HOME=$HOME/Android/Sdk \
  ./gradlew assembleRelease
adb install -r app/build/outputs/apk/release/app-release.apk
adb shell am start -n xsms.app/.MainActivity

Release builds require a keystore.properties at the repo root with storeFile, storePassword, keyAlias, keyPassword (gitignored). Without it Gradle still builds, just unsigned — fine for assembleDebug.

JAVA_HOME must point at a JDK ≤ 21 — Gradle 8.10 rejects 22+. Android Studio ships an embedded JBR at the path above; if you don't have Studio installed, point at any Adoptium / Temurin JDK 21.


Releases

Version Date Notes
v1.5 2026-07-13 Full backup & restore of all settings (bots, users, permissions, filters, mute lists) — from the app or the bot; two new bot permissions.
v1.4 2026-06-08 Mute & restrict now work for name-only senders (alphanumeric IDs, short codes); reconnects on screen-wake without opening the app.
v1.3 2026-05-30 Missed-SMS replay, per-user sender filter + SIM receive scoping, in-app updates, faster reconnect.
v1.2 2026-05-28 Inbox search, date/time on every SMS surface, visual polish, Home on push.
v1.1 2026-05-28 Mute list (replaces Block), contact names, pretty numbers, SIM info, refactor.
v1.0 2026-05-27 Initial public release.

Download xsms-v1.5.apk from the Releases page directly on your phone and tap it to install (Android will prompt you to allow installs from unknown sources the first time). Verify the package name is xsms.app after install. Subsequent updates can be done from inside the app.

License

xsms is released under the MIT License — © 2026 Sir-MmD.

About

Self-hosted SMS ↔ Telegram bridge.

Resources

Stars

11 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages