build.gradle.kts
⎇
Raw
1import java.util.Properties
2
3// The only build file in the project. AGP 9 ships Kotlin built in, so there is
4// no `org.jetbrains.kotlin.android` line; and at one module a root build file and
5// a version catalog would both be pure ceremony. Versions are pinned exactly —
6// no `+`, no version ranges — because reproducible release builds are a goal from
7// day one.
8//
9// The Compose compiler plugin is *not* implied by AGP's built-in Kotlin, despite
10// what the "built-in Kotlin" framing suggests: enabling `buildFeatures.compose`
11// without it fails configuration outright. Its version must equal the KGP version
12// AGP bundles — 2.2.10 for AGP 9.2.1. Verify after any AGP bump with:
13// ./gradlew :app:buildEnvironment | grep kotlin-gradle-plugin
14plugins {
15 id("com.android.application") version "9.2.1"
16 id("org.jetbrains.kotlin.plugin.compose") version "2.2.10"
17}
18
19android {
20 namespace = "net.lexcom.opentracker"
21 compileSdk = 36
22
23 defaultConfig {
24 applicationId = "net.lexcom.opentracker"
25 // 29 is the first API with the 3-arg startForeground() overload, which
26 // is what lets us run a location foreground service without any
27 // compat library.
28 minSdk = 29
29 targetSdk = 36
30 versionCode = 1
31 versionName = "0.1.0"
32 }
33
34 buildFeatures {
35 compose = true
36 // Off by default since AGP 8; we read FLAG_DEBUGGABLE instead of
37 // generating a class for one boolean.
38 buildConfig = false
39 }
40
41 compileOptions {
42 sourceCompatibility = JavaVersion.VERSION_21
43 targetCompatibility = JavaVersion.VERSION_21
44 }
45
46 buildTypes {
47 debug {
48 // So a debug build can sit next to a release build on the same
49 // device. Referenced by the adb commands in the README.
50 applicationIdSuffix = ".debug"
51 }
52 release {
53 // No reflection anywhere in this app, and Compose/AndroidX ship
54 // consumer ProGuard rules, so minification can be turned on later
55 // without ever creating a proguard-rules.pro. Costs a few MB of APK.
56 isMinifyEnabled = false
57 signingConfig = signingConfigs.findByName("release")
58 }
59 }
60
61 signingConfigs {
62 // Release signing is configured only when the (gitignored) properties
63 // file exists, so a fresh clone can still build debug.
64 val props = rootProject.file("keystore.properties")
65 if (props.exists()) {
66 create("release") {
67 val p = Properties().apply { props.inputStream().use(::load) }
68 storeFile = rootProject.file(p.getProperty("storeFile"))
69 storePassword = p.getProperty("storePassword")
70 keyAlias = p.getProperty("keyAlias")
71 keyPassword = p.getProperty("keyPassword")
72 // v1 signatures are only needed below API 24. Schemes v2/v3 are
73 // enough at minSdk 29 and keep the APK's zip entries untouched.
74 enableV1Signing = false
75 enableV2Signing = true
76 enableV3Signing = true
77 }
78 }
79 }
80
81 lint {
82 disable += setOf(
83 // There is no res/values/strings.xml on purpose: this app is not
84 // localized, and UI strings live as Kotlin constants next to the
85 // code that uses them.
86 "HardcodedText",
87 "MissingDefaultResource",
88
89 // "a newer version is available" checks. Every version here is
90 // pinned deliberately, for reproducible builds; being told daily
91 // that a newer one exists is noise, and with warningsAsErrors it
92 // would break the build the moment Google publishes anything.
93 // Upgrades are a decision, not a lint finding.
94 "AndroidGradlePluginVersion",
95 "GradleDependency",
96 "NewerVersionAvailable",
97 "OldTargetApi",
98
99 // Suggests replacing android:allowBackup="false" with a
100 // dataExtractionRules XML resource. There is nothing to configure:
101 // this app's data directory holds the device's token key, and
102 // backup/transfer of it is exactly what must not happen. Following
103 // the advice would add a res/ file that says "back up nothing".
104 "DataExtractionRules",
105 )
106 // A lint failure should stop the build rather than scroll past.
107 warningsAsErrors = true
108 abortOnError = true
109 }
110
111 packaging {
112 resources.excludes += setOf(
113 "META-INF/{AL2.0,LGPL2.1}",
114 "DebugProbesKt.bin",
115 )
116 }
117
118 testOptions {
119 unitTests.isReturnDefaultValues = true
120 }
121
122 sourceSets["test"].resources.srcDir("../../crates/otproto/tests")
123}
124
125dependencies {
126 implementation(platform("androidx.compose:compose-bom:2026.06.01"))
127 implementation("androidx.compose.material3:material3")
128 implementation("androidx.activity:activity-compose:1.13.0")
129 implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.11.0")
130 // The map. FOSS, no Play Services, raster tiles, points straight at our own
131 // /tiles proxy. Hosted in Compose via AndroidView.
132 implementation("org.osmdroid:osmdroid-android:6.1.20")
133
134 // Everything non-trivial in this app (sampling policy, frame codec, queue,
135 // AEAD) is pure Kotlin and tested on the JVM. No androidTest/, no emulator
136 // in the loop.
137 // Spelled out rather than `kotlin("test")`: AGP's built-in Kotlin does not
138 // apply the Kotlin Gradle plugin's version-inferring `kotlin()` helper, so
139 // that form resolves to a versionless coordinate and silently contributes
140 // nothing. The `-junit` variant brings the JUnit 4 runner AGP's unit tests
141 // expect. Version must track the KGP that AGP bundles.
142 testImplementation("org.jetbrains.kotlin:kotlin-test-junit:2.2.10")
143 // Test-only. The app itself parses its one JSON response (the login reply)
144 // with the framework's org.json, but unit tests run against android.jar
145 // stubs where those methods return defaults, so the golden-vector test needs
146 // a real parser. Never reaches the APK.
147 testImplementation("org.json:json:20260719")
148}
149