App 2.18.0: install from a USB stick

Settings -> App -> "Install from USB stick" (car build only). It looks
in the top folder of every mounted volume (found from the app's own
folder on it, since the head unit has no file picker) for
NilsMG4-unstable-<version>.apk, takes the newest one newer than the
installed version, checks it against its .sha256 sidecar and the
package name, and installs it with the same PackageInstaller session
as the over-the-air update. The stable build gets a stub and no button.

Tested on the emulator: 2.18.0 installed a 2.18.1 from the storage's
top folder without a confirmation, as the system user, so the OTA
install step should not need one either.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
This commit is contained in:
2026-10-18 20:59:45 +02:00
co-authored by Claude Opus 5.5
parent 0fd024cc2e
commit 69d284f81b
11 changed files with 209 additions and 4 deletions
@@ -0,0 +1,52 @@
package com.evsuite.abrp;
import java.io.File;
import java.util.Locale;
/**
* Installing a car build from a USB stick — UNSTABLE BUILDS ONLY. For development: copy
* {@code NilsMG4-unstable-<version>.apk} and its {@code .sha256} (both from build-car) to the top
* folder of a stick, plug it in, press the button on the Settings tab.
*
* The same checks as the over-the-air update apply, minus the server: the newest such file that
* is newer than the installed version, a matching SHA-256 sidecar, this app's package, and the
* installer's own signature check. The pure parts are here, unit-tested; UpdateHook does the IO.
*/
final class UsbUpdate {
private UsbUpdate() { }
/**
* The top folder of the volume an app-specific folder lives on:
* /storage/1234-ABCD/Android/data/<package>/files → /storage/1234-ABCD. Null when the path
* does not have that shape. This is how a stick is found without a file picker, which the
* head unit does not have.
*/
static File volumeRoot(File appFilesDir) {
if (appFilesDir == null) return null;
File data = appFilesDir.getParentFile() == null ? null : appFilesDir.getParentFile().getParentFile();
File android = data == null ? null : data.getParentFile();
if (!"files".equals(appFilesDir.getName()) || data == null || !"data".equals(data.getName())
|| android == null || !"Android".equals(android.getName())) return null;
return android.getParentFile();
}
/**
* The car-build APK to install among [names]: the highest version that is newer than
* [currentVersion], or null. Only names build-car writes count.
*/
static String pickNewest(String[] names, String currentVersion) {
String best = null, bestVersion = null;
if (names == null) return null;
for (String name : names) {
if (name == null || !name.toLowerCase(Locale.US).startsWith("nilsmg4-unstable-")) continue;
String version = OtaUpdater.versionFromAssetName(name);
if (version == null || !OtaUpdater.isNewer(version, currentVersion)) continue;
if (bestVersion == null || OtaUpdater.isNewer(version, bestVersion)) {
best = name;
bestVersion = version;
}
}
return best;
}
}