diff --git a/OTA.md b/OTA.md new file mode 100644 index 0000000..60138d1 --- /dev/null +++ b/OTA.md @@ -0,0 +1,109 @@ +# Publishing an OTA release + +How to ship an update the car will install by itself. Written for whoever does this +next — probably me, having forgotten the details. + +## What the updater actually looks for + +`OtaUpdater.check()` reads `https://api.github.com/repos/fatihdonmezdev/MG4ABRP/releases` +and walks **every** entry, keeping the highest version rather than the first match. A +release is a candidate only if all of these hold: + +| Requirement | Where it is enforced | What happens otherwise | +|---|---|---| +| Marked **pre-release** | `release.optBoolean("prerelease")` | Skipped entirely | +| Asset name contains `unstable` | `name.contains("unstable")` | Asset ignored | +| Asset name ends `-.apk` | `ASSET_VERSION` regex | No version, asset ignored | +| Version strictly higher than installed | `isNewer()` | Nothing to do, reports "Güncel" | +| Download host on the allowlist | `isAllowedUrl()` | Refused and logged | +| APK signed with the **same certificate** as the running app | `signatureMatchesRunningApp()` | Deleted, never installed | + +The last one is the one that bites. The car runs a platform-signed build; an APK signed +with a debug key — or anyone else's key — is downloaded, rejected and deleted. Sign every +release with the same platform key or the update silently never lands. + +The version comes from the **asset filename**, not the git tag. The tag can stay +`unstable` forever and be moved; the filename is what identifies a build. + +## Release checklist + +1. **Raise the version.** `app/build.gradle`, `defaultConfig`: + + ```gradle + versionCode 20 // must increase — Android orders updates by this, not by name + versionName "2.2.3" + ``` + + The unstable flavour appends its own suffix, so `2.2.3` becomes `2.2.3.0-unstable`. + Check what the installed build reports before picking a number: the app's About + dialog shows it, and `isNewer` compares numerically segment by segment. + +2. **Build.** + + ```bash + ./gradlew :app:assembleUnstableDebug + ``` + +3. **Sign with the platform key.** Options come *before* the input APK — apksigner + rejects the other order. + + ```bash + BT=~/Android/Sdk/build-tools/35.0.0 + KEYS=~/Desktop/Coding/LUMINA/DriveHub_Kamera/tools + + "$BT/zipalign" -f 4 \ + app/build/outputs/apk/unstable/debug/app-unstable-debug.apk aligned.apk + + "$BT/apksigner" sign \ + --key "$KEYS/platform.pk8" \ + --cert "$KEYS/platform.x509.pem" \ + --out FatihsMG4-unstable-2.2.3.apk \ + aligned.apk + + rm aligned.apk + ``` + + The output filename **is** the version the updater reads. Name it + `FatihsMG4-unstable-.apk` and nothing else. + +4. **Verify the signer** before uploading. It must say `CN=Android`: + + ```bash + "$BT/apksigner" verify --print-certs FatihsMG4-unstable-2.2.3.apk | grep DN: + ``` + +5. **Publish.** On github.com/fatihdonmezdev/MG4ABRP → Releases → Draft a new release: + - tag: `unstable` (reuse it; the rolling tag is the design) + - **tick "Set as a pre-release"** ← the whole thing is inert without this + - attach the signed APK + +6. **Install it.** Open the app on the head unit and press the refresh button in the top + bar. Expected toast: `Güncellendi: 2.2.3 — yeniden başlatın`. + +## When it says something else + +| Toast | Meaning | +|---|---| +| `Güncel (2.2.2.0-unstable)` | No release beat the installed version. Check the pre-release tick and the asset filename. | +| `İndirme başarısız` | Release found, download did not complete. Usually no network. | +| `Kurulum reddedildi (imza uyuşmuyor?)` | Signed with the wrong key — rebuild and re-sign with `platform.pk8`. | +| `Güncelleme kontrolü başarısız` | The check threw. `adb logcat -s EVABRP.Update OtaUpdater` if a cable is available; otherwise the in-app Log page. | + +## Untested + +`OtaUpdater.install()` shells out to `/system/bin/pm install -r`, which only a +platform-signed app may call. That is what this build is, so it should work — but it has +never actually run on the car. If installs are refused with a signature that *does* match, +the fallback is an `ACTION_INSTALL_PACKAGE` intent, which asks the driver to confirm +instead of installing silently. + +## Why the check is manual + +The refresh button is the only trigger. `UpdateHook.checkInBackground(this)` also runs on +app start, but the app is rarely launched — the upload service does the work, and the +driver does not open the UI on every trip. The button is there so an update can be pulled +deliberately rather than waiting for a launch that may not come. + +The upload service does **not** check for updates on its tick. Installing a new APK +restarts the process, and doing that mid-drive would interrupt the telemetry the service +exists to send.