Files
KwabaTV/docs/superpowers/specs/2026-09-27-in-app-updates-design.md
2026-09-27 19:02:31 +02:00

7.3 KiB

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:

  • <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
  • No new <provider> 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.