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=` 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