Přeskočit obsah

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:

  1. Discover — ukáže seznam Android aplikací, ke kterým má uživatel přístup.
  2. Install — stáhne + nainstaluje APK (OS install dialog).
  3. Update — detekuje novější verzi a nabídne aktualizaci.
  4. 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É
Žádný nový bucket, žádný nový publish flow od nuly — jen Android artefakt + Android metadata v manifestu.


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}.jsonversion, - 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/beta viditelné 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.s3testcz.avaxis.avaxs3, dir app-s3testapp-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

  1. 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.
  2. 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.
  3. Cert pinning (signing_sha256) zapnout od začátku — levné, chrání i když selže katalog/S3 ACL.
  4. MDM/silent install (device-owner) — mimo scope teď; eviduj pro firemní flotily (tiché hromadné updaty bez user dialogu).
  5. 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 + backend android-apps/publish (manuálně nahraný 1 APK pro test).
  • B AVAXs3 „Aplikace" screen (discover → download presigned → sha256 → PackageInstaller) + launcher publish hook.
  • C Vendor CI assembleRelease signed 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.