Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions android-llmservice/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`:
Expand Down
10 changes: 9 additions & 1 deletion android-llmservice/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
3 changes: 2 additions & 1 deletion android-llmservice/requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading