import java.util.Properties // The only build file in the project. AGP 9 ships Kotlin built in, so there is // no `org.jetbrains.kotlin.android` line; and at one module a root build file and // a version catalog would both be pure ceremony. Versions are pinned exactly — // no `+`, no version ranges — because reproducible release builds are a goal from // day one. // // The Compose compiler plugin is *not* implied by AGP's built-in Kotlin, despite // what the "built-in Kotlin" framing suggests: enabling `buildFeatures.compose` // without it fails configuration outright. Its version must equal the KGP version // AGP bundles — 2.2.10 for AGP 9.2.1. Verify after any AGP bump with: // ./gradlew :app:buildEnvironment | grep kotlin-gradle-plugin plugins { id("com.android.application") version "9.2.1" id("org.jetbrains.kotlin.plugin.compose") version "2.2.10" } android { namespace = "net.lexcom.opentracker" compileSdk = 36 defaultConfig { applicationId = "net.lexcom.opentracker" // 29 is the first API with the 3-arg startForeground() overload, which // is what lets us run a location foreground service without any // compat library. minSdk = 29 targetSdk = 36 versionCode = 1 versionName = "0.1.0" } buildFeatures { compose = true // Off by default since AGP 8; we read FLAG_DEBUGGABLE instead of // generating a class for one boolean. buildConfig = false } compileOptions { sourceCompatibility = JavaVersion.VERSION_21 targetCompatibility = JavaVersion.VERSION_21 } buildTypes { debug { // So a debug build can sit next to a release build on the same // device. Referenced by the adb commands in the README. applicationIdSuffix = ".debug" } release { // No reflection anywhere in this app, and Compose/AndroidX ship // consumer ProGuard rules, so minification can be turned on later // without ever creating a proguard-rules.pro. Costs a few MB of APK. isMinifyEnabled = false signingConfig = signingConfigs.findByName("release") } } signingConfigs { // Release signing is configured only when the (gitignored) properties // file exists, so a fresh clone can still build debug. val props = rootProject.file("keystore.properties") if (props.exists()) { create("release") { val p = Properties().apply { props.inputStream().use(::load) } storeFile = rootProject.file(p.getProperty("storeFile")) storePassword = p.getProperty("storePassword") keyAlias = p.getProperty("keyAlias") keyPassword = p.getProperty("keyPassword") // v1 signatures are only needed below API 24. Schemes v2/v3 are // enough at minSdk 29 and keep the APK's zip entries untouched. enableV1Signing = false enableV2Signing = true enableV3Signing = true } } } lint { disable += setOf( // There is no res/values/strings.xml on purpose: this app is not // localized, and UI strings live as Kotlin constants next to the // code that uses them. "HardcodedText", "MissingDefaultResource", // "a newer version is available" checks. Every version here is // pinned deliberately, for reproducible builds; being told daily // that a newer one exists is noise, and with warningsAsErrors it // would break the build the moment Google publishes anything. // Upgrades are a decision, not a lint finding. "AndroidGradlePluginVersion", "GradleDependency", "NewerVersionAvailable", "OldTargetApi", // Suggests replacing android:allowBackup="false" with a // dataExtractionRules XML resource. There is nothing to configure: // this app's data directory holds the device's token key, and // backup/transfer of it is exactly what must not happen. Following // the advice would add a res/ file that says "back up nothing". "DataExtractionRules", ) // A lint failure should stop the build rather than scroll past. warningsAsErrors = true abortOnError = true } packaging { resources.excludes += setOf( "META-INF/{AL2.0,LGPL2.1}", "DebugProbesKt.bin", ) } testOptions { unitTests.isReturnDefaultValues = true } sourceSets["test"].resources.srcDir("../../crates/otproto/tests") } dependencies { implementation(platform("androidx.compose:compose-bom:2026.06.01")) implementation("androidx.compose.material3:material3") implementation("androidx.activity:activity-compose:1.13.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.11.0") // The map. FOSS, no Play Services, raster tiles, points straight at our own // /tiles proxy. Hosted in Compose via AndroidView. implementation("org.osmdroid:osmdroid-android:6.1.20") // Everything non-trivial in this app (sampling policy, frame codec, queue, // AEAD) is pure Kotlin and tested on the JVM. No androidTest/, no emulator // in the loop. // Spelled out rather than `kotlin("test")`: AGP's built-in Kotlin does not // apply the Kotlin Gradle plugin's version-inferring `kotlin()` helper, so // that form resolves to a versionless coordinate and silently contributes // nothing. The `-junit` variant brings the JUnit 4 runner AGP's unit tests // expect. Version must track the KGP that AGP bundles. testImplementation("org.jetbrains.kotlin:kotlin-test-junit:2.2.10") // Test-only. The app itself parses its one JSON response (the login reply) // with the framework's org.json, but unit tests run against android.jar // stubs where those methods return defaults, so the golden-vector test needs // a real parser. Never reaches the APK. testImplementation("org.json:json:20260719") }