The README said the engine is held to `engine/test/vectors/protocol-v1.json`, a path from the pre-split subtree layout that does not exist here. The vectors live in flamenet-e2e and are read from there rather than copied, because a copied vector file drifts and a drifting vector file stops being a check. PublishedVectorsTest already searches for it and fails loudly when it cannot find it; the README now says so, and names FLAMENET_VECTORS. CONTRIBUTING records what a JVM test cannot tell you: Conscrypt and the JVM's JCA are different providers, so :engine:test passing proves nothing about a handset, which is what interop/ is for. Also the exception escaping a WebRTC callback that SIGABRTs rather than surfacing, the missing permission that presented as a dead button, and `*/` inside a KDoc reporting an unclosed comment hundreds of lines from its cause. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2.9 KiB
Contributing
The Android client for Flamenet Messenger, in Kotlin and Compose. Three modules:
engine (the encryption primitives), client (protocol and transport), app
(UI). Feature parity with iOS is tracked in PARITY.md — read it
before assuming something is missing by accident.
Security bugs do not go in an issue
Email security@flamenetmessenger.com. See SECURITY.md.
Building
JDK 21 and the Android SDK. On a machine with no default Java — which is the usual state of a Mac — Gradle reports "Unable to locate a Java Runtime" rather than anything about JDKs, so set it explicitly:
export JAVA_HOME=/opt/homebrew/opt/openjdk@21
export ANDROID_HOME=$HOME/Library/Android/sdk
./gradlew :engine:test :client:test :app:testDebugUnitTest
Those three suites are the merge gate.
🔴 JVM crypto tests prove nothing about Android. The JVM's JCA and Android's
Conscrypt are different providers with different opinions — X25519/X448 need DER
key specs and API 33 on device, and code that passes :engine:test on the JVM
can throw on a handset. Anything touching key agreement or signatures needs an
instrumented test, which is why interop/ exists.
Instrumented tests are not in the gate: they need an emulator and a local relay.
What a change needs
- Touching the wire? iOS and the relay move with you. A field this client required and the relay never sent broke groups completely while every unit test stayed green. Diff the wire types first.
- Touching state that arrives incrementally — avatars, rosters, device lists — merge, never replace. Absent from a fetch means "not loaded yet", not "deleted".
- Touching WebRTC callbacks? Never let an exception escape one. It does not
surface as a caught error, it
SIGABRTs the process. Calls have crashed this app twice that way. - Adding a permission? Declare it and verify on device. A missing
ACCESS_NETWORK_STATEpresented as a dead button, not as a permission error. - Writing KDoc that mentions a path like
/e2e/*? Kotlin does not nest block comments, so*/inside one closes it and the compiler reports "Unclosed comment" at the end of the file, hundreds of lines from the cause.
Releases
The signed release APK is built with a keystore that is not in this repository and never will be — it is public. Signing keys are permanent for the life of an installed app: a key change means every existing user must uninstall and lose their local history, so the certificate identity is not something to fix later.
House rules
- Fail closed. Unverifiable signatures, unknown versions and unpinned identities are refusals, and the user is told which.
- Never log message contents, keys, or tokens.
- No secrets, keystores, or passphrases in the repository.
- Match the surrounding style. Comments explain why a line exists — usually, which attack or which platform quirk it accounts for.