This restores the interrupted stash work on a dedicated feature branch, including backend decoding support, Dockerized build scripts, and staging channel artifacts for end-to-end OTA validation. Co-authored-by: Cursor <cursoragent@cursor.com>
5.3 KiB
Over-the-air (OTA) updates — deployment schema v0
The app checks a stable channel URL, follows it to a per-build manifest, then downloads the update. Version comparison uses major.minor.build (packed into Android versionCode at build time). Deploy never computes versionCode by hand.
The client runs checks and downloads in OtaUpdateService (a standalone Service, not embedded in cast/receiver services). Launch checks enqueue work there; download uses a foreground service (dataSync) with a progress notification.
MQTT is fully supported for OTA sources using retained topic payloads (mqtt://... / mqtts://...). The URI path is treated as the exact topic to subscribe.
URL layout
https://host/v0/ota/channel/stable.json
https://host/v0/ota/00/00.{major}/00.{major}.{minor}/android_cast_00.{major}.{minor}.{build}.otapkg
https://host/v0/ota/00/00.{major}/00.{major}.{minor}/android_cast_00.{major}.{minor}.{build}_sign.json
https://host/v0/ota/00/00.{major}/00.{major}.{minor}/android_cast_00.{major}.{minor}.{build}_manifest.json
https://host/v0/ota/00/00.{major}/00.{major}.{minor}/android_cast_00.{major}.{minor}.{build}.otabundle.zip
v0— deployment schema version (default channel).00afterota/— OTA payload format revision within v0.- Components are zero-padded to 2 digits in paths and filenames (
00.01.05).
Versioning
In app/build.gradle (overridable via local.properties):
ota.major=0
ota.minor=1
ota.build=0
Packed rule (each part 0–99):
versionCode = major * 10000 + minor * 100 + build
Example: 0.1.5 → versionCode 105.
JSON files
v0/ota/channel/stable.json
{
"schema": "v0",
"manifestUrl": "https://host/v0/ota/00/00.01/00.01.05/android_cast_00.01.05_manifest.json"
}
*_manifest.json
{
"schema": "v0",
"major": 0,
"minor": 1,
"build": 5,
"versionName": "0.1.5",
"apkUrl": "https://host/v0/ota/00/00.01/00.01.05/android_cast_00.01.05.otapkg",
"signUrl": "https://host/v0/ota/00/00.01/00.01.05/android_cast_00.01.05_sign.json",
"sizeBytes": 42100000,
"bundleUrl": "https://host/v0/ota/00/00.01/00.01.05/android_cast_00.01.05.00.otabundle.zip",
"bundleSha256": "…",
"bundleSizeBytes": 42150000,
"mandatory": false,
"releaseNotes": ""
}
*_sign.json
{
"schema": "v0",
"sha256": "…"
}
The app loads sha256 from sign.json when the manifest omits it (APK hash inside the bundle or standalone .otapkg).
Optional HTTP bundle (.otabundle.zip)
generate-ota-v0.sh emits a STORE (uncompressed) zip so the APK is not re-deflated:
| Entry inside zip | Content |
|---|---|
manifest.json |
Same as *_manifest.json |
sign.json |
Same as *_sign.json |
package.apk |
Release APK bytes |
Check path (lightweight): channel → small *_manifest.json (+ optional *_sign.json) — no bundle download until the user installs.
Download path (HTTP): if bundleUrl is http/https, the app downloads one zip, verifies bundleSha256, unpacks, verifies package.apk against sha256, then runs the installer. If bundleUrl is absent, it falls back to separate apkUrl / signUrl fetches (required for MQTT).
MQTT: keep per-artifact retained topics (manifest.json, sign.json, .otapkg). Do not publish full bundles on MQTT (message size limits). The app ignores bundleUrl when the scheme is not HTTP(S).
MQTT transport
Use the same JSON payload shapes as HTTP, but publish them as retained messages on topics that match your URI paths:
mqtt://host:1883/v0/ota/channel/stable.json-> retained payload isstable.jsoncontentmqtt://host:1883/v0/ota/00/..._manifest.json-> retained payload is manifest JSONmqtt://host:1883/v0/ota/00/..._sign.json-> retained payload is sign JSONmqtt://host:1883/v0/ota/00/...otapkg-> retained payload is raw APK/otapkg bytes
When ota.trusted=true and build is debug, strict checksum/sign enforcement is relaxed for that source. Release builds always remain strict.
Publish from a release APK
chmod +x scripts/generate-ota-v0.sh
./scripts/generate-ota-v0.sh app/build/outputs/apk/release/app-release.apk https://your-host:port ./ota-publish staging
Fourth argument is the channel file name (stable, staging, next, …). Upload ota-publish/v0/ to your web root so https://your-host/v0/ota/... resolves.
Docker build + staging channel
See BUILD_DEPLOY.md:
export OTA_BASE_URL=https://apps.f0xx.org
./scripts/docker-build-ota.sh
./scripts/deploy-ota-staging.sh # after setting OTA_DEPLOY_TARGET
App configuration
local.properties (not committed):
ota.channel.url=https://your-host/v0/ota/channel/stable.json
ota.major=0
ota.minor=1
ota.build=0
Or set the channel URL under Developer settings → App updates (OTA).
Legacy ota.manifest.url still works as a direct manifest URL fallback.
Checklist
- Bump
ota.major/ota.minor/ota.build(or Gradle defaults). - Build signed release APK (
versionCodeis derived automatically). - Run
generate-ota-v0.shand uploadv0/ota/**(including.otabundle.zip). - Confirm
stable.jsonpoints at the new*_manifest.json.