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) } } // Keeps build output out of the module folder: /build/gearslip/... instead of // /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 = 11 // -PgearslipVersionName=0.1.99-test makes a build that thinks it's older, to try the updater. versionName = project.findProperty("gearslipVersionName")?.toString() ?: "0.3.02-alpha" 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" // 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 } compileOptions { sourceCompatibility = JavaVersion.VERSION_21 targetCompatibility = JavaVersion.VERSION_21 } defaultConfig { // Speech-to-text (sherpa-onnx, in jniLibs) ships for 64-bit ARM only: every phone // Gearslip runs on, and one copy of the native library instead of four. ndk { abiFilters += "arm64-v8a" } } 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 (/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) testImplementation(libs.junit) } tasks.withType().configureEach { compilerOptions { jvmTarget.set(JvmTarget.JVM_21) } }