Rename package and APKs to NilsMG4, credit Fatih, add SPECS.md

- applicationId com.evsuite.abrp -> com.stinnesbeck.nilsmg4 (the car
  build is .unstable); the Java namespace is unchanged. Installs next to
  upstream's app rather than over it.
- APK names NilsMG4-*.apk, debug builds included; build-car follows.
- OTA install-result action, cache/download names, User-Agent and log
  tags follow the new name.
- About credits fatihdonmezdev / FatihsMG4.
- SPECS.md journals what changed and what is still to do, starting
  with pointing the About link and QR code at git.stinnesbeck.com/nils/mg4.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-17 13:08:09 +02:00
co-authored by Claude Opus 5.5
parent 3da3fa9f49
commit a707b590f7
11 changed files with 90 additions and 32 deletions
+55
View File
@@ -0,0 +1,55 @@
# SPECS — Nils' MG4
Journal for this fork of [fatihdonmezdev/FatihsMG4](https://github.com/fatihdonmezdev/FatihsMG4):
what has been changed, and what is still to do.
- Our repository: https://git.stinnesbeck.com/nils/mg4
- Upstream: https://github.com/fatihdonmezdev/FatihsMG4 (itself a fork of malys / EVABRPUploader
and Leon Kernan / ABRP_Uploader). Credited in the app's About dialog.
## To do
- [ ] **About dialog link and QR code → https://git.stinnesbeck.com/nils/mg4.**
Both come from `REPOSITORY_URL` in `app/src/main/java/com/evsuite/abrp/MainActivity.java`
and `about_repository` in `app/src/main/res/values/strings.xml`. They still point to Fatih's
GitHub repository.
- [ ] **Re-enable OTA updates against our own releases.** `RELEASES_API` in
`app/src/unstable/java/com/evsuite/abrp/OtaUpdater.java` points at `updates.invalid` on purpose,
so checks fail. The updater speaks the GitHub releases API and only allows GitHub hosts
(`ALLOWED_HOSTS`). For git.stinnesbeck.com it needs the host on the allowlist, and the
response parsing checked against that server's releases API. Release assets must be named
`NilsMG4-unstable-<version>.apk` with a `.sha256` sidecar (`build-car` writes both).
- [ ] **Fix the unit tests.** They don't compile upstream: `AbrpApiTest`, `TelemetryPayloadTest`
and others still use `AbrpApi`/`TelemetryPayload`, which upstream commit `ea8032a` deleted.
- [ ] **Charging currency.** Prices and costs are shown in Turkish lira (`₺/kWh`, `₺`) in
`pane_charging.xml` and `MainActivity.java`.
- [ ] **Cloud consumption sync.** `CONSUMPTION_INSTALLATION_ID` in `app/build.gradle` is a fixed
UUID inherited from upstream. Set our own before configuring `CONSUMPTION_API_URL` /
`CONSUMPTION_API_TOKEN`, so our data cannot land under upstream's id.
- [ ] **Translate the rest.** The `web/` dashboard (Next.js) and docs (`CHANGELOG.md`, `OTA.md`'s
message table, comments in `mise.toml`/`app/build.gradle`) are still Turkish or French.
- [ ] **Docs still use the old package ID.** `README.md`, `OTA.md`, `CHANGELOG.md`, `mise.toml` and
`docs/MG4_PLATFORM_INSTALL.md` refer to `com.evsuite.abrp(.unstable)` and `FatihsMG4-*.apk`.
- [ ] **Move hardcoded UI strings into `strings.xml`.** Many are inline in Java and layouts.
## Done
- 2026-10-17 — Translated the app UI from Turkish to English.
- 2026-10-17 — Nix flake: `nix develop` provides JDK 21 and the Android SDK. `build-car` builds the
unstable APK and signs it with the AOSP test platform key, which the MG4 head unit uses
(cert SHA-256 `c8a2e9bc…192ab8`).
- 2026-10-17 — OTA update checks disabled by pointing at an unresolvable host (see To do).
- 2026-10-17 — Renamed to "Nils' MG4": package ID `com.stinnesbeck.nilsmg4` (`.unstable` for the car
build), APK names `NilsMG4-*.apk`. It installs next to the old FatihsMG4 app instead of replacing
it, and starts with empty settings and history.
## Installing on the car
```
nix develop
build-car
adb push build/car/NilsMG4-unstable-<version>.apk /data/local/tmp/update.apk
adb shell "pm install -r -f -d -t /data/local/tmp/update.apk"
```
To remove the old upstream app: `adb uninstall com.evsuite.abrp.unstable`.
+7 -4
View File
@@ -31,7 +31,7 @@ android {
}
defaultConfig {
applicationId "com.evsuite.abrp"
applicationId "com.stinnesbeck.nilsmg4"
minSdk 27
targetSdk 36
versionCode 56
@@ -113,7 +113,7 @@ android {
}
// Version publiée par la CI pour composer le nom de l'asset APK : ce nom DOIT être
// comparable par l'updater ("FatihsMG4-unstable-1.0.42.apk"). Le tag de la
// comparable par l'updater ("NilsMG4-unstable-1.0.42.apk"). Le tag de la
// pré-release est la constante "unstable", écrasée à chaque build.
tasks.register('printUnstableVersion') {
doLast {
@@ -129,8 +129,11 @@ androidComponents {
// Le canal unstable garde un nom de fichier stable : l'updater identifie
// la version par le tag de la pré-release, jamais par le nom d'APK.
output.outputFileName.set(variant.flavorName == "unstable"
? "FatihsMG4-unstable.apk"
: "FatihsMG4-${variant.flavorName}-${output.versionName.get()}.apk")
? "NilsMG4-unstable.apk"
: "NilsMG4-${variant.flavorName}-${output.versionName.get()}.apk")
} else {
// Debug builds: NilsMG4-stable-debug.apk, NilsMG4-unstable-debug.apk.
output.outputFileName.set("NilsMG4-${variant.flavorName}-${variant.buildType}.apk")
}
}
}
@@ -28,7 +28,7 @@ import java.util.List;
* picks up right where the old one left off instead of showing zeroes.
*/
final class ConsumptionCloudClient {
private static final String TAG = "FatihsMG4.Mongo";
private static final String TAG = "NilsMG4.Mongo";
private static final long CONSUMPTION_SYNC_INTERVAL_MS = 2 * 60_000L;
private static final long CHARGE_RETRY_INTERVAL_MS = 5 * 60_000L;
private static final long HISTORY_FETCH_INTERVAL_MS = 5 * 60_000L;
@@ -216,7 +216,7 @@ final class ConsumptionTracker {
if (cloudLifetime.km > local.km) {
lifetime.set(cloudLifetime.km, cloudLifetime.kwh, cloudLifetime.hours, cloudLifetime.soc);
persist();
Log.i("FatihsMG4.Consumption", "Lifetime baseline synced from cloud: "
Log.i("NilsMG4.Consumption", "Lifetime baseline synced from cloud: "
+ cloudLifetime.km + " km (was " + local.km + " km)");
}
}
+1 -1
View File
@@ -128,7 +128,7 @@
<string name="about_action">About</string>
<string name="about_title">About Nils\' MG4</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_attribution">Fork of fatihdonmezdev / FatihsMG4,\nmalys / EVABRPUploader\nand Leon Kernan / ABRP_Uploader</string>
<string name="about_repository">github.com/fatihdonmezdev/FatihsMG4</string>
<string name="about_made_with_love">MG4 EH32 · SWI69</string>
<string name="about_logo_description">Nils\' MG4 logo</string>
@@ -102,13 +102,13 @@ public class OtaUpdaterTest {
@Test
public void versionIsReadFromTheAssetName() {
// The release tag is the constant "unstable", so the asset name carries the build.
assertEquals("1.0.42", OtaUpdater.versionFromAssetName("FatihsMG4-unstable-1.0.42.apk"));
assertEquals("1.0.100", OtaUpdater.versionFromAssetName("FatihsMG4-unstable-1.0.100.APK"));
assertEquals("1.0.42", OtaUpdater.versionFromAssetName("NilsMG4-unstable-1.0.42.apk"));
assertEquals("1.0.100", OtaUpdater.versionFromAssetName("NilsMG4-unstable-1.0.100.APK"));
}
@Test
public void assetNameWithoutAVersionIsIgnored() {
assertNull(OtaUpdater.versionFromAssetName("FatihsMG4-unstable.apk"));
assertNull(OtaUpdater.versionFromAssetName("NilsMG4-unstable.apk"));
assertNull(OtaUpdater.versionFromAssetName("unstable"));
}
@@ -125,14 +125,14 @@ public class OtaUpdaterTest {
public void sha256SidecarFormatsAreParsed() {
String hash = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
assertEquals(hash, OtaUpdater.parseExpectedSha256(hash,
"FatihsMG4-unstable-2.2.7.apk"));
"NilsMG4-unstable-2.2.7.apk"));
assertEquals(hash, OtaUpdater.parseExpectedSha256(
hash + " *FatihsMG4-unstable-2.2.7.apk\n",
"FatihsMG4-unstable-2.2.7.apk"));
hash + " *NilsMG4-unstable-2.2.7.apk\n",
"NilsMG4-unstable-2.2.7.apk"));
assertNull(OtaUpdater.parseExpectedSha256(hash + " another.apk",
"FatihsMG4-unstable-2.2.7.apk"));
"NilsMG4-unstable-2.2.7.apk"));
assertNull(OtaUpdater.parseExpectedSha256("not-a-hash",
"FatihsMG4-unstable-2.2.7.apk"));
"NilsMG4-unstable-2.2.7.apk"));
}
@Test
@@ -150,14 +150,14 @@ public class OtaUpdaterTest {
.put(release(true, asset("2.2.5")));
OtaUpdater.Update update = OtaUpdater.selectUpdate(releases, "2.2.2.0-unstable");
assertEquals("2.2.6", update.versionName);
assertTrue(update.apkUrl.endsWith("FatihsMG4-unstable-2.2.6.apk"));
assertTrue(update.apkUrl.endsWith("NilsMG4-unstable-2.2.6.apk"));
}
@Test
public void stableDraftForeignAndUnversionedAssetsCannotWin() throws Exception {
JSONObject foreign = asset("9.0.0").put("browser_download_url", "https://example.com/app.apk");
JSONObject unversioned = asset("9.0.0").put("name", "FatihsMG4-unstable.apk");
JSONObject stableName = asset("9.0.0").put("name", "FatihsMG4-stable-9.0.0.apk");
JSONObject unversioned = asset("9.0.0").put("name", "NilsMG4-unstable.apk");
JSONObject stableName = asset("9.0.0").put("name", "NilsMG4-stable-9.0.0.apk");
JSONArray releases = new JSONArray()
.put(release(false, asset("9.0.0")))
.put(release(true, asset("9.0.0")).put("draft", true))
@@ -216,7 +216,7 @@ public class OtaUpdaterTest {
}
private static JSONObject asset(String version) throws Exception {
String name = "FatihsMG4-unstable-" + version + ".apk";
String name = "NilsMG4-unstable-" + version + ".apk";
return new JSONObject().put("name", name).put("browser_download_url",
"https://github.com/fatihdonmezdev/FatihsMG4/releases/download/unstable/" + name);
}
+1 -1
View File
@@ -27,7 +27,7 @@
android:name=".OtaInstallResultReceiver"
android:exported="false">
<intent-filter>
<action android:name="com.evsuite.abrp.OTA_INSTALL_RESULT" />
<action android:name="com.stinnesbeck.nilsmg4.OTA_INSTALL_RESULT" />
</intent-filter>
</receiver>
</application>
@@ -19,7 +19,7 @@ import android.widget.Toast;
*/
public final class OtaInstallResultReceiver extends BroadcastReceiver {
static final String ACTION_INSTALL_RESULT = "com.evsuite.abrp.OTA_INSTALL_RESULT";
static final String ACTION_INSTALL_RESULT = "com.stinnesbeck.nilsmg4.OTA_INSTALL_RESULT";
private static final String TAG = "EVABRP.Update";
@@ -53,7 +53,7 @@ import java.security.MessageDigest;
final class OtaUpdater {
private static final String TAG = "OtaUpdater";
private static final String CACHE_PREFIX = "FatihsMG4-ota-";
private static final String CACHE_PREFIX = "NilsMG4-ota-";
/**
* Pre-releases live here; the unstable channel tracks them.
@@ -167,7 +167,7 @@ final class OtaUpdater {
/**
* Version carried by an unstable asset name:
* "FatihsMG4-unstable-1.0.42.apk" -> "1.0.42".
* "NilsMG4-unstable-1.0.42.apk" -> "1.0.42".
*
* The release tag is the fixed string "unstable" (one rolling pre-release), so the asset
* name is what identifies a build. Returns null when the name carries no version.
@@ -203,7 +203,7 @@ final class OtaUpdater {
try {
conn.setInstanceFollowRedirects(false);
conn.setRequestProperty("Accept", "application/vnd.github.v3+json");
conn.setRequestProperty("User-Agent", "FatihsMG4-Android");
conn.setRequestProperty("User-Agent", "NilsMG4-Android");
conn.setConnectTimeout(TIMEOUT_MS);
conn.setReadTimeout(TIMEOUT_MS);
int status = conn.getResponseCode();
@@ -302,7 +302,7 @@ final class OtaUpdater {
connection.setInstanceFollowRedirects(false);
connection.setConnectTimeout(TIMEOUT_MS);
connection.setReadTimeout(TIMEOUT_MS);
connection.setRequestProperty("User-Agent", "FatihsMG4-Android");
connection.setRequestProperty("User-Agent", "NilsMG4-Android");
try {
int status = connection.getResponseCode();
if (status >= 300 && status <= 399) {
@@ -584,7 +584,7 @@ final class OtaUpdater {
if (downloads == null || (!downloads.isDirectory() && !downloads.mkdirs())) {
throw new IllegalStateException("Downloads directory unavailable");
}
File destination = new File(downloads, "FatihsMG4-OTA-update.apk");
File destination = new File(downloads, "NilsMG4-OTA-update.apk");
try (FileInputStream input = new FileInputStream(source);
FileOutputStream output = new FileOutputStream(destination, false)) {
byte[] buffer = new byte[64 * 1024];
+5 -5
View File
@@ -1,5 +1,5 @@
{
description = "FatihsMG4 — Android Automotive build environment";
description = "NilsMG4 — Android Automotive build environment";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
@@ -57,7 +57,7 @@
root=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
cd "$root"
if [ ! -f app/build.gradle ]; then
echo "build-car: run this from the FatihsMG4 checkout" >&2
echo "build-car: run this from the NilsMG4 checkout" >&2
exit 1
fi
@@ -81,10 +81,10 @@
./gradlew assembleUnstableDebug "$@"
version=$(./gradlew -q printUnstableVersion "$@" | tail -1)
in=app/build/outputs/apk/unstable/debug/app-unstable-debug.apk
in=app/build/outputs/apk/unstable/debug/NilsMG4-unstable-debug.apk
mkdir -p build/car
aligned=build/car/aligned.apk
out="build/car/FatihsMG4-unstable-$version.apk"
out="build/car/NilsMG4-unstable-$version.apk"
"${buildTools}/zipalign" -f -p 4 "$in" "$aligned"
# Options come before the input APK; apksigner rejects the other order.
@@ -122,7 +122,7 @@
shellHook = ''
echo "sdk.dir=$ANDROID_HOME" > local.properties
echo "FatihsMG4 dev shell — JDK $(java -version 2>&1 | head -1 | cut -d'"' -f2), SDK at $ANDROID_HOME"
echo "NilsMG4 dev shell — JDK $(java -version 2>&1 | head -1 | cut -d'"' -f2), SDK at $ANDROID_HOME"
echo " ./gradlew assembleStableDebug # debug APK"
echo " ./gradlew assembleUnstableDebug # build that can update itself (OTA)"
echo " build-car # unstable build signed for the car"
+1 -1
View File
@@ -22,7 +22,7 @@ dependencyResolutionManagement {
}
}
rootProject.name = "FatihsMG4"
rootProject.name = "NilsMG4"
include ':app'
include ':evhardware'
project(':evhardware').projectDir = file('EVHardware/lib')