Recently Written · git

assistkey

Hardware key remapper for the Viwoods AiPaper Reader: AI key, volume keys and power, with multi-tap, hold and chords

git clone https://github.com/equwal/assistkey

Log | Files | Refs


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 }