Files
Gearslip/gearslip/build.gradle.kts
Seb3thehacker e72e99d699 Place lists get a map behind them, drawn by Gearslip as Android Auto draws one
RF Sentinel and other point-of-interest apps leave this map to the host. It uses MapLibre with
OpenFreeMap's tiles, which need no key or account, with a numbered pin for each place.
2026-11-06 15:58:13 -08:00

212 lines
9.6 KiB
Kotlin

import java.util.Properties
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile
/**
* Gearslip: a phone-side Android Auto client built against the reverse-engineered AA wire
* protocol, so a non-Google app can authenticate to and project onto a real car head unit
* with no gearhead in the path. No third-party dependencies: the X.509 certificate is
* generated by hand (see `SelfSignedCert`) so there is no BouncyCastle version to track and
* no second crypto provider to collide with Conscrypt.
*/
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.compose)
}
// The release signing key, read from an untracked keystore.properties beside this file or at the
// repository root. Third-party apps allowlist Gearslip by the SHA-256 of this certificate, so the
// key must stay the same for the life of the project - and must never be committed.
val keystoreProperties = Properties().apply {
listOf(
rootProject.file("keystore.properties"),
project.file("keystore.properties"),
).firstOrNull { it.exists() }?.inputStream()?.use { load(it) }
}
// The version comes from the newest v* git tag, so a build can never disagree with its tag. A
// build of the tagged commit is named after it ("0.4.0-beta"); anything later adds the commits
// since and the commit ("0.4.0-beta+3.ba4374e"), so a test build can't pass for a release.
// The version code is worked out from the numbers, ((major*100 + minor)*100 + patch)*10 + stage,
// where stage is 1 for alpha, 2 for beta and 9 for a finished release. It only ever goes up as
// long as minor and patch stay under 100. Changelog entries use the same number.
class GitVersion(val name: String, val code: Int)
val gitVersion: GitVersion = run {
val described = runCatching {
providers.exec {
commandLine("git", "describe", "--tags", "--match", "v*", "--abbrev=7")
isIgnoreExitValue = true
}.standardOutput.asText.get().trim()
}.getOrNull().orEmpty()
val match = Regex("""^v((\d+)\.(\d+)\.(\d+)(?:-([A-Za-z]+))?)(?:-(\d+)-g([0-9a-f]+))?$""").find(described)
?: return@run GitVersion("0.0.0-dev", 1) // no git or no tags, e.g. a source download
val (base, major, minor, patch, suffix, since, commit) = match.destructured
val stage = when (suffix.lowercase()) {
"" -> 9
"beta" -> 2
"alpha" -> 1
else -> 0
}
GitVersion(
name = if (since.isEmpty()) base else "$base+$since.$commit",
code = ((major.toInt() * 100 + minor.toInt()) * 100 + patch.toInt()) * 10 + stage,
)
}
// Keeps build output out of the module folder: <root>/build/gearslip/... instead of
// <root>/gearslip/build/..., so everything this project produces lands under one
// root-level build/ directory. Set before android {}, which works out the path to R8's default
// rules from it.
layout.buildDirectory.set(rootProject.layout.buildDirectory.dir("gearslip"))
android {
namespace = "app.seb3thehacker.gearslip"
compileSdk = 37
defaultConfig {
applicationId = "app.seb3thehacker.gearslip"
minSdk = 31
targetSdk = 37
versionCode = gitVersion.code
// -PgearslipVersionName=0.1.99-test makes a build that thinks it's older, to try the updater.
versionName = project.findProperty("gearslipVersionName")?.toString() ?: gitVersion.name
manifestPlaceholders["appLabel"] = "Gearslip"
// Where opted-in phones send their daily usage note: the stats-worker/ Worker's address,
// set as gearslipStatsUrl in gradle.properties. Left empty, nothing is ever sent.
buildConfigField("String", "STATS_URL", "\"${project.findProperty("gearslipStatsUrl") ?: ""}\"")
}
signingConfigs {
// Declared only when the properties file is present, so a clone without the key still
// builds debug; assembleRelease then falls back to unsigned rather than failing here.
if (keystoreProperties.getProperty("storeFile") != null) {
create("release") {
storeFile = rootProject.file(keystoreProperties.getProperty("storeFile"))
storePassword = keystoreProperties.getProperty("storePassword")
keyAlias = keystoreProperties.getProperty("keyAlias")
keyPassword = keystoreProperties.getProperty("keyPassword")
}
}
}
buildTypes {
release {
signingConfig = signingConfigs.findByName("release")
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
// Its own package, so a debug build installs beside the release one instead of over it.
// Signed with the release key when it's here: both builds declare the car template
// permission, and Android refuses a second app declaring it under a different key.
debug {
applicationIdSuffix = ".dev"
signingConfigs.findByName("release")?.let { signingConfig = it }
manifestPlaceholders["appLabel"] = "Gearslip Debug"
// English (XA) and Arabic (XB) under the phone's languages: stretched, accented and
// right-to-left text, which shows what's still hardcoded and what doesn't fit.
isPseudoLocalesEnabled = true
// Shrunk with R8 like a release, so a debug install behaves like what ships (a keep
// rule missing for release shows up here first). -PnoR8 skips it for a quicker build.
if (!project.hasProperty("noR8")) {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
}
buildFeatures {
buildConfig = true
}
// Lists each language that has a translation in the app's locale config, so Android 13+
// offers it under the system's per-app languages. English is the unqualified default
// (res/resources.properties).
//
// Only languages that are mostly translated ship, so the app never comes up half in English.
// Danish and Estonian are still early on Weblate; add them here once they're close to done.
androidResources {
generateLocaleConfig = true
localeFilters += listOf(
"en", "ar", "cs", "de", "es", "fr", "it", "ja", "ko", "nl", "pl", "pt-rBR", "ro", "ru",
"sv", "tr", "uk", "zh-rCN",
)
}
lint {
// Weblate fills languages in a string at a time; the rest fall back to English.
disable += "MissingTranslation"
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
defaultConfig {
// Speech-to-text (sherpa-onnx 1.13.8, static-link-onnxruntime build, in jniLibs) is the
// only native code. One APK for all three, since the updater takes the release's only APK;
// 32-bit x86 is left out, nothing that drives a car runs it.
ndk { abiFilters += listOf("arm64-v8a", "armeabi-v7a", "x86_64") }
}
// Compressed, the three copies cost a 33 MB download instead of 75 MB; the phone unpacks
// only its own at install.
packaging { jniLibs { useLegacyPackaging = true } }
testOptions {
// GearslipLog calls android.util.Log, which is a stub on the local JVM.
unitTests.isReturnDefaultValues = true
}
}
// Copies the produced APK to the root build/ folder (<root>/build/Gearslip-debug.apk) after the
// fact rather than fighting AGP's variant-output API across versions - simpler and doesn't break
// when that API moves again. A copy, not a move: AGP packages incrementally into the APK it left
// last time, and with that file gone a build came out holding nothing but classes.dex.
tasks.register("renameApk") {
doLast {
val rootBuildDir = rootProject.layout.buildDirectory.get().asFile
layout.buildDirectory.dir("outputs/apk").get().asFile.walkTopDown()
.filter { it.extension == "apk" }
.toList()
.forEach { apk ->
val renamed = File(rootBuildDir, "Gearslip-${apk.parentFile.name}.apk")
apk.copyTo(renamed, overwrite = true)
}
}
}
tasks.matching { it.name.startsWith("assemble") }.configureEach { finalizedBy("renameApk") }
dependencies {
// Only for the XML window theme (Theme.Material3.DynamicColors); every screen is Compose.
implementation(libs.google.material)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.material.icons.core)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.lifecycle.runtime.compose)
// Used from the host side, not the app side: Gearslip renders other apps' templates rather
// than publishing its own, so this is here for the AIDL interfaces and the model classes
// their Bundleables deserialize into.
implementation(libs.androidx.car.app)
implementation(libs.androidx.car.app.projected)
implementation(libs.androidx.media)
// Document-start scripts, so the ad blocker hides ad slots before a page first paints.
implementation(libs.androidx.webkit)
// The map behind a place list (PlaceListMapTemplate), which the host draws, not the app.
implementation(libs.maplibre.android)
testImplementation(libs.junit)
}
tasks.withType<KotlinJvmCompile>().configureEach {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_21)
}
}