From d2d38bcb06f0d390396a20de09f0f7df64b11e6b Mon Sep 17 00:00:00 2001
From: Claude <noreply@anthropic.com>
Date: Fri, 10 Jul 2026 06:55:06 +0000
Subject: [PATCH] fix(android-llmservice): monotonic versionCode for clean
 in-place upgrades
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

The release build hardcoded versionCode=1, so every APK advertised the
same version — one of the two reasons an in-place upgrade of a sideloaded
build failed (the installer shows the generic 'problem parsing the
package'). versionCode now derives from GITHUB_RUN_NUMBER in CI (strictly
increasing per run; -PappVersionCode overrides; local hand builds use 1).

The other half of clean upgrades is a STABLE signing key across builds:
without the upload-key CI secrets the release falls back to debug signing,
and a GitHub runner generates a fresh random debug key per build, so
consecutive APKs have different signers and Android rejects the update.
Documented in README ('In-place upgrades') and requirements.md R10.2/R10.4;
the actual key stays a private CI secret (never committed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HTS5FpMtBLJENTGoRUrp5m
---
 android-llmservice/README.md            | 23 +++++++++++++++++++++++
 android-llmservice/app/build.gradle.kts | 10 +++++++++-
 android-llmservice/requirements.md      |  3 ++-
 3 files changed, 34 insertions(+), 2 deletions(-)

diff --git a/android-llmservice/README.md b/android-llmservice/README.md
index dd327d39..8bdc012e 100644
--- a/android-llmservice/README.md
+++ b/android-llmservice/README.md
@@ -120,6 +120,29 @@ app + enroll in Play App Signing). After that, automated Play uploads are possib
 Gradle Play Publisher plugin or fastlane `supply` (needs a Play service-account JSON) — left
 out here to keep the demo KISS.
 
+### In-place upgrades (why "problem parsing the package" / "app not installed" on update)
+
+Android only installs an update **over** an existing app when **both** hold:
+
+1. **Same signing key.** A different signer is rejected (often surfaced as the generic
+   *"There was a problem parsing the package"* / *"App not installed"*) — you must uninstall
+   the old app first. This is exactly what happens with the **debug-signing fallback**: a
+   GitHub-hosted runner has no persistent debug keystore, so **each CI build auto-generates a
+   new random debug key** → consecutive sideloaded APKs have **different** signatures.
+   **Fix:** set the four `ANDROID_UPLOAD_KEYSTORE_BASE64` / `…_STORE_PASSWORD` / `…_KEY_ALIAS`
+   / `…_KEY_PASSWORD` CI secrets (above) so every build is signed with the **same** upload key.
+   Keep that keystore private and backed up — never commit it (the repo is public; a leaked
+   signing key lets anyone ship a fake update over your users' install).
+2. **Higher `versionCode`.** The new APK must advertise a strictly greater `versionCode`.
+   `app/build.gradle.kts` derives it from `GITHUB_RUN_NUMBER` in CI (strictly increasing per
+   run; `-PappVersionCode=<n>` overrides; local hand builds use `1`), so successive CI APKs
+   upgrade cleanly once the signing key is stable.
+
+Until the upload-key secrets are configured, sideloaded CI builds are debug-signed with an
+ephemeral key and will keep requiring an **uninstall before reinstall**. (Also make sure you
+install the `.apk` artifact, not the `.aab` — an `.aab` is not directly installable and also
+fails to parse.)
+
 ## CI: build, sign, and a **real** on-device test
 
 The `build-android-llmservice` job in `.github/workflows/publish.yml`:
diff --git a/android-llmservice/app/build.gradle.kts b/android-llmservice/app/build.gradle.kts
index c3bc9ddf..8ee12d89 100644
--- a/android-llmservice/app/build.gradle.kts
+++ b/android-llmservice/app/build.gradle.kts
@@ -37,7 +37,15 @@ android {
         applicationId = "net.ladenthin.android.llmservice"
         minSdk = 28 // AAR floor (bionic weak-symbol gate for posix_spawn); AGP enforces it.
         targetSdk = 35
-        versionCode = 1
+        // Monotonic versionCode so Android accepts IN-PLACE UPGRADES: a new APK must advertise a
+        // strictly higher code than the installed one, or the install is rejected. CI passes the
+        // workflow run number (GITHUB_RUN_NUMBER, strictly increasing per run); `-PappVersionCode`
+        // overrides it; a local hand build falls back to 1. (The other half of clean upgrades is a
+        // STABLE signing key across builds — see the signing block below and README "Signing".)
+        versionCode = (
+            providers.gradleProperty("appVersionCode").orNull
+                ?: System.getenv("GITHUB_RUN_NUMBER")
+        )?.toIntOrNull() ?: 1
         versionName = "1.0"
         // arm64 = real phones; x86_64 = the CI emulator (and x86_64 Android hardware).
         ndk {
diff --git a/android-llmservice/requirements.md b/android-llmservice/requirements.md
index 5edf3b19..d4883b2a 100644
--- a/android-llmservice/requirements.md
+++ b/android-llmservice/requirements.md
@@ -123,8 +123,9 @@ by hand only (no automated test); `build` = enforced at build/resource-compile t
 | ID | Requirement | Source | Verified by |
 |---|---|---|---|
 | R10.1 | The top-bar **model-name title is horizontally scrollable** (draggable), so a long name can be read in full — single line, no auto-marquee. | `MainActivity` top bar | manual |
-| R10.2 | The release `signingConfig` reads an **upload keystore** from env vars / `-P` props, and **falls back to debug signing** when none is set (so forks/PRs/local builds stay green). | `app/build.gradle.kts` | build |
+| R10.2 | The release `signingConfig` reads an **upload keystore** from env vars / `-P` props, and **falls back to debug signing** when none is set (so forks/PRs/local builds stay green). **Upgrade caveat:** the debug fallback uses an ephemeral per-runner key, so debug-signed CI APKs are **not** mutually upgradeable (a signer change is rejected → uninstall required); stable in-place upgrades need the upload-key secrets. | `app/build.gradle.kts` | build |
 | R10.3 | The build produces a release **`.aab`** (for Play) and an installable release **`.apk`** (sideload); the Maven Central GPG key cannot sign these — Android needs a Java keystore upload key. | `app/build.gradle.kts`; `README.md` | build |
+| R10.4 | `versionCode` is **monotonic** (derived from `GITHUB_RUN_NUMBER` in CI, strictly increasing per run; `-PappVersionCode` overrides; local hand builds use `1`), so successive release APKs advertise a higher version and Android accepts the in-place upgrade (given a stable signer, R10.2). | `app/build.gradle.kts` | build |
 
 ## R11 — Testing hooks & automated coverage
 
