A clean, fast SMS and MMS app for Android. Free, open source and fully offline.
Layout · Build · Run on an emulator · Release build
A messaging app for Android that looks and feels like Google Messages. Clean SMS replaces your SMS app: it holds the
default SMS role (ROLE_SMS), so it receives, sends and stores every SMS and MMS on the device.
On first launch Clean SMS imports the system SMS/MMS store into its own Room database. After that, a content observer brings in new messages as they arrive. All UI reads from that local copy through Paging 3, so lists with 50,000+ messages scroll smoothly.
- SMS and MMS (photos, group chats). No RCS: Android has no public RCS API for third-party apps.
- Full-text search across all messages (SQLite FTS4).
- Archive, delete and mark-as-read from a long press.
- Notifications with quick reply and mark-as-read.
- System, light and dark themes, with Material You colours on Android 12+.
- No
INTERNETpermission. MMS goes through the system MMS service; nothing else leaves the device.
app/app/src/main/java/dev/codingcoffee/cleansms/
CleanSmsApp.kt # Application + AppContainer (manual DI)
MainActivity.kt # entry point, SENDTO / notification intents
HeadlessSmsSendService.kt # "respond via message" from the call screen
data/db/ # Room entities, DAOs, FTS table
data/sync/TelephonySync.kt # system provider -> Room import (full + incremental)
data/Messaging.kt # send / receive / read / archive / delete
data/mms/MmsService.kt # MMS send, download, persist
data/mms/pdu/ # minimal MMS PDU encoder/decoder
data/contacts/ # cached contact lookup and search
data/prefs/SettingsStore.kt # theme + sync state (SharedPreferences)
data/DebugSeeder.kt # debug-only: 50k fake messages
receivers/ # SMS_DELIVER, WAP_PUSH, send results, notification actions
notify/Notifier.kt # MessagingStyle notifications
ui/ # Compose screens
nix develop
cd app
./gradlew assembleDebug # build
./gradlew lintDebug # optionalavdmanager create avd -n pixel37 -d pixel_8 -k "system-images;android-37.0;google_apis;x86_64"
emulator -avd pixel37
adb devices # ensure device is running
cd app && ./gradlew installDebug
adb shell am start -n dev.codingcoffee.cleansms/.MainActivity
# in case its not killed
adb emu kill
emulator -list-avds
avdmanager delete avd -n <name>To type with your computer's keyboard, set hw.keyboard = yes in ~/.android/avd/<name>.avd/config.ini and
restart the emulator.
Open the app and set Clean SMS as the default SMS app. Then simulate incoming messages:
adb emu sms send 5551234 "hello" # new conversation
adb emu sms send 5551234 "are you free tonight?" # same thread, grouped bubble
adb emu sms send 5559876 "$(printf 'long %.0s' {1..60})" # multipart SMSThe emulator can't receive MMS. Test MMS on a real device with a SIM.
To attach a photo, push it into the emulator and rescan media so the photo picker sees it:
adb push ~/Pictures/test.jpg /sdcard/Pictures/
adb shell content call --method scan_volume --uri content://media --arg external_primaryTo check scrolling performance, open Settings → Debug → Seed 50,000 test messages. This writes only to Clean SMS's local database, not to the system SMS store; Remove test messages clears it. Judge smoothness on a release build: debug Compose builds are much slower.
To pull the local database from a debug build:
adb exec-out run-as dev.codingcoffee.cleansms cat databases/cleansms.db > cleansms.db
adb exec-out run-as dev.codingcoffee.cleansms cat databases/cleansms.db-wal > cleansms.db-walkeytool -genkeypair -v -keystore ~/.android/cleansms-release.jks \
-alias cleansms -keyalg RSA -keysize 4096 -validity 10000Back up the keystore. Updates must be signed with the same key; losing it means uninstalling, which wipes Clean SMS's local database (your messages stay in the system SMS store and are re-imported).
Add the credentials to ~/.gradle/gradle.properties (user-level, never the repo), then
chmod 600 ~/.gradle/gradle.properties:
CLEANSMS_STORE_FILE=/home/<you>/.android/cleansms-release.jks
CLEANSMS_STORE_PASSWORD=<password>
CLEANSMS_KEY_ALIAS=cleansms
CLEANSMS_KEY_PASSWORD=<password>Use an absolute path; Gradle doesn't expand ~. Without these properties the release APK is built unsigned.
nix develop
cd app
./gradlew assembleRelease
$ANDROID_HOME/build-tools/36.0.0/apksigner verify --print-certs app/build/outputs/apk/release/app-release.apkThe output is app/build/outputs/apk/release/app-release.apk. If it's named app-release-unsigned.apk, the
signing properties weren't picked up.
From a clean, up-to-date main inside nix develop, with a GITHUB_TOKEN that can write releases
(fine-grained, Contents: read and write, this repo only):
just release patch # or minor / majorThis bumps versionName and versionCode, builds the signed APK, commits and tags vX.Y.Z, pushes both,
and creates a GitHub release with generated notes and cleansms-vX.Y.Z.apk attached.
Debug and release builds are signed with different keys, so uninstall the debug build first. This wipes the app's data.
adb uninstall dev.codingcoffee.cleansms
adb install -r app/build/outputs/apk/release/app-release.apk
adb shell am start -n dev.codingcoffee.cleansms/.MainActivityLater release builds install over the previous one with adb install -r, keeping data.
With USB debugging on: adb install -r app/build/outputs/apk/release/app-release.apk. Or copy the APK to the
phone and open it, allowing "Install unknown apps" when asked. If setting Clean SMS as the default SMS app is
blocked as a restricted setting, go to Settings → Apps → Clean SMS → ⋮ → Allow restricted settings.
Switching back to Google Messages later is safe: messages live in the system SMS store, not only in Clean SMS.
adb logcat -c
adb shell am start -n dev.codingcoffee.cleansms/.MainActivity
adb logcat -d -b crashR8 obfuscates class names. Map them back with app/build/outputs/mapping/release/mapping.txt from the same
build. run-as doesn't work on release builds.