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 (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 }