ci(android): never ship a debug-signed APK; check the signer (#5116)
release / govulncheck (push) Successful in 26s
release / go (push) Successful in 2m5s
release / web (push) Successful in 1m24s
release / integration (push) Successful in 5m19s
release / android (push) Successful in 5m57s
release / Build signed APK (releases and dev) (push) Successful in 5m54s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 1m26s
release / Verify release artifacts (tag releases only) (push) Skipped
release / govulncheck (push) Successful in 26s
release / go (push) Successful in 2m5s
release / web (push) Successful in 1m24s
release / integration (push) Successful in 5m19s
release / android (push) Successful in 5m57s
release / Build signed APK (releases and dev) (push) Successful in 5m54s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 1m26s
release / Verify release artifacts (tag releases only) (push) Skipped
Adopts the rest of family idea #5103 (distributing your own APK): - Practice 2: build.gradle.kts no longer falls back to the debug key when ANDROID_KEYSTORE_PATH is unset; the release build is signed with the release key or left unsigned. Main no longer builds and uploads a debug-signed app-debug.apk, which no install could ever update. - Practice 3: android-release runs apksigner on the built APK, prints the signer's DN and SHA-256 digest, and fails on a debug signer. An unsigned build fails the same step, since there is no app-release.apk to verify. - Practice 9: debug builds offer no server update. The banner does not poll and the About card says updates come from Android Studio, since the release-signed APK cannot install over a debug-signed app. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -72,9 +72,13 @@ android {
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro",
|
||||
)
|
||||
// Signed with the release key or not at all. Falling back to the
|
||||
// debug key made a missing secret into a published APK that no
|
||||
// install could ever update (family idea #5103, practice 2). An
|
||||
// unsigned build installs nowhere, so the gap shows at once.
|
||||
signingConfig =
|
||||
if (System.getenv("ANDROID_KEYSTORE_PATH").isNullOrEmpty()) {
|
||||
signingConfigs.getByName("debug")
|
||||
null
|
||||
} else {
|
||||
signingConfigs.getByName("release")
|
||||
}
|
||||
|
||||
+5
-1
@@ -41,6 +41,10 @@ data class AboutUiState(
|
||||
// check must decide on. Held in state rather than read inline so a test
|
||||
// can drive the comparison without a BuildConfig.
|
||||
val installedCode: Long = BuildConfig.VERSION_CODE.toLong(),
|
||||
// A debug build is signed with this machine's debug key, so the server's
|
||||
// release-signed APK can never install over it (family idea #5103,
|
||||
// practice 9). It updates from Android Studio instead.
|
||||
val selfUpdates: Boolean = !BuildConfig.DEBUG,
|
||||
val isChecking: Boolean = false,
|
||||
val installStage: InstallStage = InstallStage.IDLE,
|
||||
val installMessage: String? = null,
|
||||
@@ -67,7 +71,7 @@ class AboutCardViewModel @Inject constructor(
|
||||
val state: StateFlow<AboutUiState> = internal.asStateFlow()
|
||||
|
||||
fun checkForUpdates() {
|
||||
if (internal.value.isChecking) return
|
||||
if (internal.value.isChecking || !internal.value.selfUpdates) return
|
||||
viewModelScope.launch {
|
||||
internal.update { it.copy(isChecking = true, installMessage = null) }
|
||||
val installed = internal.value.installedVersion
|
||||
|
||||
@@ -383,6 +383,14 @@ private fun AboutCard(viewModel: AboutCardViewModel = hiltViewModel()) {
|
||||
|
||||
@Composable
|
||||
private fun UpdateControls(state: AboutUiState, viewModel: AboutCardViewModel) {
|
||||
if (!state.selfUpdates) {
|
||||
Text(
|
||||
text = "Debug build: updates install from Android Studio.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
return
|
||||
}
|
||||
UpdateCheckLine(result = state.result)
|
||||
Button(
|
||||
onClick = viewModel::checkForUpdates,
|
||||
|
||||
+7
@@ -29,6 +29,9 @@ private const val POLL_INTERVAL_MS = 24 * 60 * 60 * 1000L
|
||||
* restart re-shows it, which is acceptable nudging for v1 (matches
|
||||
* Flutter). Server 404 / network errors stay silent. Constructed at
|
||||
* launch via the construct-the-singleton trick in `MinstrelApplication`.
|
||||
*
|
||||
* A debug build never polls: it is signed with a local debug key, so the
|
||||
* server's release-signed APK could never install over it (#5103).
|
||||
*/
|
||||
@Singleton
|
||||
class UpdateBannerController @Inject constructor(
|
||||
@@ -45,6 +48,10 @@ class UpdateBannerController @Inject constructor(
|
||||
}.stateIn(scope, SharingStarted.Eagerly, null)
|
||||
|
||||
init {
|
||||
if (!BuildConfig.DEBUG) startPolling()
|
||||
}
|
||||
|
||||
private fun startPolling() {
|
||||
scope.launch {
|
||||
while (true) {
|
||||
runOnce()
|
||||
|
||||
Reference in New Issue
Block a user