Files
Warp/apps/mobile/README.md
T
tajniak81andClaude Opus 5.5 41f800ab0b Mobile: voice and video calls (MatrixRTC with LiveKit)
Calls the way the web client and Element Call do them, with the same
events, so the three can share a call. The famedly SDK's own group calls
use com.famedly.call.member events, which the others don't read, so this
is written against the matrix-js-sdk wire format instead (lib/matrix/
calls.dart):

- Membership: org.matrix.msc3401.call.member state, key
  _@user:server_DEVICE_m.call, with the legacy session fields the js-sdk
  sends (application m.call, call_id "", membershipID, expires,
  focus_active, foci_preferred). An empty delayed copy (MSC4140) is
  scheduled first and pushed back every 5 s; leaving sends the empty
  state and cancels it.
- Media: a LiveKit token from the JWT service (the call's, else the
  homeserver's rtc/transports or .well-known rtc_foci) for an OpenID
  token; livekit_client connects.
- Encrypted rooms: a random 16-byte key per device, sent to every other
  device in the call over encrypted to-device
  io.element.call.encryption_keys, keys from others applied per LiveKit
  identity (@user:server:DEVICE); per-participant keys, no ratcheting,
  HKDF, like the web client's livekit-client.
- Ringing: a call started in a room of two sends a ring notification
  (MSC4075); the other side gets an incoming-call card and an insistent
  ringtone notification (full screen on a locked phone), declines with
  MSC4310 or answers by joining. The caller gives up after a minute or on
  a decline.
- UI: voice and video buttons in the room header, a call screen (the
  other person large and you in the corner, or a grid; mute, camera, flip,
  leave), a Join banner and a room-list marker for calls going on.

Not tested end to end yet: it needs a LiveKit SFU and JWT service.

README: replies, reactions, threads, spaces, QR verification, sliding
sync and calls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 08:43:19 +02:00

219 lines
11 KiB
Markdown

# Warp for Android
A Matrix client for Android, written in Flutter on the famedly
[Matrix Dart SDK](https://github.com/famedly/matrix-dart-sdk) with
[vodozemac](https://github.com/matrix-org/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](../web/README.md): 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 (`/me` and 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*.
- Replies (the message answered is quoted above the reply) and reactions
(six quick ones and any other; chips with counts, tap to add or take
back, long press for who reacted), from a message's long press.
- Threads: *Reply in thread* opens the thread, its root and replies, with
a composer that sends into it. Replies stay out of the main timeline;
the root shows "N replies". Room info lists a room's threads.
- Spaces: the room list's title picks a space, which narrows the list to
its rooms (and its subspaces'). A space's browser (`/hierarchy`) shows
the rooms you're in and the ones you can join; admins add existing or
new rooms. *New space* in the new-chat sheet.
- Voice and video calls, MatrixRTC with a LiveKit SFU like the web client
(see below).
- 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 from room info, the session list, or an
incoming request: by QR code (show this device's, or scan the other's
with the camera) when both sides can, else by comparing emoji.
- 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](https://unifiedpush.org) (below).
- Settings: display name, avatar, theme (system, light, dark).
**Not yet:** a per-message trust shield like the web client's, screen
sharing in calls, calls that keep going with the app in the background
(Android stops the process; a foreground service would keep it), and
read receipts per thread.
## Sync
When the homeserver advertises `org.matrix.simplified_msc3575`, the app
uses Simplified Sliding Sync (MSC4186), as Element X does; otherwise
`/sync`. The famedly SDK only knows `/sync`, so `WarpClient`
(`lib/matrix/sliding_sync.dart`) answers the SDK's sync call with a
sliding sync request and rewrites the response into the `/sync` shape:
encryption, the database and timelines don't know the difference.
- The first request asks for the 20 most recent rooms with a handful of
state events each, so the room list appears at once; the next ones
widen the window to every room (up to 1000).
- Two lists: rooms, and direct chats with all their members (so a DM's
name is the other person's). Members are otherwise `$LAZY`.
- Extensions: to-device, E2EE, account data, receipts, typing.
- The SDK's token becomes `ss1|<pos>|<to-device since>`. `M_UNKNOWN_POS`
(the server forgot the connection, after a restart or 30 idle minutes)
starts a new one.
## Calls
Every call, between two people or more, is a MatrixRTC call (MSC4143)
with its media through a LiveKit SFU: the model of the web client and
Element Call, with the same events, so the three can share a call. See
[docs/calls.md](../../docs/calls.md) for the server side
(`client_api.rtc`, LiveKit and the JWT service).
- Voice and video buttons in the room header. A room of two rings the
other person (`org.matrix.msc4075.rtc.notification`): an incoming-call
card with Answer, Voice only and Decline
(`org.matrix.msc4310.rtc.decline`), and an insistent ringtone
notification, full screen on a locked phone.
- Who is in a call is `org.matrix.msc3401.call.member` state, one event
per device. Before sending ours the app schedules an empty one with the
homeserver (a delayed event, MSC4140) and pushes it back every five
seconds, so a phone that dies drops out of the call by itself.
- The LiveKit token comes from the JWT service named by the call already
going on, else by the homeserver (`rtc/transports`, or
`org.matrix.msc4143.rtc_foci` in `.well-known`), for an OpenID token.
- In an encrypted room the media is end-to-end encrypted: each device
makes its own key and sends it to the others over encrypted to-device
messages (`io.element.call.encryption_keys`); keys are derived with
HKDF and never ratcheted, like the web client's livekit-client.
- A call of two shows the other person large and you in the corner; more
is a grid. Mute, camera, flip camera, leave. A "Join" banner in a room
with a call you're not in; the room list marks rooms with one.
## Push notifications
Warp doesn't use Firebase. Push goes through UnifiedPush: the user installs
a *distributor* app, such as [ntfy](https://ntfy.sh), 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](../../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.
```bash
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`:
```bash
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.properties` turns off Kotlin's incremental compilation:
with the project and the pub cache on different drives, plugins fail to
compile with "different roots".
- `flutter test` builds the native code of every dependency for the host,
including `webcrypto` (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`), sliding sync, calls, 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, the sliding sync conversion, 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.