Files
flamenet-messenger-android/app/build.gradle.kts
T
Paul HittandClaude Opus 5 053cefbce4 Two apps, three defects that only driving them could find
The relay is asked what it is, and the answer went nowhere. SiteConfig is a
data class whose only constructor property was `baseUrl`; `discovered` was a
`var` in the body, so the generated equals() ignored it. MutableStateFlow
conflates on equality, which made

    _site.value = config.also { it.discovered = found }

a no-op against the value already there. The descriptor was fetched correctly
and discarded. The sign-in screen sat on "Checking what this server uses…"
forever — the one line whose whole job is to say, BEFORE the passphrase is
typed, whether that passphrase leaves the device — and accountProvider stayed
LOCAL for a relay that had just answered SHARED. Now a constructor property,
written with copy(), with the first JVM unit tests the :app module has ever
had. Confirmed live: the screen reads "One Flamenet account, shared with
Flamenet Files."

Two XCUITest queries matched more than one element and so could tap neither:

- "👍" is in the emoji picker's Gestures row, in its Recent row once picked,
  and in ChatView's quick-reaction bar. Picker buttons now carry a
  section-scoped identifier.
- "Sign out" is both the row in ProfileView and the destructive button in the
  confirmation it opens; SwiftUI keeps both in the tree, so the query was
  ambiguous before the dialog was even reached. Both now identified.

Neither was load — both fail the same way on an idle machine.

Also adds completeSignIn to UITestSupport, the counterpart to completeSignUp.
Sign-ups are rate limited at three an hour per address, so a test that only
needs AN identity should reuse one rather than mint another and fail with a
429 that reads like a broken sign-up screen. CrossPlatformUITests takes
FN_TEST_PASSWORD and signs in when given one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 20:42:21 -04:00

153 lines
6.9 KiB
Kotlin

import java.io.File
import java.util.Properties
// The Android app shell. Everything below it — the cryptography and the wire
// layer — lives in :engine and :client, which are plain JVM libraries so they
// can be tested without any of this.
plugins {
id("com.android.application")
kotlin("android")
id("org.jetbrains.kotlin.plugin.compose")
}
android {
namespace = "io.flamenet.messenger"
compileSdk = 35
defaultConfig {
applicationId = "io.flamenet.messenger"
// Android exposes the XDH algorithm from API 33. Below that a provider
// such as BouncyCastle has to be registered at startup; the engine is
// provider-agnostic, but nothing here does that registration yet, so
// claiming a lower floor would be claiming something untrue.
minSdk = 33
targetSdk = 35
versionCode = 1
versionName = "0.1"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
sourceSets {
getByName("main") { java.srcDir("src/main/kotlin") }
getByName("androidTest") { java.srcDir("src/androidTest/kotlin") }
}
buildFeatures { compose = true }
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlin { compilerOptions { jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17) } }
// Read from a properties file outside the repository, so the keystore and
// its password are never in git and a checkout without them still builds
// (unsigned, which is exactly what an unsigned build should be).
// 🔴 **This app's OWN key, not the one every Flamenet Android app shares.**
// `.flamenet-android-signing.properties` was made for Flamenet Files, and
// because the path is app-agnostic this app silently inherited it — every
// release before 2026-08-25 went out as `CN=Flamenet Files`. Nobody chose
// that for a messenger; it fell out of the layout.
//
// It matters because **an Android signing key is permanent**: once a device
// installs a build signed with one key, it REFUSES every later build signed
// with another. Sharing one key across products also means one compromised
// keystore takes out both.
//
// So: prefer this app's own file, fall back to the shared one so a machine
// that only has the old setup still produces a signed build rather than
// failing confusingly.
//
// ⚠️ Both files live outside the repo and hold a password in the clear. They
// are never in git, and a checkout without them still builds — unsigned,
// which is exactly what an unsigned build should be.
val signingProperties = Properties()
val home = File(System.getProperty("user.home"))
val signingFile = listOf(
File(home, ".flamenet-messenger-signing.properties"),
File(home, ".flamenet-android-signing.properties"),
).firstOrNull { it.exists() }
signingFile?.inputStream()?.use { signingProperties.load(it) }
signingConfigs {
if (signingProperties.isNotEmpty()) {
create("release") {
storeFile = File(signingProperties.getProperty("storeFile"))
storePassword = signingProperties.getProperty("storePassword")
keyAlias = signingProperties.getProperty("keyAlias")
keyPassword = signingProperties.getProperty("keyPassword")
}
}
}
buildTypes {
release {
// ⚠️ **WebRTC ships native libraries for four ABIs and they
// dominate the APK** — about 41 MB uncompressed, of which x86 and
// x86_64 are ~24 MB and exist only for emulators. Real phones are
// arm64-v8a, with armeabi-v7a for old ones.
//
// Restricted here rather than split into per-ABI APKs on purpose:
// this app is handed to testers as one file, and per-ABI outputs
// turn "send them the APK" into "send them the right APK".
//
// Debug keeps all four, because that is what runs on an emulator.
ndk { abiFilters += setOf("arm64-v8a", "armeabi-v7a") }
isMinifyEnabled = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
signingConfig = signingConfigs.findByName("release")
}
}
packaging {
resources.excludes += setOf("META-INF/*.kotlin_module", "META-INF/DEPENDENCIES")
}
}
dependencies {
implementation(project(":engine"))
implementation(project(":client"))
implementation(platform("androidx.compose:compose-bom:2024.12.01"))
implementation("androidx.compose.material3:material3")
implementation("androidx.compose.material:material-icons-extended")
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-tooling-preview")
debugImplementation("androidx.compose.ui:ui-tooling")
implementation("androidx.activity:activity-compose:1.9.3")
implementation("androidx.core:core-ktx:1.15.0")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7")
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.7")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0")
// Voice calls (§8.10).
//
// The one third-party dependency in this app, and it is here because the
// alternative is implementing ICE, DTLS-SRTP and an audio pipeline, which
// is not a thing to hand-roll. `webrtc-sdk` is the maintained build of
// Google's own libwebrtc — the upstream project stopped publishing usable
// Android artifacts, and every Android client of consequence uses either
// this or its own build of the same source.
//
// ⚠️ It is a large native blob: roughly 15-20 MB of the APK, and not
// something that can be audited line by line. Worth stating rather than
// absorbing quietly, because everything else in this app deliberately is
// auditable.
implementation("io.github.webrtc-sdk:android:125.6422.07")
// On-device tests. The JVM suites cannot see a provider difference, and a
// provider difference is what broke X25519 the first time this engine's
// sibling ran on a phone.
// Plain JVM unit tests for the app module. There were none until a
// StateFlow write-back bug in SiteConfig shipped unnoticed; logic that
// lives in :app and needs no device belongs here, not in androidTest.
testImplementation(kotlin("test"))
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.2.1")
androidTestImplementation("androidx.test:runner:1.6.2")
androidTestImplementation(platform("androidx.compose:compose-bom:2024.12.01"))
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
debugImplementation("androidx.compose.ui:ui-test-manifest")
androidTestImplementation("androidx.tracing:tracing:1.2.0")
}