fix: install OTA updates through PackageInstaller
The refresh button found 2.2.3, downloaded it, and reported "Kurulum reddedildi (imza uyuşmuyor?)" for a build signed with exactly the same platform certificate as the one already running. The signature was never the problem. install() shelled out to `pm install -r` with a path inside the app's private cache. pm does not read the file — it hands the path to the package manager service, which opens it from its own process and SELinux context, where that directory is not readable. The install failed for a reason nobody could see: install() returned a bare boolean, and UpdateHook mapped every false to the one message it had. The pm output that would have named the real error was captured into a StringBuilder and discarded. A PackageInstaller session takes no path. We open the archive ourselves, as the app, and stream it into the session, so no second process ever has to reach our cache. Approach taken from merthankaraman/DriveHub_Dort, which hit the same wall on the same head unit. Consequences worth knowing when reading a future failure: - Committing a session is asynchronous, so the refresh button can only say an install started. OtaInstallResultReceiver has the verdict, and surfaces the package manager's own INSTALL_FAILED_* text rather than guessing. - STATUS_PENDING_USER_ACTION is not an error. The receiver launches the confirmation activity the platform supplies. - The PendingIntent must be MUTABLE or it arrives with no status extras. install() now returns an enum, so a certificate mismatch, an unparseable or wrong-package archive, and a failed session each say what they are. Also checks the archive's package name against ours, which was a genuine gap: the platform key signs every app on this unit, so a certificate match alone never meant the APK was this app. The `pm install -r` exit-code test goes with the shell-out it covered. Nothing in the session path is exercisable off-device. REQUEST_INSTALL_PACKAGES and the receiver are declared in the unstable manifest only — the stable channel has no updater at all, and its merged manifest was checked to confirm it gains neither. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
322ebcc4c0
commit
091d26d6d6
@@ -0,0 +1,65 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageInstaller;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
/**
|
||||
* Where a {@link PackageInstaller} session reports what it did — UNSTABLE BUILDS ONLY.
|
||||
*
|
||||
* The session commit is asynchronous, so unlike the {@code pm install -r} it replaced, the
|
||||
* install outcome cannot be the return value of the call that started it. It arrives here.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
public final class OtaInstallResultReceiver extends BroadcastReceiver {
|
||||
|
||||
static final String ACTION_INSTALL_RESULT = "com.evsuite.abrp.OTA_INSTALL_RESULT";
|
||||
|
||||
private static final String TAG = "EVABRP.Update";
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (intent == null) return;
|
||||
int status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE);
|
||||
String message = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE);
|
||||
Log.i(TAG, "OTA install result: status=" + status + " msg=" + message);
|
||||
|
||||
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"));
|
||||
return;
|
||||
}
|
||||
confirm.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
try {
|
||||
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()));
|
||||
}
|
||||
break;
|
||||
case PackageInstaller.STATUS_SUCCESS:
|
||||
toast(context, context.getString(R.string.ota_install_success));
|
||||
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));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void toast(Context context, String text) {
|
||||
Toast.makeText(context.getApplicationContext(), text, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user