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
@@ -41,7 +41,7 @@ public class MainActivity extends AppCompatActivity {
private static final int LOCATION_PERMISSION_REQUEST = 100;
/** Shared with {@link AbrpUploadService}, which reads it on every tick. */
static final String WIFI_AUTO_KEY = "wifi_auto_connect";
private static final String REPOSITORY_URL = "https://github.com/fatihdonmezdev/EVABRP";
private static final String REPOSITORY_URL = "https://github.com/fatihdonmezdev/MG4ABRP";
/** Top-bar tabs, in page order. Parallel to {@link #panes}. */
private static final int[] TAB_IDS =
@@ -230,6 +230,7 @@ public class MainActivity extends AppCompatActivity {
setUpPager();
findViewById(R.id.about_button).setOnClickListener(v -> showAbout());
findViewById(R.id.update_button).setOnClickListener(v -> checkForUpdate(v));
bindCadenceControls();
@@ -629,6 +630,30 @@ public class MainActivity extends AppCompatActivity {
AbrpUploadService.reloadSettings();
}
// ---------- Update check ----------
/**
* Checks for a newer build, and installs one if it is there.
*
* The button is disabled for the duration rather than guarded by a flag: the whole
* sequence — API call, download, {@code pm install} — runs on a background thread, and
* a second press would start a second download of the same APK. It comes back on when
* the result lands, whatever that result is.
*
* The outcome goes to a toast because there is nowhere better: the check is reachable
* from every page, so no one page owns the answer.
*/
private void checkForUpdate(View button) {
button.setEnabled(false);
android.widget.Toast.makeText(this, R.string.update_checking,
android.widget.Toast.LENGTH_SHORT).show();
UpdateHook.checkInBackground(this, message -> {
button.setEnabled(true);
android.widget.Toast.makeText(this, message,
android.widget.Toast.LENGTH_LONG).show();
});
}
// ---------- Wi-Fi page ----------
private void bindWifiPane() {
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Refresh, for the update check in the top bar. -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -8,8s3.58,8 8,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z" />
</vector>
+15 -2
View File
@@ -92,8 +92,21 @@
android:text="@string/tab_log"
app:icon="@drawable/ic_tab_log" />
<!-- About opens a dialog, not a page: it stays a plain button so it cannot be
mistaken for a fourth destination of the pager. -->
<!-- Neither of these is a destination: they stay plain buttons, outside the tab
row, so they cannot be mistaken for pages of the pager. -->
<com.google.android.material.button.MaterialButton
android:id="@+id/update_button"
style="@style/Widget.EV.Button.Outlined"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/spacing_sm"
android:contentDescription="@string/update_check"
app:icon="@drawable/ic_refresh"
app:iconPadding="0dp"
android:minWidth="0dp"
android:paddingStart="@dimen/spacing_md"
android:paddingEnd="@dimen/spacing_md" />
<com.google.android.material.button.MaterialButton
android:id="@+id/about_button"
style="@style/Widget.EV.Button.Outlined"
+3 -1
View File
@@ -109,11 +109,13 @@
<string name="notif_channel_desc">Shows vehicle telemetry upload status</string>
<string name="notif_title">ABRP Uploader</string>
<string name="notif_action_stop">Stop</string>
<string name="update_check">Güncelleme kontrol et</string>
<string name="update_checking">Kontrol ediliyor…</string>
<string name="about_action">About</string>
<string name="about_title">Fatih\'sMG4 Hakkında</string>
<string name="about_version">Version %1$s</string>
<string name="about_attribution">Fork of malys / EVABRPUploader\nand Leon Kernan / ABRP_Uploader</string>
<string name="about_repository">github.com/fatihdonmezdev/EVABRP</string>
<string name="about_repository">github.com/fatihdonmezdev/MG4ABRP</string>
<string name="about_made_with_love">MG4 EH32 · SWI69</string>
<string name="about_logo_description">EVABRPUploader logo</string>
<string name="about_qr_description">QR code for the project repository</string>
@@ -13,8 +13,21 @@ final class UpdateHook {
private UpdateHook() { }
/**
* Mirrors the unstable seam's shape so shared UI code compiles in both flavours.
* Nothing implements it here.
*/
interface Listener {
void onResult(String message);
}
/** Does nothing. Stable users install updates themselves. */
static void checkInBackground(Context context) { }
/** Answers immediately that this channel does not self-update. */
static void checkInBackground(Context context, Listener listener) {
if (listener != null) listener.onResult("Bu sürümde otomatik güncelleme yok");
}
static boolean isSupported() { return false; }
}
@@ -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?)";
}
}