matrix-android/AGENTS.md

8 KiB

AGENTS.md — matrix-android

Progress log and architecture notes for AI agents working on this project.


Status

Component Status
Build infrastructure (Gradle, version catalog) done
NixOS CLI dev shell / Android SDK toolchain done
Matrix networking (Retrofit API + Session) done
SSO / password login flow done
Room list screen done
Chat screen (send + receive) done
E2EE via local Olm wrapper done
Device verification (SAS) done
Terminal theme (green on black, monospace) done
Launcher icon (vector adaptive) done
README done

Design decisions

No heavy Matrix SDK

Instead of importing the full matrix-android-sdk2 (used by Element), we call the Matrix REST API directly with Retrofit. This keeps the APK small and the code easy to follow. The trade-off is no E2E encryption support.

Local Olm build instead of Maven-native olm-sdk

The upstream org.matrix.android:olm-sdk Maven artifact shipped a libolm.so that was only 4 KB ELF-aligned on arm64, which triggered Android's 16 KB page-size compatibility warning on modern devices. The app now vendors:

  • app/libs/olm-sdk-3.2.16.jar
  • app/src/main/jniLibs/*/libolm.so

Those JNI binaries were rebuilt locally from the Matrix olm Android wrapper with linker flags that produce 16 KB-aligned LOAD segments on arm64.

Nix-first CLI builds

The repo includes a flake.nix dev shell for NixOS. It provisions:

  • JDK 17
  • Gradle
  • Android SDK platforms 31 and 35
  • Build Tools 30.0.2, 34.0.0, and 35.0.0
  • Android NDK

The shell rewrites local.properties on entry so CLI builds use the Nix-provided SDK path.

SSO via Custom Tabs

Login opens the homeserver's /login/sso/redirect endpoint in a Chrome Custom Tab. After auth the homeserver redirects to matrixandroid://sso?loginToken=<token>. The app intercepts this URI via an <intent-filter> in AndroidManifest.xml and exchanges the token for an access token.

Single-activity Compose navigation

MainActivityAppNavigation hosts all three screens (Login, Rooms, Chat) inside a single NavHost. No fragments.

MatrixSession singleton

MatrixSession is a process-scoped singleton (companion object). It holds the Retrofit client, persists the access token in SharedPreferences, and runs the /sync long-polling loop in a coroutine scope. All screens observe syncState: StateFlow<SyncState>.

Timeline is event-cache driven

MatrixSession now keeps a per-room in-memory RoomEvent cache and rebuilds the visible Message list from those events. This allows:

  • local filtering/search over loaded events
  • message edits (m.replace)
  • reactions (m.annotation)
  • incremental history pagination
  • retrying decryption after keys arrive from another device

Sync filter

Initial and incremental syncs use a compact filter:

  • Timeline limited to 50 events per room
  • State events use lazy member loading
  • Presence and account data stripped

This prevents the first sync from being huge on accounts with many rooms.


Known issues / future work

  • Olm is still legacy crypto: The app uses a locally rebuilt Olm wrapper. Matrix has deprecated libolm in favor of vodozemac. A future migration should replace the current JNI/JAR setup.
  • SAS verification edge case: A handshake-ordering fix is currently in progress. The known symptom was that self-verification could fail depending on which device confirmed matching emojis first.
  • Push notifications: Not implemented. The app only receives messages while open.
  • History is partial: Pagination exists only while the room exposes a prev_batch token and only for message history. There is still no durable local database.
  • Image/file messages: Silently dropped (only m.text is rendered).
  • Rich message support is partial: Reactions and edits are handled for loaded events, but there is no reaction picker, no redactions, and no full relation aggregation from the homeserver.
  • Search is in-memory only: Chat search only scans events already loaded into memory for the current room.
  • Error handling: Basic inline send/load errors are shown, but there is still no global offline state, retry queue, or structured error UX.

File map

matrix-android/
├── settings.gradle.kts
├── build.gradle.kts
├── gradle/
│   ├── libs.versions.toml          ← version catalog
│   └── wrapper/
│       └── gradle-wrapper.properties
├── app/
│   ├── build.gradle.kts
│   ├── proguard-rules.pro
│   └── src/main/
│       ├── AndroidManifest.xml
│       ├── java/com/ltadeu6/matrix/
│       │   ├── MainActivity.kt
│       │   ├── navigation/AppNavigation.kt
│       │   ├── auth/LoginScreen.kt
│       │   ├── auth/LoginViewModel.kt
│       │   ├── verification/VerificationScreen.kt
│       │   ├── rooms/RoomListScreen.kt
│       │   ├── chat/ChatScreen.kt
│       │   ├── chat/ChatViewModel.kt
│       │   ├── matrix/MatrixApi.kt
│       │   ├── matrix/MatrixModels.kt
│       │   ├── matrix/MatrixSession.kt
│       │   ├── matrix/CryptoService.kt
│       │   ├── matrix/VerificationService.kt
│       │   └── ui/theme/Theme.kt
│       └── res/
│           ├── drawable/ic_launcher_foreground.xml
│           ├── mipmap-anydpi-v26/ic_launcher.xml
│           ├── mipmap-anydpi-v26/ic_launcher_round.xml
│           └── values/{strings,colors,themes}.xml
├── flake.nix
├── flake.lock
├── gradlew
├── gradlew.bat
├── app/libs/olm-sdk-3.2.16.jar
├── app/src/main/jniLibs/
├── README.md
└── AGENTS.md

Session 1 — initial build (2026-05-14)

  • Inspected matrix-message terminal app for UI reference:
    • app.py: curses chat with < prefix for incoming, > for outgoing
    • font.py: big pixel ASCII font for incoming banners
    • Color scheme: COLOR_GREEN on black
  • Created full Android project from scratch (no fork)
  • Chose Retrofit + kotlinx.serialization over matrix-android-sdk2 for simplicity
  • SSO flow: Custom Tabs → homeserver → matrixandroid://sso deep link → token exchange
  • Compose UI with monospace font, #00FF41 green, #0A0A0A background
  • All screens text-only, no images/icons/avatars

Session 2 — NixOS CLI + 16 KB Android compatibility (2026-05-16)

  • Added flake.nix + flake.lock for NixOS CLI builds
  • Generated and committed Gradle wrapper files
  • Discovered Android 16 KB compatibility warning on Pixel 7a
  • Confirmed APK ZIP alignment was already correct; root cause was Maven libolm.so being ELF-aligned to 4 KB
  • Cloned upstream Matrix olm Android wrapper, patched NDK build flags for 16 KB page size, and rebuilt libolm.so
  • Worked around old upstream Android wrapper assumptions:
    • required platform 31 + build-tools 30.0.2
    • required NDK path/layout adjustments
    • required one small C++ compatibility fix in olm::List for modern clang/NDK
  • Replaced implementation(libs.olm.sdk) with:
    • local app/libs/olm-sdk-3.2.16.jar
    • local app/src/main/jniLibs/*/libolm.so
  • Validated final APK on-device:
    • libolm.so inside the APK has LOAD Align = 0x4000
    • APK installed and launched over ADB on the Pixel 7a

Session 3 — verification and chat UX expansion (2026-05-16, in progress)

  • Created checkpoint commit 0ede945 before feature work
  • Began fixing SAS verification handshake ordering
  • Added event-cache-backed message reconstruction in MatrixSession
  • Added local support for:
    • in-room search over loaded messages
    • message edits via m.replace
    • reactions via m.annotation
    • older-message pagination via /rooms/{roomId}/messages
    • room-level encryption/history indicators
    • inline chat error feedback
  • Added first-pass DM name resolution fallback via joined members lookup
  • This session is still dirty in the worktree; verify behavior before treating these changes as final