feat: turn the OTA updater back on, behind a button in the top bar

OtaUpdater was complete and unreachable: upstream's UpdateHook returned false
and had an empty checkInBackground, so nothing ever called the downloader or
the installer. That seam was suspended for an audit of the published suite;
this fork ships its own builds from its own repository, signed with the
platform key the head unit already trusts, so it is live here.

The release URL and the About link now point at this repository. An APK from
anywhere else is signed with a different key, and OtaUpdater.install checks
the archive's certificate against the running app's before calling
pm install -r — a mismatch is deleted rather than installed.

The refresh button sits outside the tab row, next to About, so it cannot read
as a sixth page. It disables itself for the duration: the check, the download
and the install are one background pass, and a second press would start a
second download of the same APK. The result goes to a toast because the check
is reachable from every page and no one page owns the answer.

The stable flavour gains a matching no-op overload. It still contains no
updater at all — the class is absent from that APK rather than disabled — and
the overload exists only so the shared UI compiles in both flavours.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
TCFDONMEZ
2026-09-22 11:35:43 +03:00
co-authored by Claude
parent 2fd7b2dde1
commit a8938c48a5
7 changed files with 150 additions and 9 deletions
@@ -46,7 +46,7 @@ final class OtaUpdater {
* nothing until this repository publishes a release, which is the intended quiet.
*/
private static final String RELEASES_API =
"https://api.github.com/repos/fatihdonmezdev/EVABRP/releases";
"https://api.github.com/repos/fatihdonmezdev/MG4ABRP/releases";
/**
* Hosts an update may come from. The githubusercontent entries are the CDNs GitHub
@@ -1,13 +1,89 @@
package com.evsuite.abrp;
import android.content.Context;
import android.util.Log;
import java.io.File;
/**
* OTA is suspended while the suite safety and legal audit is open.
* Keep this flavour seam inert until a reviewed change explicitly re-enables it.
* 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.
*
* Installation goes through {@code pm install -r}, which only a platform-signed build may
* call. {@link OtaUpdater#install} checks the downloaded APK's certificate against the
* running app's first and refuses a mismatch, so an update signed with any other key is
* deleted rather than installed — the same check that makes an upstream APK unusable here.
*/
final class UpdateHook {
private static final String TAG = "EVABRP.Update";
private UpdateHook() { }
static boolean isSupported() { return false; }
static void checkInBackground(Context context) { }
static boolean isSupported() { return true; }
/** Result of one check, for the UI to report. */
interface Listener {
void onResult(String message);
}
/** Fire-and-forget check on app start. Nothing is reported; failures stay in the log. */
static void checkInBackground(Context context) {
checkInBackground(context, null);
}
/**
* Checks, downloads and installs in one background pass.
*
* The whole sequence runs off the main thread because every step blocks: the release
* API call, the download, and {@code pm install}. [listener] is called back on the main
* thread, so a UI caller can write straight to a view.
*/
static void checkInBackground(Context context, Listener listener) {
Context app = context.getApplicationContext();
android.os.Handler main = new android.os.Handler(android.os.Looper.getMainLooper());
new Thread(() -> {
String message;
try {
message = runCheck(app);
} catch (Throwable t) {
Log.w(TAG, "Update check threw", t);
message = "Güncelleme kontrolü başarısız";
}
if (listener != null) {
final String result = message;
main.post(() -> listener.onResult(result));
}
}, "ota-check").start();
}
/** The blocking part. Returns a line describing what happened, for the UI. */
private static String runCheck(Context app) {
String current;
try {
current = app.getPackageManager()
.getPackageInfo(app.getPackageName(), 0).versionName;
} catch (Exception e) {
return "Sürüm okunamadı";
}
OtaUpdater.Update update = OtaUpdater.check(current);
if (update == null) return "Güncel (" + current + ")";
Log.i(TAG, "Update available: " + update.versionName);
File apk = OtaUpdater.download(app, update);
if (apk == null) return "İndirme başarısız (" + update.versionName + ")";
// The cached APK is cleared whichever way install goes: a rejected archive must not
// linger, and an accepted one has already been copied out by the package manager.
boolean installed = OtaUpdater.install(app, apk);
if (!apk.delete()) Log.w(TAG, "Could not remove cached OTA APK");
return installed
? "Güncellendi: " + update.versionName + " — yeniden başlatın"
: "Kurulum reddedildi (imza uyuşmuyor?)";
}
}