nigig-org/crates/robius-sms
nigig-ci cbe47a5f6c
Some checks failed
doc-engine / engine (push) Waiting to run
doc-engine / consumer (push) Waiting to run
nigig-map / test (push) Waiting to run
sms / gates (push) Waiting to run
sms / robius-sms (push) Waiting to run
sms / android (push) Waiting to run
sms / nigig-sms (push) Waiting to run
sms / supply-chain (push) Waiting to run
repo hygiene / hygiene (push) Has been cancelled
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
perf(sms): close the outstanding Phase B and D items
An audit of Phase 0 through D against the tree found three tasks marked
done in prose but absent from the code. This closes them.

D5 -- bulk send still blocked the UI thread.

  The send loop ran synchronously in handle_send_bulk.
  SmsManager.sendTextMessage queues to the radio and rate-limits, so a
  200-recipient batch was a multi-minute ANR with no progress and no
  way to tell whether anything was happening. Phase E8's throttle
  bounded the worst case at 30 sends, but bounded blocking is still
  blocking -- and I said as much when deferring it.

  Now uses the worker pattern D1 established: spawn, publish progress
  through a mutex-guarded slot, SignalToUI, drain on the UI thread. The
  status line counts up ("Sending 12/200…") instead of freezing.
  BULK_SEND_IN_FLIGHT prevents two overlapping batches.

D2 -- the ContentObserver, the half I left open.

  Phase D removed the 5-second poll that re-armed itself via redraw()
  and stopped the app ever idling. That fixed the busy loop but left a
  gap I documented rather than closed: a message arriving while the app
  was open did not surface until the next Resume or manual pull.

  Adds SmsInboxObserver.java -- a ContentObserver on content://sms,
  registered with a main-Looper Handler, idempotent so onResume can call
  it freely -- compiled and dexed by the existing build.rs pipeline and
  loaded through the same in-memory dex loader as the receivers.

  onChange calls into Rust, which does two cheap things: set an atomic,
  and invoke a registered waker. The waker matters. robius-sms has no UI
  dependency and cannot call SignalToUI itself, so without it the flag
  would only be observed on the next event-loop turn that happened for
  some other reason -- which, with the poll gone, might be never while
  the app sits idle. The app registers SignalToUI::set_ui_signal, so
  this is a genuine push.

  Native binding is dynamic, not #[no_mangle], for the same reason as
  E6: the class comes from an in-memory dex and is not on the JVM's
  search path.

B0 (wider) -- 23 crates declared robius-sms and never called it.

  Phase B removed the three declarations that put RUSTSEC advisories on
  nigig-build and explicitly flagged the rest as "the same latent
  problem, sweep separately". This is that sweep: every crate with zero
  references to robius_sms in its sources loses the dependency.

  nigig-mpesa, nigig-pay and nigig-sms keep it -- they are the only real
  users. nigig-system-prefs only mentions robius-sms in its package
  description, so its manifest is untouched.

  Cargo.lock loses another 21 lines.

Verified: 13/13 CI checks. clippy -D warnings clean on host and
aarch64-linux-android; the Android build compiles, javac-builds and
dexes the new observer class. cargo deny still "advisories ok, bans ok,
licenses ok, sources ok". clippy ratchet holds at 49. Sampled four of
the 23 stripped crates plus all five I edited; all build.

Pre-existing and unrelated: nigig-map fails to compile on pristine
origin/main (12 errors in view.rs, a Script/Widget derive problem), so
pageflipnav and anything else reaching it cannot be checked here. I
touched no files under crates/apps/map.

NOT verified on a device. The observer's registration, the onChange
callback and the waker all need an emulator or handset with a live SMS
provider; this sandbox has neither, and there is still no CI runner.
2026-08-02 07:28:01 +00:00
..
src perf(sms): close the outstanding Phase B and D items 2026-08-02 07:28:01 +00:00
.gitignore added missing crates 2026-07-26 21:22:21 +03:00
Cargo.toml security(sms): stop persisting message bodies, throttle sends (Phase E) 2026-08-01 04:58:46 +00:00
README.md fix(sms): send long messages whole, and confirm before bulk (A5, A7, A8) 2026-07-31 22:22:27 +00:00

robius-sms

A Rust library to access and send SMS messages across multiple platforms.

Currently, advanced SMS functionality is implemented on Android only.

Android capabilities

This crate currently supports:

  • sending a single SMS
  • sending bulk SMS to multiple recipients
  • listing SMS messages from the device SMS database
  • checking/requesting SMS permissions
  • scheduling recurring SMS sends on Android

Android manifest permissions

Add these to your AndroidManifest.xml:

<manifest ... >
  <uses-permission android:name="android.permission.SEND_SMS" />
  <uses-permission android:name="android.permission.READ_SMS" />
  <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

  <!-- Only needed if you call schedule_sms(). Android 12+ refuses exact
       alarms without it, and it is special-access: the user must enable
       "Alarms & reminders" in Settings, a runtime prompt cannot grant
       it. Without it, scheduled sends are batched by Doze. -->
  <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />

  <application ...>
    <receiver
      android:name="robius.sms.SmsAlarmReceiver"
      android:exported="false" />

    <receiver
      android:name="robius.sms.SmsBootReceiver"
      android:enabled="true"
      android:exported="false">
      <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED" />
      </intent-filter>
    </receiver>
  </application>
</manifest>

Standard iOS (stubs)

cargo build --target aarch64-apple-ios

TrollStore iOS (silent SMS + message reading)

cargo build --target aarch64-apple-ios --features trollstore

Delivery is not confirmed

send_sms returning Ok means the send was handed to the platform, not that the message was delivered. Both the sent and delivery PendingIntents are null, so there is nothing to report back. Do not show Ok to a user as "delivered".

Cost

You are billed per segment. Call segment_count(body) before sending: any character outside the GSM-7 alphabet -- one emoji is enough -- forces the whole message to UCS-2, dropping the per-part limit from 160 characters to 70. Bodies longer than one part are sent with sendMultipartTextMessage.

Note that the accented Latin characters common in Swahili, French and German (e, a, o, n, u with diacritics) ARE in GSM-7 and do not trigger this.