App 2.16.0, server 2.6.0: over-the-air updates from our own repository
The car updates itself again, from this fork's own releases, while the repository stays private: - publish-car (flake) creates the pre-release car-<version> on the forge and attaches the APK and its .sha256. It needs a forge token that may write releases, from ~/.config/nilsmg4/forge-token or $FORGE_TOKEN; it is sent in a header and never printed. - The server relays releases and attachments (/v1/app/releases, /v1/app/assets/...), read with a read-only FORGE_TOKEN from its Secret, GitHub-shaped with download URLs back at itself. Redirects away from the forge are not followed; off (404) without a token. - The app asks its server with its own paired token, allows exactly the server's host, downloads the APK itself instead of through the DownloadManager (so the token stays in the app), verifies SHA-256 and package name, and tries a PackageInstaller session (it may install silently as the system user) before falling back to Downloads. OTA.md is rewritten for this flow; the k8s and server READMEs explain the new token. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
This commit is contained in:
@@ -10,9 +10,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
/**
|
||||
* Drives {@link OtaUpdater} — UNSTABLE BUILDS ONLY.
|
||||
*
|
||||
* Upstream keeps this seam inert pending a safety and legal audit of the published suite.
|
||||
* This fork publishes its own builds from its own repository, signed with the platform key
|
||||
* the head unit already trusts, so that audit does not govern here and the seam is live.
|
||||
* Releases are this fork's own car builds, published as pre-releases on the private forge and
|
||||
* relayed by the car server (OtaUpdater); they are signed with the platform key the head unit
|
||||
* already trusts.
|
||||
*
|
||||
* Installation goes through the platform {@link android.content.pm.PackageInstaller}, not
|
||||
* {@code pm install -r}: that shelled out with a path inside our private cache, which the
|
||||
@@ -21,8 +21,11 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
* authority for signing-certificate compatibility; the app verifies SHA-256 and package name
|
||||
* before handing it the archive.
|
||||
*
|
||||
* Committing a session is asynchronous, so the message this returns says an install has been
|
||||
* started, never that one finished. {@link OtaInstallResultReceiver} has the verdict.
|
||||
* The verified APK stays in Downloads either way. The app first offers it to a PackageInstaller
|
||||
* session — as the system user it may install without asking — and only if that cannot start
|
||||
* does it open the file manager there, to tap the file. Committing a session is asynchronous,
|
||||
* so the message says an install has started, never that one finished;
|
||||
* {@link OtaInstallResultReceiver} has the verdict.
|
||||
*/
|
||||
final class UpdateHook {
|
||||
|
||||
@@ -44,9 +47,11 @@ final class UpdateHook {
|
||||
Context app = context.getApplicationContext();
|
||||
new Thread(() -> {
|
||||
try {
|
||||
ServerConfig config = ServerConfig.get(app);
|
||||
if (!config.isConfigured()) return;
|
||||
String current = app.getPackageManager()
|
||||
.getPackageInfo(app.getPackageName(), 0).versionName;
|
||||
OtaUpdater.Update update = OtaUpdater.check(current);
|
||||
OtaUpdater.Update update = OtaUpdater.check(config, current);
|
||||
if (update != null && update.hashUrl != null)
|
||||
new android.os.Handler(android.os.Looper.getMainLooper())
|
||||
.post(() -> android.widget.Toast.makeText(app,
|
||||
@@ -100,25 +105,37 @@ final class UpdateHook {
|
||||
return "Could not read version";
|
||||
}
|
||||
|
||||
OtaUpdater.Update update = OtaUpdater.check(current);
|
||||
ServerConfig config = ServerConfig.get(app);
|
||||
if (!config.isConfigured()) return "Set up the server first (Server tab): updates come through it";
|
||||
OtaUpdater.Update update = OtaUpdater.check(config, current);
|
||||
if (update == null) return "Up to date (" + current + ")";
|
||||
|
||||
Log.i(TAG, "Update available: " + update.versionName);
|
||||
String expectedHash;
|
||||
try {
|
||||
expectedHash = OtaUpdater.fetchExpectedSha256(update);
|
||||
expectedHash = OtaUpdater.fetchExpectedSha256(update, config);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "OTA hash unavailable", e);
|
||||
Log.w(TAG, "OTA hash unavailable: " + e.getMessage());
|
||||
return "Update refused: no SHA-256 verification";
|
||||
}
|
||||
File apk = OtaUpdater.download(app, update, expectedHash, percent -> {
|
||||
File apk = OtaUpdater.download(update, expectedHash, config, percent -> {
|
||||
if (listener != null) main.post(() -> listener.onProgress(percent));
|
||||
});
|
||||
if (apk == null) return "Download or SHA-256 verification failed ("
|
||||
+ update.versionName + ")";
|
||||
|
||||
switch (OtaUpdater.install(app, apk)) {
|
||||
case SESSION_STARTED:
|
||||
return "Installing " + update.versionName + "… the app restarts when it is done";
|
||||
case INSTALL_UI_STARTED:
|
||||
return "Confirm the installation of " + update.versionName + " on the screen";
|
||||
case UNREADABLE_ARCHIVE:
|
||||
return "Update refused: the downloaded file is not this app";
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (!OtaUpdater.openDownloads(app))
|
||||
return "APK downloaded to Downloads; could not open the file manager";
|
||||
return update.versionName + ".apk downloaded to Downloads — tap the file to install";
|
||||
return update.versionName + " is in Downloads; open it with the Files app to install";
|
||||
return update.versionName + ".apk is in Downloads — tap the file to install";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user