app/build.gradle.kts (6945 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 defaultConfig { 46 applicationId = "dev.equwal.assistkey" 47 // QuickAccessWalletService, which the wallet channel needs, is API 31. 48 minSdk = 31 49 targetSdk = 36 50 versionCode = prop("assistkey.versionCode").toInt() 51 versionName = prop("assistkey.versionName") 52 53 buildConfigField( 54 "long", "BETA_EXPIRES_MS", 55 epochMillis(prop("assistkey.betaExpires")).toString() + "L" 56 ) 57 buildConfigField("String", "BETA_EXPIRES_DATE", "\"" + prop("assistkey.betaExpires") + "\"") 58 buildConfigField("String", "TESTER_CODE_SHA256", "\"" + testerCodeHash + "\"") 59 } 60 61 signingConfigs { 62 if (hasSigning) { 63 create("release") { 64 storeFile = rootProject.file(keystoreProps.getProperty("storeFile")) 65 storePassword = keystoreProps.getProperty("storePassword") 66 keyAlias = keystoreProps.getProperty("keyAlias") 67 keyPassword = keystoreProps.getProperty("keyPassword") 68 } 69 } 70 } 71 72 // Two builds of one app, same id and same signature. 73 // play: for Google Play. No Shizuku code, no Shizuku permission. 74 // full: for direct install. Adds shell access through Shizuku, which is 75 // what reads the Power button and switches the navigation bar. 76 // fdroid: for F-Droid. Like `full`, but free and complete: no Google Play 77 // Billing, no licence check, no apps of other makers inside. The 78 // version name has no suffix, because F-Droid compares it. 79 flavorDimensions += "store" 80 productFlavors { 81 create("play") { 82 dimension = "store" 83 buildConfigField("boolean", "LICENCE_CHECK", "true") 84 } 85 create("full") { 86 dimension = "store" 87 versionNameSuffix = "-full" 88 buildConfigField("boolean", "LICENCE_CHECK", "true") 89 } 90 create("fdroid") { 91 dimension = "store" 92 buildConfigField("boolean", "LICENCE_CHECK", "false") 93 } 94 } 95 96 // Code that two flavours share lives in its own folder. 97 sourceSets { 98 getByName("play").java.srcDir("src/billing/java") 99 getByName("full").java.srcDir("src/billing/java") 100 getByName("fdroid").java.srcDir("src/full/java/dev/equwal/assistkey/shell") 101 } 102 103 buildTypes { 104 release { 105 // No shrinking: the app is tiny, and R8 would only complicate 106 // keeping the accessibility service and the three impersonation 107 // entry points reachable. 108 isMinifyEnabled = false 109 if (hasSigning) signingConfig = signingConfigs.getByName("release") 110 } 111 debug { 112 applicationIdSuffix = ".debug" 113 versionNameSuffix = "-debug" 114 } 115 } 116 117 buildFeatures { 118 buildConfig = true 119 } 120 121 compileOptions { 122 sourceCompatibility = JavaVersion.VERSION_17 123 targetCompatibility = JavaVersion.VERSION_17 124 } 125 126 kotlinOptions { 127 jvmTarget = "17" 128 } 129 130 lint { 131 // The whole app is built out of things a normal app has no business 132 // doing; the interesting warnings are drowned out by the expected ones. 133 abortOnError = false 134 } 135 } 136 137 // One dependency, and only because there is no other way to sell on Play. 138 // Everything else is framework API. 139 // 140 // The billing library talks to the Play Store app over IPC and needs no network 141 // access of its own. What does want the network is its usage telemetry, which 142 // rides on Google's datatransport runtime and would merge INTERNET and 143 // ACCESS_NETWORK_STATE into the manifest. An accessibility service that sees 144 // key presses should not also hold INTERNET, so that runtime is left out. 145 // 146 // This is safe by construction, not by luck: the only class in the library that 147 // touches datatransport (zzdn in 8.3.0) initialises it inside a catch-all and 148 // falls back to "Skipping logging since initialization failed". Re-check that 149 // with javap before bumping the billing version, and re-check the merged 150 // permissions with `aapt2 dump badging` after. 151 dependencies { 152 // The `fdroid` flavour has no billing library: it has no proprietary dependency. 153 val billing = "com.android.billingclient:billing:8.3.0" 154 "playImplementation"(billing) { exclude(group = "com.google.android.datatransport") } 155 "fullImplementation"(billing) { exclude(group = "com.google.android.datatransport") } 156 157 // Shell access, `full` and `fdroid` flavours. Shizuku (MIT) lets the app run a small service of its own 158 // under the shell uid, which is what reads the Power key and switches the 159 // settings Android keeps from ordinary apps. The user starts Shizuku through 160 // wireless debugging; nothing here needs root or a computer. 161 "fullImplementation"("dev.rikka.shizuku:api:13.1.5") 162 "fullImplementation"("dev.rikka.shizuku:provider:13.1.5") 163 "fdroidImplementation"("dev.rikka.shizuku:api:13.1.5") 164 "fdroidImplementation"("dev.rikka.shizuku:provider:13.1.5") 165 166 // Tests only, never in the APK. JUnit 4 is what the Android Gradle plugin 167 // runs with no more setup; there was no test framework before it. 168 testImplementation("junit:junit:4.13.2") 169 // org.json is part of Android, where unit tests get an empty stub of it. 170 // This is the same library as a plain jar, so SettingsFile can be tested. 171 testImplementation("org.json:json:20240303") 172 }