When both sides support it, a verification offers QR codes first: this device shows its code (in a white quiet zone, whatever the theme) and can scan the other's with the camera (mobile_scanner; only codes that start with MATRIX count). The side whose code was scanned confirms the other saw it succeed. Compare emoji stays available, and is used directly when either side has no QR support. Adds the CAMERA permission (camera hardware not required). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Warp for Android
A Matrix client for Android, written in Flutter on the famedly Matrix Dart SDK with vodozemac for end-to-end encryption. It speaks the plain Client-Server API, so it works against any Matrix homeserver, not only Warp. The same code is meant to ship to iOS later (PLAN.md §8.5); only Android is built and tested today.
It looks like the web client: the same tokens (colours, radii), the Outfit and Figtree fonts, the Warp symbol.
What it does
- Sign in with a password, on a server name (
example.org, found through/.well-known/matrix/client), a homeserver address, or a full user ID alone. Sign up when the server allows it, with a registration token when it asks for one. - The session survives restarts: sync state, rooms and keys are in SQLite in
the app's private storage, and the access token and cached secrets also
in the Android Keystore (
flutter_secure_storage). They are excluded from cloud backups and device transfers, since a restored copy would be a second session with this one's keys. - Room list: unread and mention counts, invites first with Join and Decline, a search, and filters for unread, people and rooms.
- Timeline: grouped messages, day separators, room events as one-line notices, older history on scroll, links that open in the browser, images (full screen with zoom), files saved where you choose. Media is fetched with the access token (authenticated media, Matrix 1.11).
- Sending: text (
/meand the SDK's other commands), photos and videos from the gallery or the camera, any file. A message shows at once as Sending, then Sent, then Read once someone else has read it; a failed one says Not sent, with Retry and Delete on a long press. - Typing notifications, read receipts, deleting messages, editing shown as edited.
- New rooms (end-to-end encrypted by default, public or not), direct messages (encrypted), joining by address. Room info: members with their role and verification shield, invite, rename, topic, notification level, turning encryption on, leaving.
- End-to-end encryption, on by default:
- Set up encryption creates cross-signing keys, secret storage and a key backup in one step, behind a recovery key shown once.
- A new session verifies itself with the recovery key (which also restores older messages from the backup) or from another session.
- Verifying people and sessions by comparing emoji, from room info, the session list, or an incoming request.
- A message this session can't decrypt says why, not just "encrypted".
- Reset encryption for a lost recovery key.
- Sessions: every session of the account, which are verified, verify one, sign one out.
- Notifications while the app runs, one per room, opening the room when tapped, and push notifications through UnifiedPush (below).
- Settings: display name, avatar, theme (system, light, dark).
Not yet: replies, reactions, threads, spaces, voice and video calls,
QR-code verification, Simplified Sliding Sync (the SDK has no client for
it, so the app uses /sync), a per-message trust shield like the web
client's.
Push notifications
Warp doesn't use Firebase. Push goes through UnifiedPush: the user installs a distributor app, such as ntfy, and turns on Push notifications in Settings. The distributor gives the app an endpoint, which becomes a Matrix pusher on the homeserver. When something notifies, the homeserver posts to the distributor's Matrix gateway, the distributor wakes the app, and the app fetches the event, decrypts it if it can, and shows it.
- Pushes are
event_id_only: the gateway and the distributor never see message content, in encrypted rooms or not. - The gateway is found with UnifiedPush's discovery
(
GET <endpoint origin>/_matrix/push/v1/notify; ntfy has one). A distributor without one goes through UnifiedPush's public gateway,matrix.gateway.unifiedpush.org. - The homeserver needs
push.enabled(the default); see docs/push.md. It accepts any gateway URL with the path/_matrix/push/v1/notify. - Without a distributor the app is still notified, but only while Android keeps it running.
Build and run
You need:
-
Flutter 3.44 or later (Dart 3.12).
-
The Android SDK with an NDK and CMake (Android Studio installs them).
-
Rust, with the Android targets: vodozemac is compiled from source during the build.
rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android i686-linux-android
Then:
| Command | What |
|---|---|
make mobile |
release APKs, one per ABI, to bin/warp-android-<abi>.apk |
make mobile-run |
run on a connected phone or emulator (ARGS=--release for a release build) |
make mobile-test |
flutter analyze and the unit tests |
The first build compiles the Rust crate and takes a few minutes; later builds reuse it.
Release builds are signed with the key in android/key.properties
(storeFile, storePassword, keyAlias, keyPassword; not committed).
Without that file they are signed with the debug key, which is fine for
trying a release build and not for distributing one.
Against a local homeserver
Debug builds may use plain HTTP, release builds only HTTPS. From the
emulator, the host machine is 10.0.2.2:
WARP_SERVER_NAME=dev.local WARP_PRIVATE_KEY=./keys/dev.key ./bin/warp generate-keys
WARP_SERVER_NAME=dev.local WARP_PRIVATE_KEY=./keys/dev.key WARP_REGISTRATION_ENABLED=true ./bin/warp serve
and sign up on the server http://10.0.2.2:8008.
Windows notes
android/gradle.propertiesturns off Kotlin's incremental compilation: with the project and the pub cache on different drives, plugins fail to compile with "different roots".flutter testbuilds the native code of every dependency for the host, includingwebcrypto(pulled in by UnifiedPush's Linux support). Its build needs CMake 3.13 or later; if Visual Studio's developer environment puts an older CMake first (Visual Studio 2017 ships 3.12), the build fails with "does not appear to contain CMakeLists.txt". Android builds use the Android SDK's CMake and are not affected.
Layout
| Path | What |
|---|---|
lib/main.dart |
start-up: notifications, the client, push; a push alone starts the app without UI (--unifiedpush-bg) |
lib/app.dart |
the app shell: signed in or not, prompts the SDK raises (password, verification requests), opening rooms from notifications |
lib/matrix/ |
everything around the SDK: the client and homeserver discovery (client.dart), the timeline projection, media, encryption setup, notifications, push |
lib/screens/ |
sign-in, sign-up, room list, room, room info, settings, encryption, sessions |
lib/widgets/ |
message bubbles, avatars, new chat, verification, password prompts |
lib/theme.dart |
the design tokens as a Flutter theme |
test/ |
the timeline projection, formatting |
assets/ |
the brand fonts (as TTF: Flutter can't load woff2) and the symbol |
tool/icons/ |
launcher icon sources; dart run flutter_launcher_icons regenerates the icons |
The SDK's objects change in place, as matrix-js-sdk's do in the web client.
Screens watch a counter bumped on every sync (syncProvider in
lib/state.dart) and read the client again.
Licences
The Matrix Dart SDK is AGPL-3.0. An APK built from this app includes it, so distributing the APK has to follow the AGPL. The fonts are under the SIL Open Font License.