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>
153 lines
6.9 KiB
Kotlin
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")
|
|
}
|