mirror of
https://github.com/screentinker/screentinker.git
synced 2026-06-14 18:22:46 -06:00
fix(android): re-sign release APK with v1 (JAR) signature for MDM signage (#81)
minSdk 26 makes AGP default the v1 (JAR) signature off, so the release APK is v2-only. Some MDM-managed commercial signage (MAXHUB via the Pivot MDM) silently removes a v2-only app on the next reboot because its boot integrity check expects a v1 signature — screens that power-cycle nightly lose the app and fall back to the setup screen. `enableV1Signing = true` has no effect at minSdk >= 24 (verified: still v2-only). Instead, finalize assembleRelease with a `resignReleaseV1` task that re-signs via apksigner with --v1-signing-enabled true and a low --min-sdk-version, emitting v1 alongside v2/v3. Verified: v1+v2+v3 at min-sdk 19, verifies at API 36, and the re-signed APK installs and runs on a live API 36 emulator. Closes #81 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3ddc209d19
commit
22376710ee
|
|
@ -33,6 +33,15 @@
|
|||
unaffected. A device with no timezone set and not reporting one falls back to the
|
||||
server clock (unchanged from before).
|
||||
|
||||
### Fixed
|
||||
- **#81 — release APK is now v1 + v2 + v3 signed.** With `minSdk 26`, the Android Gradle
|
||||
Plugin defaulted the v1 (JAR) signature *off*, producing a v2-only APK that some
|
||||
MDM-managed commercial signage (e.g. MAXHUB via the Pivot MDM) silently removes on the
|
||||
next reboot — so screens that power-cycle nightly lost the app and fell back to the
|
||||
setup screen. Setting `enableV1Signing = true` had no effect at minSdk ≥ 24; the release
|
||||
build now re-signs with `apksigner` and a low `--min-sdk-version` to emit the JAR
|
||||
signature alongside v2/v3. Verified to install and run on Android 14+/API 36 as well.
|
||||
|
||||
### Notes
|
||||
- **Scheduling fails open.** If the on-device evaluator ever errors (bad timezone id,
|
||||
malformed block), the item **plays** rather than being hidden. A blank screen is worse
|
||||
|
|
|
|||
|
|
@ -403,6 +403,13 @@ The APK will be at `android/app/build/outputs/apk/debug/app-debug.apk`. Copy it
|
|||
cp android/app/build/outputs/apk/debug/app-debug.apk ScreenTinker.apk
|
||||
```
|
||||
|
||||
> **Release builds & MDM signage (#81):** `./gradlew assembleRelease` is automatically
|
||||
> re-signed to carry a **v1 (JAR) signature alongside v2/v3** (the `resignReleaseV1` task in
|
||||
> `app/build.gradle.kts`). At `minSdk 26` the Gradle plugin omits v1, and some MDM-managed
|
||||
> commercial displays (e.g. MAXHUB/Pivot) **strip a v2-only APK on reboot** — screens that
|
||||
> power-cycle nightly then lose the app. v1+v2+v3 installs everywhere from API 19 to the
|
||||
> latest Android. (`enableV1Signing = true` alone does not work at minSdk ≥ 24.)
|
||||
|
||||
To generate a new signing keystore:
|
||||
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -21,6 +21,9 @@ android {
|
|||
storePassword = System.getenv("KEYSTORE_PASSWORD") ?: findProperty("KEYSTORE_PASSWORD") as String? ?: ""
|
||||
keyAlias = System.getenv("KEY_ALIAS") ?: findProperty("KEY_ALIAS") as String? ?: "remotedisplay"
|
||||
keyPassword = System.getenv("KEY_PASSWORD") ?: findProperty("KEY_PASSWORD") as String? ?: ""
|
||||
// #81: AGP ignores enableV1Signing at minSdk>=24, so assembleRelease emits a
|
||||
// v2-only APK. The v1 (JAR) signature that some MDM-managed signage (MAXHUB)
|
||||
// requires is added by the `resignReleaseV1` task below (apksigner re-sign).
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -87,3 +90,38 @@ dependencies {
|
|||
tasks.withType<Test> {
|
||||
systemProperty("scheduleVectors", File(rootProject.projectDir.parentFile, "shared/schedule-vectors.json").absolutePath)
|
||||
}
|
||||
|
||||
// #81: AGP ignores enableV1Signing at minSdk>=24, so `assembleRelease` produces a
|
||||
// v2-only APK - and some MDM-managed signage (MAXHUB/Pivot) silently removes a v2-only
|
||||
// app on the next reboot because its boot integrity check expects a v1 (JAR) signature.
|
||||
// Re-sign the assembled release APK with apksigner, forcing a low --min-sdk-version so
|
||||
// the v1 signature is emitted alongside v2/v3. v1+v2+v3 verifies on every Android
|
||||
// version (legacy MDM hardware via v1, modern Android via v2/v3).
|
||||
tasks.register<Exec>("resignReleaseV1") {
|
||||
val apk = layout.buildDirectory.file("outputs/apk/release/app-release.apk").get().asFile
|
||||
onlyIf { apk.exists() }
|
||||
doFirst {
|
||||
val sdkDir = System.getenv("ANDROID_HOME")
|
||||
?: System.getenv("ANDROID_SDK_ROOT")
|
||||
?: rootProject.file("local.properties").takeIf { it.exists() }
|
||||
?.readLines()?.firstOrNull { it.startsWith("sdk.dir=") }?.substringAfter("=")?.trim()
|
||||
?: throw GradleException("#81 resign: set ANDROID_HOME or sdk.dir in local.properties")
|
||||
val buildTools = File(sdkDir, "build-tools").listFiles()
|
||||
?.filter { it.isDirectory }?.maxByOrNull { it.name }
|
||||
?: throw GradleException("#81 resign: no build-tools found under $sdkDir")
|
||||
commandLine(
|
||||
File(buildTools, "apksigner").absolutePath, "sign",
|
||||
"--ks", file("../release-key.jks").absolutePath,
|
||||
"--ks-key-alias", (System.getenv("KEY_ALIAS") ?: "remotedisplay"),
|
||||
"--ks-pass", "pass:" + (System.getenv("KEYSTORE_PASSWORD") ?: ""),
|
||||
"--key-pass", "pass:" + (System.getenv("KEY_PASSWORD") ?: ""),
|
||||
"--v1-signing-enabled", "true",
|
||||
"--v2-signing-enabled", "true",
|
||||
"--v3-signing-enabled", "true",
|
||||
"--min-sdk-version", "19",
|
||||
apk.absolutePath
|
||||
)
|
||||
}
|
||||
}
|
||||
// AGP registers assembleRelease lazily, so match it when/after it's created.
|
||||
tasks.matching { it.name == "assembleRelease" }.configureEach { finalizedBy("resignReleaseV1") }
|
||||
|
|
|
|||
Loading…
Reference in a new issue