Android App Distribution & Update přes S3 — AVAXs3¶
Status: NÁVRH (2026-06-06). AVAXs3 = AVAX „Android store + updater". Telefon stahuje + instaluje + aktualizuje APK aplikací, ke kterým má uživatel přístup, přes S3 — analogicky k self-updatu desktop launcheru. Navazuje na
s3-data-exchange.md(QR pairing,_options.json, „telefon má jen S3 creds, NE JWT") a na desktop S3-channel model (catalog.py).
0. Účel¶
Dnes se Android appky (app-hotline-capture, AVAXs3) buildí ručně Dockerem a APK se přenáší ručně. Chceme, aby AVAXs3 byl jediný vstupní bod pro Android:
- Discover — ukáže seznam Android aplikací, ke kterým má uživatel přístup.
- Install — stáhne + nainstaluje APK (OS install dialog).
- Update — detekuje novější verzi a nabídne aktualizaci.
- Self-update — AVAXs3 aktualizuje sám sebe stejným kanálem.
Princip: stejný S3-channel model jako desktop launcher, jen artefakt je
.apk místo .exe. Telefon nemá JWT → katalog mu předgeneruje backend na S3
(stejně jako _options.json), download běží přes presigned URL.
1. Princip — reuse desktop S3-channel modelu¶
Desktop launcher (catalog.py) už řeší přesně tohle pro .exe:
- per-app distribuční bucket app.distribution_bucket (app-{slug}),
- channels/{stable|beta|alpha}.json = autoritativní „co je v kanálu",
- versions/{ver}/version-manifest.json + versions/{ver}/files/win/…exe.
Android = přidaná platforma do téhož stromu:
files/win/… ← desktop (existuje)
files/linux/… ← desktop (existuje)
files/android/app-release.apk ← NOVÉ
2. Úložiště v S3¶
Per-app distribuční bucket (dist_bucket(app) = app.distribution_bucket):
<app-dist-bucket>/
├── channels/
│ ├── stable.json # { version, promoted_at, promoted_by } (existuje)
│ ├── beta.json
│ └── alpha.json
└── versions/{version}/
├── version-manifest.json # ROZŠÍŘIT o blok "android" (níže)
└── files/
├── win/avax-launcher2.exe
└── android/app-release.apk # NOVÉ
version-manifest.json — nový blok android (per-version, vedle total_size_win):
{
"version": "1.4.0",
"changelog": "…",
"total_size_win": 48000000,
"android": {
"version_code": 1400, // monotónní int — Android update gate
"package_name": "cz.avaxis.hotline.capture",
"min_sdk": 28,
"apk_key": "versions/1.4.0/files/android/app-release.apk",
"apk_size": 5242880,
"apk_sha256": "9f2c…", // integrita po stažení
"signing_sha256": "AA:BB:…" // fingerprint release certu (cert pinning)
}
}
android chybí → app nemá Android build (jen desktop). To je platný stav.
3. Katalog pro telefon — users/{uid}/apps/_android.json¶
Telefon nemůže enumerovat per-app buckety (creds jsou scoped na company bucket).
Backend proto předgeneruje per-user katalog do company bucketu (stejně jako
_options.json), telefon ho čte svými creds. Download APK = presigned URL
(telefon nepotřebuje creds k dist bucketu).
Schema android-apps/v1:
{
"schema": "android-apps/v1",
"channel": "stable",
"generated_at": "2026-06-06T…Z",
"apps": [
{
"slug": "app-hotline-capture",
"name": "Hotline Capture",
"package_name": "cz.avaxis.hotline.capture",
"version": "1.4.0",
"version_code": 1400,
"min_sdk": 28,
"apk_size": 5242880,
"apk_sha256": "9f2c…",
"signing_sha256": "AA:BB:…",
"download_url": "https://s3.avaxis.cz/app-hotline-capture/versions/1.4.0/files/android/app-release.apk?X-Amz-…",
"icon_url": "https://…",
"changelog": "…",
"has_access": true
}
]
}
download_url = presigned GET, TTL (test) 7 dní; produkce kratší + STS refresh.
- Jen apps, ke kterým má user přístup (get_catalog_for_company — required_roles +
aktivní subscription), které mají android blok v manifestu daného kanálu.
4. Backend — publish endpoint¶
POST /storage/android-apps/publish (admin gate, vedle sync-options/publish):
1. entries = get_catalog_for_company(db, cid, uid, system_role) — přístupné apps.
2. Pro každou app s distribution_bucket:
- načti channels/{channel}.json → version,
- načti versions/{version}/version-manifest.json → blok android (skip když chybí),
- presigned GET na apk_key (boto3 generate_presigned_url, TTL),
- sestav entry.
3. Zapiš users/{uid}/apps/_android.json na S3 (company creds, jako _options.json).
Launcher2 ho volá při QR pairingu společně se sync-options (best-effort).
Produkce (STS pairing): telefon s refresh_tokenem → access_token → tento endpoint
→ re-publish (obnoví presigned URL). Default channel = stable; vyšší kanály
jen pro oprávněné role (gate jako required_roles).
5. Klient AVAXs3 — flow¶
Nová záložka „Aplikace":
1. Přečti users/{uid}/apps/_android.json (S3Client, spárované creds).
2. Per app: packageManager.getPackageInfo(package_name).longVersionCode
→ installed version_code (nebo „není nainstalováno").
3. Stav:
- není nainstalováno → Instalovat,
- installed version_code ≥ katalog → Aktuální,
- installed version_code < katalog → Aktualizovat.
4. Akce Instalovat/Aktualizovat:
- download z download_url (progress) do AVAX/aplikace/<slug>/<slug>.apk
(konvence docs/spec/android-device-layout.md — viditelný přehled instalérů),
- ověř sha256 proti apk_sha256 → mismatch = abort,
- PackageInstaller session (nebo ACTION_VIEW + FileProvider content://,
MIME application/vnd.android.package-archive) → OS install dialog,
- po instalaci refresh stavu.
Android nedovolí tichou instalaci bez device-owner/MDM → uživatel každou instalaci potvrdí (a jednou povolí „instalace z tohoto zdroje" pro AVAXs3).
6. Detekce updatu — version_code¶
version_code (monotónní int z build.gradle.kts) = jediný zdroj pravdy pro
update (Android požadavek). version/versionName jen pro zobrazení. Update se
aplikuje jen když: stejný package_name, vyšší version_code, stejný podpisový
klíč (jinak OS instalaci odmítne — INSTALL_FAILED_UPDATE_INCOMPATIBLE).
7. Kanály + gating¶
- Default
stable.alpha/betaviditelné jen oprávněným rolím (backend gate — endpoint generuje katalog pro kanál, na který má user nárok). - AVAXs3 si pamatuje zvolený kanál; přepnutí kanálu = re-publish katalogu.
8. Bezpečnost¶
| Vrstva | Opatření |
|---|---|
| Instalace | REQUEST_INSTALL_PACKAGES + per-app „install unknown apps" toggle (user grant). FileProvider, žádný world-readable APK. |
| Integrita | apk_sha256 ověřit po stažení, před instalací. Mismatch → abort. |
| Autenticita | signing_sha256 — AVAXs3 pinne fingerprint AVAX release certu, odmítne APK podepsaný jinak (i kdyby sha seděla na podvržený katalog). |
| Podpis | Všechny AVAX Android appky podepsané jedním release keystore → updaty fungují napříč. (infra úkol §11) |
| Přístup | Katalog jen přístupné apps (backend filtr). Telefon nikdy nevidí cizí apps. |
| Download | Presigned URL s TTL; produkce STS refresh. APK se nekopírují per-company (zůstávají v per-app bucketu). |
| Transport | HTTPS endpoint (s3.avaxis.cz). |
9. Vendor CI — publish Android APK¶
Rozšíření Android build pipeline (Dockerfile co už buildí debug APK):
1. Signed release build (assembleRelease + release keystore z Gitea secret).
2. Upload APK do versions/{ver}/files/android/app-release.apk (dist bucket).
3. Zápis android bloku do versions/{ver}/version-manifest.json.
4. Promote = update channels/{channel}.json (existující flow).
Zrcadlí desktop publish.yml. Nová verze APK = push → CI → S3 → next
android-apps/publish ji nabídne telefonům.
10. AVAXs3 self-update¶
AVAXs3 je sám Android app (package cz.avaxis.avaxs3, dist bucket app-avaxs3)
→ má vlastní entry v katalogu → aktualizuje se stejným flow jako ostatní. Při
startu zkontroluje svůj version_code proti katalogu a nabídne update.
11. Moje změny (navržené)¶
| # | Komponenta | Změna | Stav |
|---|---|---|---|
| 1 | AndroidManifest | android:label → „AVAXs3" |
✅ hotovo |
| 2 | applicationId / Kotlin package |
cz.avaxis.s3test → cz.avaxis.avaxs3, dir app-s3test→app-avaxs3 |
⏳ (pozn.: změna applicationId = čistá instalace, ztratí spárování → re-pair) |
| 3 | DB apps |
sloupec android_package_name TEXT NULL (null = bez Android buildu) — Alembic migrace |
⏳ |
| 4 | version-manifest.json |
blok android (§2) |
⏳ (vendor CI) |
| 5 | Backend | POST /storage/android-apps/publish (§4) + presigned helper |
⏳ |
| 6 | Launcher2 | api.publish_android_apps() volat v QR _gen_qr vedle sync-options |
⏳ |
| 7 | AVAXs3 | záložka „Aplikace" — AndroidAppsScreen.kt (S3 read katalog + PackageInstaller) + REQUEST_INSTALL_PACKAGES + FileProvider |
⏳ |
| 8 | Vendor CI | assembleRelease + signed + upload APK + manifest android blok (§9) |
⏳ |
12. Doporučení / rozhodnutí pro Michala¶
- Download model — PRESIGNED URL (doporučeno). Backend řídí přístup, telefon nepotřebuje creds k dist bucketům, funguje pro test (TTL 7 d + re-pair) i produkci (STS refresh). Alternativa: dát pairing creds read-scope na android artefakty (bez expirace, ale širší scope) — odložit na produkční STS.
- Jeden AVAX release keystore pro všechny Android appky (jinak updaty napříč
nefungují). Uložit jako Gitea secret
ANDROID_KEYSTORE_B64+…_PASS. Bez tohoto je celý update systém slepý — prerekvizita. - Cert pinning (
signing_sha256) zapnout od začátku — levné, chrání i když selže katalog/S3 ACL. - MDM/silent install (device-owner) — mimo scope teď; eviduj pro firemní flotily (tiché hromadné updaty bez user dialogu).
- Pořadí implementace: keystore (§11.2) → migrace
android_package_name(§11.3) → backend endpoint (§11.5) → AVAXs3 „Aplikace" screen (§11.7) → vendor CI release publish (§11.8). Discover+install MVP lze otestovat ručně nahraným APK + manifestem do jednoho app bucketu.
13. Fáze¶
- A Keystore + migrace
android_package_name+ backendandroid-apps/publish(manuálně nahraný 1 APK pro test). - B AVAXs3 „Aplikace" screen (discover → download presigned → sha256 → PackageInstaller) + launcher publish hook.
- C Vendor CI
assembleReleasesigned publish + cert pinning. - D AVAXs3 self-update + kanály gating + (budoucí) MDM silent.
14. Související¶
docs/spec/s3-data-exchange.md— QR pairing,_options.json, „telefon bez JWT".backend/app/services/catalog.py— desktop S3-channel model (reuse).desktop/launcher2/— referenční self-update klient (analog pro Android).docs/spec/s3-architecture.md— bucket-per-company, STS scope.