Add in-app updates design spec
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LHgWjmEJXsJEi1EVAoTp4K
This commit is contained in:
@@ -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:**
|
||||||
|
- `<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.
|
||||||
Reference in New Issue
Block a user