Both sides now declare what they will talk to, and enforce it. Two axes kept
deliberately separate, because conflating them is the trap:
protocol_version — CAN these builds talk. The correctness axis. Below 1.0.0
the minor is the breaking axis, per SemVer §4.
release window — MAY they, per policy. [min, max), advertised in the
profile, overridable by the operator.
The server refuses out-of-window apps with 426 and a body naming both versions
and the accepted range; the app checks the profile in both directions before a
run rather than discovering mid-measurement that it will be refused.
Three rules that shape the rest:
- GET /v1/profile is never gated. It is where a refused client learns which
version it needs; gating it leaves the user with a network error instead of
an answer, which is precisely the confusion this exists to remove.
- An unparseable or absent version is "unknown", and is allowed. Development
builds report "dev", and a client too old to send the header cannot be
identified anyway.
- Bounds sit at breaking boundaries, not at releases, so shipping a patch
never requires editing a range. The app's server minimum is 0.4.2 for a
stated reason: earlier multi-homed servers mis-addressed granted sends and
the client measured 100% downstream loss that never happened.
The app's versionCode is now derived from its SemVer instead of being a second
number someone has to remember to bump.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
84 lines
3.4 KiB
Kotlin
84 lines
3.4 KiB
Kotlin
// SPDX-FileCopyrightText: 2026 Echolot contributors
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
plugins {
|
|
// AGP 9 built-in Kotlin — no kotlin.android here (see core-probe note).
|
|
alias(libs.plugins.android.application)
|
|
alias(libs.plugins.kotlin.compose)
|
|
alias(libs.plugins.kotlin.serialization)
|
|
}
|
|
|
|
// The app's version is SemVer and lives here, once. versionCode is derived from it rather than
|
|
// maintained alongside: Play/F-Droid need a monotonically increasing integer, but a second number
|
|
// that a human has to remember to bump is a number that eventually disagrees with the first — and
|
|
// the version is now load-bearing, since the server decides whether to serve us by it.
|
|
//
|
|
// major*1_000_000 + minor*10_000 + patch*10 leaves room for 9 patch-level rebuilds (the trailing
|
|
// digit) without disturbing the mapping, and stays inside the 2_100_000_000 ceiling until major 2100.
|
|
val appVersionName = "0.2.0"
|
|
|
|
fun versionCodeOf(semver: String): Int {
|
|
val (major, minor, patch) = semver.substringBefore('-').split(".").map(String::toInt)
|
|
return major * 1_000_000 + minor * 10_000 + patch * 10
|
|
}
|
|
|
|
android {
|
|
namespace = "app.echo_lot.app"
|
|
compileSdk = 36
|
|
|
|
defaultConfig {
|
|
applicationId = "app.echo_lot.app"
|
|
minSdk = 26
|
|
targetSdk = 36
|
|
versionCode = versionCodeOf(appVersionName)
|
|
versionName = appVersionName
|
|
// Automation: `adb shell am start -n app.echo_lot.app/.MainActivity --ez autorun true`
|
|
// runs a measurement immediately and POSTs the report here (dev collection endpoint).
|
|
buildConfigField("String", "REPORT_UPLOAD_URL", "\"http://89.185.109.150:443/report\"")
|
|
buildConfigField("String", "REPORT_UPLOAD_SECRET", "\"D4OmG5gGJsElqVVbtYIZbR\"")
|
|
// The bare SemVer, without the debug build's "-dev" suffix stripped away by the server's
|
|
// parser anyway — sent to servers so they can apply their compatibility window.
|
|
buildConfigField("String", "APP_SEMVER", "\"$appVersionName\"")
|
|
}
|
|
buildTypes {
|
|
release { isMinifyEnabled = false }
|
|
debug {
|
|
// The dev build is a separate app: own package (installs alongside a real
|
|
// Echolot), own label and a DEV-badged icon (src/debug/res).
|
|
applicationIdSuffix = ".dev"
|
|
versionNameSuffix = "-dev"
|
|
}
|
|
}
|
|
compileOptions {
|
|
sourceCompatibility = JavaVersion.VERSION_17
|
|
targetCompatibility = JavaVersion.VERSION_17
|
|
}
|
|
buildFeatures {
|
|
compose = true
|
|
buildConfig = true
|
|
}
|
|
}
|
|
|
|
dependencies {
|
|
implementation(project(":core-measurement"))
|
|
implementation(project(":core-protocol"))
|
|
implementation(project(":core-engine"))
|
|
implementation(project(":core-probe"))
|
|
implementation(project(":core-shizuku"))
|
|
implementation(project(":core-privacy"))
|
|
implementation(project(":core-archive"))
|
|
|
|
implementation(libs.kotlinx.serialization.json)
|
|
implementation(libs.kotlinx.coroutines.android)
|
|
implementation(libs.androidx.core.ktx)
|
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
|
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
|
implementation(libs.androidx.activity.compose)
|
|
implementation(platform(libs.androidx.compose.bom))
|
|
implementation(libs.androidx.ui)
|
|
implementation(libs.androidx.ui.graphics)
|
|
implementation(libs.androidx.ui.tooling.preview)
|
|
implementation(libs.androidx.material3)
|
|
debugImplementation(libs.androidx.ui.tooling)
|
|
}
|