App 2.40.1: an update always reaches the installer screen

The car refuses the installer session, and only a refusal thrown at
once opened the installer screen; one that came back through the
session's result ended in a toast, so the update downloaded and then
nothing seemed to happen. Now any result but success or the driver's
cancel opens the installer screen with the verified APK, as does a
confirmation that cannot be shown, and a session silent for 20 s is
abandoned for it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6
This commit is contained in:
2026-10-27 00:18:31 +01:00
co-authored by Claude Opus 5.5
parent 9ef058cb44
commit c3c47f5159
5 changed files with 121 additions and 21 deletions
@@ -464,23 +464,59 @@ final class OtaUpdater {
static InstallResult install(Context context, File apk) {
if (!archiveIsThisPackage(context, apk)) return InstallResult.UNREADABLE_ARCHIVE;
try {
commitSession(context, apk);
int sessionId = commitSession(context, apk);
watchForAnswer(context, sessionId, apk);
return InstallResult.SESSION_STARTED;
} catch (Throwable t) {
Log.w(TAG, "OTA session install failed; trying system install UI", t);
UpdateLog.record(context, "Installer session failed: " + t);
try {
launchInstallerActivity(context, apk);
UpdateLog.record(context, "Opened the system installer instead");
return InstallResult.INSTALL_UI_STARTED;
} catch (Throwable fallback) {
Log.w(TAG, "OTA install UI failed", fallback);
UpdateLog.record(context, "System installer failed too: " + fallback);
return InstallResult.SESSION_FAILED;
}
return openInstallerScreen(context, apk) ? InstallResult.INSTALL_UI_STARTED : InstallResult.SESSION_FAILED;
}
}
/** The installer screen for [apk], as the fallback; false if even that could not open. */
static boolean openInstallerScreen(Context context, File apk) {
try {
launchInstallerActivity(context, apk);
UpdateLog.record(context, "Opened the system installer instead");
return true;
} catch (Throwable fallback) {
Log.w(TAG, "OTA install UI failed", fallback);
UpdateLog.record(context, "System installer failed too: " + fallback);
return false;
}
}
/** How long a committed session may stay silent before the installer screen is opened. */
private static final long ANSWER_WAIT_MS = 20_000L;
/** Sessions the platform has answered (any status), so the watch knows to stand down. */
private static final java.util.Set<Integer> answered =
java.util.Collections.synchronizedSet(new java.util.HashSet<>());
/** The platform said something about [sessionId]; called by {@link OtaInstallResultReceiver}. */
static void noteAnswer(int sessionId) {
answered.add(sessionId);
}
/**
* A committed session normally answers within a second or two: installed (the app
* restarts), refused, or "confirm on screen". If it says nothing at all, the update would
* end silently after its download, so the installer screen is opened instead.
*/
private static void watchForAnswer(Context context, int sessionId, File apk) {
Context app = context.getApplicationContext();
new android.os.Handler(android.os.Looper.getMainLooper()).postDelayed(() -> {
if (answered.contains(sessionId)) return;
UpdateLog.record(app, "No answer from the installer after " + ANSWER_WAIT_MS / 1000 + " s");
try {
app.getPackageManager().getPackageInstaller().abandonSession(sessionId);
} catch (Throwable ignored) {
// already gone
}
new Thread(() -> openInstallerScreen(app, apk), "ota-fallback").start();
}, ANSWER_WAIT_MS);
}
/**
* True if the archive parses and declares our own package name.
*
@@ -510,7 +546,8 @@ final class OtaUpdater {
}
}
private static void commitSession(Context context, File apk) throws Exception {
/** Commits [apk] to a new installer session and returns its id. */
private static int commitSession(Context context, File apk) throws Exception {
PackageInstaller installer = context.getPackageManager().getPackageInstaller();
PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(
PackageInstaller.SessionParams.MODE_FULL_INSTALL);
@@ -533,7 +570,10 @@ final class OtaUpdater {
}
Intent callback = new Intent(context, OtaInstallResultReceiver.class)
.setAction(OtaInstallResultReceiver.ACTION_INSTALL_RESULT)
.setPackage(context.getPackageName());
.setPackage(context.getPackageName())
// Where the verified APK is, for the installer screen if the session fails.
.putExtra(OtaInstallResultReceiver.EXTRA_APK, apk.getAbsolutePath())
.putExtra(OtaInstallResultReceiver.EXTRA_SESSION, sessionId);
// MUTABLE: the platform fills in EXTRA_STATUS and, when it wants the driver to
// confirm, EXTRA_INTENT. An immutable PendingIntent would arrive empty.
PendingIntent pending = PendingIntent.getBroadcast(context, sessionId, callback,
@@ -543,6 +583,7 @@ final class OtaUpdater {
session.commit(pending.getIntentSender());
committed = true;
Log.i(TAG, "OTA session " + sessionId + " committed (" + apk.length() + " bytes)");
return sessionId;
} finally {
// Abandon failed staging, but always close our local handle after a commit too.
if (!committed) {