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

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_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.