app/build.gradle.kts (7350 bytes)
1 import java.security.MessageDigest 2 import java.time.LocalDate 3 import java.time.ZoneOffset 4 import java.util.Properties 5 6 plugins { 7 id("com.android.application") 8 id("org.jetbrains.kotlin.android") 9 } 10 11 /** 12 * Release signing comes from keystore.properties, which is deliberately not in 13 * the repository. Without it the release build still runs and produces an 14 * unsigned APK, so a fresh clone is never broken - it just cannot ship. 15 */ 16 val keystoreProps = Properties().apply { 17 val f = rootProject.file("keystore.properties") 18 if (f.exists()) f.inputStream().use { load(it) } 19 } 20 val hasSigning = keystoreProps.getProperty("storeFile") != null 21 22 fun prop(name: String): String = 23 (project.findProperty(name) as String?) ?: error("missing gradle property $name") 24 25 /** Midnight UTC at the start of the given ISO date, as epoch millis. */ 26 fun epochMillis(isoDate: String): Long = 27 LocalDate.parse(isoDate) 28 .atStartOfDay(ZoneOffset.UTC).toInstant().toEpochMilli() 29 30 fun sha256Hex(text: String): String = 31 MessageDigest.getInstance("SHA-256") 32 .digest(text.toByteArray(Charsets.UTF_8)) 33 .joinToString("") { b -> "%02x".format(b) } 34 35 // Only the digest of the tester code is compiled in. With no code configured 36 // the digest is of the empty string, which no entered code can match. 37 val testerCodeHash = sha256Hex( 38 (keystoreProps.getProperty("testerCode") ?: "").trim().uppercase() 39 ) 40 41 android { 42 namespace = "dev.equwal.assistkey" 43 compileSdk = 36 44 45 // No list of dependencies, encrypted for Google alone, in the signed APK. 46 // F-Droid does not accept it, and it would stop the reproducible build check. 47 dependenciesInfo { 48 includeInApk = false 49 includeInBundle = false 50 } 51 52 defaultConfig { 53 applicationId = "dev.equwal.assistkey" 54 // QuickAccessWalletService, which the wallet channel needs, is API 31. 55 minSdk = 31 56 targetSdk = 36 57 versionCode = prop("assistkey.versionCode").toInt() 58 versionName = prop("assistkey.versionName") 59 60 buildConfigField( 61 "long", "BETA_EXPIRES_MS", 62 epochMillis(prop("assistkey.betaExpires")).toString() + "L" 63 ) 64 buildConfigField("String", "BETA_EXPIRES_DATE", "\"" + prop("assistkey.betaExpires") + "\"") 65 buildConfigField("String", "TESTER_CODE_SHA256", "\"" + testerCodeHash + "\"") 66 } 67 68 signingConfigs { 69 if (hasSigning) { 70 create("release") { 71 storeFile = rootProject.file(keystoreProps.getProperty("storeFile")) 72 storePassword = keystoreProps.getProperty("storePassword") 73 keyAlias = keystoreProps.getProperty("keyAlias") 74 keyPassword = keystoreProps.getProperty("keyPassword") 75 } 76 } 77 } 78 79 // Two builds of one app, same id and same signature. 80 // play: for Google Play. No Shizuku code, no Shizuku permission. 81 // full: for direct install. Adds shell access through Shizuku, which is 82 // what reads the Power button and switches the navigation bar. 83 // fdroid: for F-Droid. Like `full`, but free and complete: no Google Play 84 // Billing, no licence check, no apps of other makers inside. The 85 // version name has no suffix, because F-Droid compares it. 86 flavorDimensions += "store" 87 productFlavors { 88 create("play") { 89 dimension = "store" 90 buildConfigField("boolean", "LICENCE_CHECK", "true") 91 } 92 create("full") { 93 dimension = "store" 94 versionNameSuffix = "-full" 95 buildConfigField("boolean", "LICENCE_CHECK", "true") 96 } 97 create("fdroid") { 98 dimension = "store" 99 buildConfigField("boolean", "LICENCE_CHECK", "false") 100 } 101 } 102 103 // Code that two flavours share lives in its own folder. 104 sourceSets { 105 getByName("play").java.srcDir("src/billing/java") 106 getByName("full").java.srcDir("src/billing/java") 107 getByName("fdroid").java.srcDir("src/full/java/dev/equwal/assistkey/shell") 108 } 109 110 buildTypes { 111 release { 112 // R8 removes unused code. F-Droid asks for it. proguard-rules.pro 113 // turns off renaming, because the code names its own activities and 114 // services in strings, and keeps Shizuku.newProcess for Shell.kt. 115 isMinifyEnabled = true 116 proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 117 if (hasSigning) signingConfig = signingConfigs.getByName("release") 118 } 119 debug { 120 applicationIdSuffix = ".debug" 121 versionNameSuffix = "-debug" 122 } 123 } 124 125 buildFeatures { 126 buildConfig = true 127 } 128 129 compileOptions { 130 sourceCompatibility = JavaVersion.VERSION_17 131 targetCompatibility = JavaVersion.VERSION_17 132 } 133 134 kotlinOptions { 135 jvmTarget = "17" 136 } 137 138 lint { 139 // The whole app is built out of things a normal app has no business 140 // doing; the interesting warnings are drowned out by the expected ones. 141 abortOnError = false 142 } 143 } 144 145 // One dependency, and only because there is no other way to sell on Play. 146 // Everything else is framework API. 147 // 148 // The billing library talks to the Play Store app over IPC and needs no network 149 // access of its own. What does want the network is its usage telemetry, which 150 // rides on Google's datatransport runtime and would merge INTERNET and 151 // ACCESS_NETWORK_STATE into the manifest. An accessibility service that sees 152 // key presses should not also hold INTERNET, so that runtime is left out. 153 // 154 // This is safe by construction, not by luck: the only class in the library that 155 // touches datatransport (zzdn in 8.3.0) initialises it inside a catch-all and 156 // falls back to "Skipping logging since initialization failed". Re-check that 157 // with javap before bumping the billing version, and re-check the merged 158 // permissions with `aapt2 dump badging` after. 159 dependencies { 160 // The `fdroid` flavour has no billing library: it has no proprietary dependency. 161 val billing = "com.android.billingclient:billing:8.3.0" 162 "playImplementation"(billing) { exclude(group = "com.google.android.datatransport") } 163 "fullImplementation"(billing) { exclude(group = "com.google.android.datatransport") } 164 165 // Shell access, `full` and `fdroid` flavours. Shizuku (MIT) lets the app run a small service of its own 166 // under the shell uid, which is what reads the Power key and switches the 167 // settings Android keeps from ordinary apps. The user starts Shizuku through 168 // wireless debugging; nothing here needs root or a computer. 169 "fullImplementation"("dev.rikka.shizuku:api:13.1.5") 170 "fullImplementation"("dev.rikka.shizuku:provider:13.1.5") 171 "fdroidImplementation"("dev.rikka.shizuku:api:13.1.5") 172 "fdroidImplementation"("dev.rikka.shizuku:provider:13.1.5") 173 174 // Tests only, never in the APK. JUnit 4 is what the Android Gradle plugin 175 // runs with no more setup; there was no test framework before it. 176 testImplementation("junit:junit:4.13.2") 177 // org.json is part of Android, where unit tests get an empty stub of it. 178 // This is the same library as a plain jar, so SettingsFile can be tested. 179 testImplementation("org.json:json:20240303") 180 }