Files
flamenet-messenger-android/CONTRIBUTING.md
T
Paul HittandClaude Opus 4.8 bd2cdf0394 Add CONTRIBUTING, and point the vectors at the repository that owns them
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>
2026-08-29 09:45:04 -04:00

67 lines
2.9 KiB
Markdown

# 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](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](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:
```bash
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 `SIGABRT`s the process. Calls have crashed this
app twice that way.
- **Adding a permission?** Declare it *and* verify on device. A missing
`ACCESS_NETWORK_STATE` presented 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.