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