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:
@@ -16,10 +16,17 @@ import android.widget.Toast;
|
||||
* STATUS_PENDING_USER_ACTION is the case worth understanding: the platform is saying it will
|
||||
* install, but wants the driver to confirm. That happens when the app is not granted a silent
|
||||
* install, and the only correct response is to launch the confirmation activity it hands us.
|
||||
*
|
||||
* Any other failure — on the car the session is refused, "Not allowed to install non-system
|
||||
* apps on internal storage", and that answer can come here rather than as an exception — opens
|
||||
* Android's installer screen with the verified APK, as {@link OtaUpdater#install} does for an
|
||||
* immediate refusal. Only a cancel by the driver ends there.
|
||||
*/
|
||||
public final class OtaInstallResultReceiver extends BroadcastReceiver {
|
||||
|
||||
static final String ACTION_INSTALL_RESULT = "com.stinnesbeck.nilsmg4.OTA_INSTALL_RESULT";
|
||||
static final String EXTRA_APK = "com.stinnesbeck.nilsmg4.extra.APK";
|
||||
static final String EXTRA_SESSION = "com.stinnesbeck.nilsmg4.extra.SESSION";
|
||||
|
||||
private static final String TAG = "EVABRP.Update";
|
||||
|
||||
@@ -31,12 +38,16 @@ public final class OtaInstallResultReceiver extends BroadcastReceiver {
|
||||
Log.i(TAG, "OTA install result: status=" + status + " msg=" + message);
|
||||
UpdateLog.record(context, "Install result: status " + status
|
||||
+ (message != null ? ", " + message : ""));
|
||||
int session = intent.getIntExtra(EXTRA_SESSION, -1);
|
||||
if (session >= 0) OtaUpdater.noteAnswer(session);
|
||||
String apkPath = intent.getStringExtra(EXTRA_APK);
|
||||
|
||||
switch (status) {
|
||||
case PackageInstaller.STATUS_PENDING_USER_ACTION:
|
||||
Intent confirm = intent.getParcelableExtra(Intent.EXTRA_INTENT);
|
||||
if (confirm == null) {
|
||||
toast(context, context.getString(R.string.ota_install_failed, "no confirm intent"));
|
||||
UpdateLog.record(context, "No confirmation screen offered");
|
||||
openInstallerScreen(context, apkPath, "no confirm intent");
|
||||
return;
|
||||
}
|
||||
confirm.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
@@ -44,23 +55,52 @@ public final class OtaInstallResultReceiver extends BroadcastReceiver {
|
||||
context.startActivity(confirm);
|
||||
} catch (Throwable t) {
|
||||
Log.e(TAG, "OTA confirm UI failed: " + t.getMessage());
|
||||
toast(context, context.getString(R.string.ota_install_failed,
|
||||
t.getClass().getSimpleName()));
|
||||
UpdateLog.record(context, "Confirmation screen failed: " + t.getClass().getSimpleName());
|
||||
openInstallerScreen(context, apkPath, t.getClass().getSimpleName());
|
||||
}
|
||||
break;
|
||||
case PackageInstaller.STATUS_SUCCESS:
|
||||
toast(context, context.getString(R.string.ota_install_success));
|
||||
break;
|
||||
case PackageInstaller.STATUS_FAILURE_ABORTED:
|
||||
// The driver cancelled: nothing opens again.
|
||||
toast(context, context.getString(R.string.ota_install_failed,
|
||||
message != null ? message : "cancelled"));
|
||||
break;
|
||||
default:
|
||||
// The status message is the package manager's own words — INSTALL_FAILED_*
|
||||
// and friends. Surfaced verbatim, because guessing at it is what made the
|
||||
// previous failure unreadable.
|
||||
toast(context, context.getString(R.string.ota_install_failed,
|
||||
message != null ? message : "status " + status));
|
||||
// and friends — kept verbatim in the update log, because guessing at it is what
|
||||
// made an earlier failure unreadable. Then the installer screen, which the car
|
||||
// allows where it refuses the session.
|
||||
openInstallerScreen(context, apkPath, message != null ? message : "status " + status);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens Android's installer screen for the APK at [apkPath] off the main thread (it is
|
||||
* copied to Downloads first); a toast with [why] when there is none or it cannot open.
|
||||
*/
|
||||
private void openInstallerScreen(Context context, String apkPath, String why) {
|
||||
java.io.File apk = apkPath == null ? null : new java.io.File(apkPath);
|
||||
if (apk == null || !apk.isFile()) {
|
||||
toast(context, context.getString(R.string.ota_install_failed, why));
|
||||
return;
|
||||
}
|
||||
PendingResult pending = goAsync();
|
||||
Context app = context.getApplicationContext();
|
||||
new Thread(() -> {
|
||||
try {
|
||||
if (!OtaUpdater.openInstallerScreen(app, apk)) {
|
||||
new android.os.Handler(android.os.Looper.getMainLooper()).post(() ->
|
||||
toast(app, app.getString(R.string.ota_install_failed, why)));
|
||||
}
|
||||
} finally {
|
||||
pending.finish();
|
||||
}
|
||||
}, "ota-fallback").start();
|
||||
}
|
||||
|
||||
private static void toast(Context context, String text) {
|
||||
Toast.makeText(context.getApplicationContext(), text, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user