From ef3648ad14afca6d04776a7165ad5c013685328f Mon Sep 17 00:00:00 2001 From: "ggb.kevin" Date: Sun, 27 Sep 2026 19:02:31 +0200 Subject: [PATCH] Add in-app updates design spec Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01LHgWjmEJXsJEi1EVAoTp4K --- .../specs/2026-09-27-in-app-updates-design.md | 158 ++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 docs/superpowers/specs/2026-09-27-in-app-updates-design.md diff --git a/docs/superpowers/specs/2026-09-27-in-app-updates-design.md b/docs/superpowers/specs/2026-09-27-in-app-updates-design.md new file mode 100644 index 0000000..1f1097f --- /dev/null +++ b/docs/superpowers/specs/2026-09-27-in-app-updates-design.md @@ -0,0 +1,158 @@ +# In-App Updates — Design + +Date: 2026-09-27 +Status: approved, pending implementation plan + +## Background + +KwabaTV has no update mechanism today — every new build requires a manual +`adb install -r` sideload from a development machine. That's fine for this +session's iteration pace, but not sustainable once the app is "done" and +just sitting on a Fire TV Stick: any future fix requires physically +reconnecting ADB. + +Kwaba Sync's own desktop Electron app (`desktop/`, in the `kwaba-sync` +repo) already solved an equivalent problem: the server proxies the latest +GitHub-style release from Gitea (`src/desktop-release.js`), gated behind a +login because that repository is private, and the Electron app polls it, +shows an in-window update banner, and never installs without an explicit +click. + +KwabaTV's own Gitea repo (`GGB/KwabaTV`) is public — confirmed via its +Gitea API (`visibility":"public"`, releases endpoint returns 200 +unauthenticated). That removes the reason the desktop app's design needed a +server-side proxy at all (private-repo auth). This design is intentionally +simpler than desktop's as a result: no `kwaba-sync` involvement, no +server-side changes, no coordination needed with that repo's owner. It +mirrors desktop's *behavior* (check on launch, never install silently, +graceful failure) without mirroring its *architecture*. + +## Goal + +The app checks Gitea for a newer release on every launch, and — if one +exists — offers to download and install it, entirely on-device. + +## Release convention + +- `versionName` in `app/build.gradle.kts` moves from the current `"1.0"` + to a proper 3-part semver (e.g. `"1.1.0"`) as part of this work, since + the comparison logic below assumes major/minor/patch on both sides. +- Tag format: `vX.Y.Z` (e.g. `v1.1.0`), matching `versionName`. +- Exactly one APK asset per release, always named `KwabaTV.apk`, so the + app never has to guess which asset to download. +- Releases are published manually for now (no CI in this repo yet). + Drafts and pre-releases are ignored by the version check. + +## Components + +All new code lives in kwabatv; nothing else changes outside it. + +**`UpdateChecker.kt`** — a small, self-contained class with one entry +point, e.g. `checkForUpdate(context, onAvailable: (UpdateInfo) -> Unit)`. +On a background thread, it does a plain HTTP GET to +`https://git.whodovoodoo.de/api/v1/repos/GGB/KwabaTV/releases`, picks the +highest-versioned non-draft, non-prerelease release whose tag matches +`vX.Y.Z`, and compares it numerically (major/minor/patch) against +`BuildConfig.VERSION_NAME`. If the release is newer, it calls back with +`{ version, apkDownloadUrl }` on the main thread. Any failure along the +way (network unreachable, non-200, malformed JSON, no matching release) is +swallowed silently — no error UI, no retry logic beyond "try again next +launch." + +**`MainActivity.onCreate()`** calls `UpdateChecker.checkForUpdate(...)` +right after `webView.loadUrl(BuildConfig.WEBSITE_URL)`, so the check never +delays getting to content. If the callback fires, it shows a native +`AlertDialog`: + +> **KwabaTV v1.1.0 ist verfügbar** +> [Aktualisieren] [Später] + +Android's default dialog buttons are focusable and DPAD-navigable with no +extra work. "Später" just dismisses; the same check runs again next +launch, with no persisted "skip this version" state. + +**Download + install**, triggered by "Aktualisieren": + +- Uses `android.app.DownloadManager` (a base-SDK system service — no + AndroidX needed, keeping the app's existing zero-dependency footprint) + to download the APK asset directly from Gitea's public release URL. +- Registers a `BroadcastReceiver` for + `DownloadManager.ACTION_DOWNLOAD_COMPLETE` to know when the download + finishes. +- On completion, calls `DownloadManager.getUriForDownloadedFile(id)` — this + returns a ready-to-use `content://` URI with no custom `FileProvider` + setup required (the earlier draft of this design needed one, which + would have pulled in an AndroidX dependency; `DownloadManager` avoids + that entirely). +- Fires `Intent(ACTION_VIEW)` with that URI, MIME type + `application/vnd.android.package-archive`, and + `FLAG_GRANT_READ_URI_PERMISSION`, which opens Android's own system + package installer. The user taps "Install" themselves inside that + system UI — Android does not allow silent unattended installs, by OS + design, regardless of permissions granted. + +**First-run permission gate:** before firing the install intent, check +`packageManager.canRequestPackageInstalls()`. If false (first time ever +installing an update), send the user to +`Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES` for this package instead, so +they can grant "install unknown apps" for KwabaTV specifically. They then +press "Aktualisieren" again to retry. + +**New manifest entries:** +- `` +- No new `` entry (avoided by using `DownloadManager`, see + above). + +## Data flow + +``` +App launch + → webView.loadUrl(WEBSITE_URL) [not blocked by the check] + → UpdateChecker (background thread) + GET git.whodovoodoo.de/.../releases + → newer release found? + no → nothing happens + yes → AlertDialog on main thread + "Später" → nothing further this launch + "Aktualisieren" → canRequestPackageInstalls()? + no → Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES + yes → DownloadManager.enqueue(apk) + → ACTION_DOWNLOAD_COMPLETE + → ACTION_VIEW install intent + → system installer UI (user taps Install) +``` + +## Error handling + +Every failure mode in the check itself (DNS/network failure, non-200, +malformed JSON, no releases, no tag matching `vX.Y.Z`) results in no +dialog and no visible change — identical to a launch where the app is +already current. This matches desktop's graceful-degradation philosophy: +an update check is a nice-to-have, never something that should be able to +block or degrade normal app use. + +Download failures (`DownloadManager` reports a failed status) are also +silent for now — the user can just press "Aktualisieren" again next +launch, or manually sideload as a fallback (this doesn't remove that +option). A visible failure toast is a reasonable future improvement, not +required for the first version of this feature. + +## Testing + +This feature cannot be meaningfully unit-tested in isolation — the +interesting behavior is entirely about real network calls, real Android +permission state, and the real system package installer. It will be +verified end-to-end on the real Fire TV Stick used throughout this +project: publish an actual test release on the `GGB/KwabaTV` Gitea repo, +then run the full check → dialog → download → permission-gate → +install flow on-device, the same hands-on verification approach used for +every other feature this session. + +## Out of scope (for this pass) + +- Any `kwaba-sync` server changes (Option B from the design discussion — + hosting the APK for discovery from the Kwaba website's lobby — is a + separate, later concern, not required for in-app updates to work). +- CI/automated release publishing. +- Periodic re-checks while the app stays open (check is launch-only). +- "Skip this version" / persisted dismissal state.