diff --git a/SPECS.md b/SPECS.md
index 7da3875..ac42c20 100644
--- a/SPECS.md
+++ b/SPECS.md
@@ -112,9 +112,11 @@ tyre pressures, GPS (lat, lon, elevation, heading) and the is_* flags. It does *
INGEST_TOKEN) into git and pushed it. Commit amended and force-pushed; both values regenerated in
the now gitignored `k8s/secrets.yaml` (since moved to `k8s/secrets/secrets.yaml`, outside
`kubectl apply -f k8s/`). Filled-in secrets live only there from now on.
+- 2026-10-18 — App 2.17.2: Wi-Fi auto-connect removed with its two permissions (ACCESS_ and
+ CHANGE_WIFI_STATE). Upstream used it to keep the head unit on a phone hotspot; this car's head
+ unit has its own connection.
- 2026-10-18 — App 2.17.1: Settings without the Wi-Fi and saved-networks cards, in three columns
- (display and steering wheel | battery | charging price and app). The background Wi-Fi
- auto-connect stays on, now without a switch. Logs keeps only the upload log: the steering-key
+ (display and steering wheel | battery | charging price and app). Logs keeps only the upload log: the steering-key
reports, the car-colour probe and the screen line are gone, their questions answered.
- 2026-10-18 — App 2.17.0: "Speed, 5-min average" card under Speed. The top bar is one compact line
(title and subtitle); version, update check and About moved to an App card on the Settings tab.
diff --git a/app/build.gradle b/app/build.gradle
index b6db3a4..fa4b9b8 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -34,8 +34,8 @@ android {
applicationId "com.stinnesbeck.nilsmg4"
minSdk 27
targetSdk 36
- versionCode 90
- versionName "2.17.1"
+ versionCode 91
+ versionName "2.17.2"
// Nothing about the server ships in the APK: its URL and ingest token are entered in
// the app's Server tab (ServerConfig), and the database is only reachable by the server.
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 36b8413..68f3033 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -14,12 +14,6 @@
-
-
-
diff --git a/app/src/main/java/com/evsuite/abrp/AbrpUploadService.java b/app/src/main/java/com/evsuite/abrp/AbrpUploadService.java
index 63892dc..78da357 100644
--- a/app/src/main/java/com/evsuite/abrp/AbrpUploadService.java
+++ b/app/src/main/java/com/evsuite/abrp/AbrpUploadService.java
@@ -68,7 +68,6 @@ public class AbrpUploadService extends Service {
private ChargeSessionTracker chargeSessionTracker;
private TelemetryUploader telemetryUploader;
private SteeringKeyListener steeringKeys;
- private WifiAutoConnect wifi;
private ScheduledExecutorService scheduler;
private SharedPreferences prefs;
@@ -97,7 +96,6 @@ public class AbrpUploadService extends Service {
consumptionTracker = ConsumptionTracker.get(this);
consumptionCloudClient = new ConsumptionCloudClient(getApplicationContext());
chargeSessionTracker = ChargeSessionTracker.get(getApplicationContext());
- wifi = new WifiAutoConnect(getApplicationContext());
telemetryUploader = new TelemetryUploader(getApplicationContext());
steeringKeys = new SteeringKeyListener(this, prefs);
steeringKeys.register();
@@ -169,12 +167,6 @@ public class AbrpUploadService extends Service {
chargeSessionTracker.sample(vehicle);
consumptionCloudClient.syncIfDue(consumptionTracker, chargeSessionTracker, sampleMs);
- // Keep the head unit on the phone hotspot — the sync needs a network, and this is
- // exactly when being offline costs something. Rate-limited internally.
- if (wifi != null && prefs.getBoolean(MainActivity.WIFI_AUTO_KEY, true)) {
- wifi.ensureConnected(false);
- }
-
UploadSettings settings = UploadSettings.from(prefs);
// Live data goes to the server, to ABRP directly, or both; with neither set up there is
// nowhere to send it.
diff --git a/app/src/main/java/com/evsuite/abrp/MainActivity.java b/app/src/main/java/com/evsuite/abrp/MainActivity.java
index a532bb6..00efca0 100644
--- a/app/src/main/java/com/evsuite/abrp/MainActivity.java
+++ b/app/src/main/java/com/evsuite/abrp/MainActivity.java
@@ -35,12 +35,6 @@ import com.evsuite.hardware.telemetry.EnergyTelemetryReader;
public class MainActivity extends AppCompatActivity {
- /**
- * Rejoin a saved network (the phone hotspot) in the background; read by
- * {@link AbrpUploadService} on every tick. On by default; the Wi-Fi card with its switch was
- * removed from Settings (2.17.1), so it now stays on.
- */
- static final String WIFI_AUTO_KEY = "wifi_auto_connect";
private static final String REPOSITORY_URL = "https://git.stinnesbeck.com/nils/mg4";
/** Navigation destinations in page order. Parallel to {@link #panes}. */
diff --git a/app/src/main/java/com/evsuite/abrp/WifiAutoConnect.java b/app/src/main/java/com/evsuite/abrp/WifiAutoConnect.java
deleted file mode 100644
index 2661d36..0000000
--- a/app/src/main/java/com/evsuite/abrp/WifiAutoConnect.java
+++ /dev/null
@@ -1,135 +0,0 @@
-package com.evsuite.abrp;
-
-import android.content.Context;
-import android.net.wifi.WifiConfiguration;
-import android.net.wifi.WifiInfo;
-import android.net.wifi.WifiManager;
-import android.util.Log;
-
-import java.util.List;
-
-/**
- * Brings Wi-Fi up and joins a network the head unit already knows.
- *
- * The point is the phone hotspot: the car has the credentials saved, but the driver still
- * has to walk into Settings and tap the network every time. This does that tap.
- *
- * Why this works here and would not on a newer phone. {@code getConfiguredNetworks}
- * and {@code enableNetwork} were deprecated in API 29 and, from that release, return an
- * empty list and refuse the call for ordinary apps — Android handed network choice to the
- * user through a system picker. This head unit runs Android 9 (API 28), where both are
- * still honoured, and the build is platform-signed, so it is the same UID family the
- * Settings app itself uses. Neither fact is portable: on a Pixel this class would find
- * nothing and change nothing. It is deliberately written to fail quietly if so.
- *
- * Nothing here creates or edits a network. It only enables one the unit was already
- * configured with, which is why no password ever passes through this app.
- */
-final class WifiAutoConnect {
-
- private static final String TAG = "EVABRP.Wifi";
-
- /** Re-checking faster than this achieves nothing: a join takes seconds to settle. */
- private static final long MIN_ATTEMPT_INTERVAL_MS = 30_000L;
-
- private final WifiManager wifi;
- private long lastAttemptMs;
-
- WifiAutoConnect(Context context) {
- this.wifi = (WifiManager) context.getApplicationContext()
- .getSystemService(Context.WIFI_SERVICE);
- }
-
- /** The SSID the unit is associated with, or null when it is not on Wi-Fi. */
- String currentSsid() {
- try {
- if (wifi == null || !wifi.isWifiEnabled()) return null;
- WifiInfo info = wifi.getConnectionInfo();
- if (info == null) return null;
- String ssid = info.getSSID();
- if (ssid == null) return null;
- // The framework quotes the SSID and reports this placeholder when it will not
- // say — usually because location is off. Neither is a network name.
- ssid = ssid.replace("\"", "").trim();
- if (ssid.isEmpty() || "".equals(ssid) || "0x".equals(ssid)) return null;
- return ssid;
- } catch (Throwable t) {
- return null;
- }
- }
-
- /**
- * One attempt to get onto a saved network, rate-limited.
- *
- * @param force ignore the rate limit — for the button, which a driver only presses
- * because the automatic attempt has not worked yet.
- * @return what happened, for the log and the UI. Never null.
- */
- Result ensureConnected(boolean force) {
- if (wifi == null) return Result.UNSUPPORTED;
-
- String ssid = currentSsid();
- if (ssid != null) return Result.alreadyOn(ssid);
-
- long now = System.currentTimeMillis();
- if (!force && now - lastAttemptMs < MIN_ATTEMPT_INTERVAL_MS) return Result.WAITING;
- lastAttemptMs = now;
-
- try {
- if (!wifi.isWifiEnabled()) {
- // Also gone for ordinary apps from API 29, and also still honoured here.
- if (!wifi.setWifiEnabled(true)) return Result.CANNOT_ENABLE;
- // The radio needs a moment before it will associate; the next tick retries.
- return Result.ENABLING;
- }
-
- List saved = wifi.getConfiguredNetworks();
- if (saved == null || saved.isEmpty()) return Result.NO_SAVED_NETWORKS;
-
- // A scan would let us prefer a network that is actually in range, but the results
- // are themselves throttled and location-gated. Asking the supplicant to pick from
- // what it knows is both simpler and what the Settings screen effectively does.
- wifi.reconnect();
- for (WifiConfiguration c : saved) {
- if (c == null) continue;
- // Second argument true: disable the others for this attempt, so the
- // supplicant commits to this one rather than drifting between them.
- if (wifi.enableNetwork(c.networkId, true)) {
- String name = c.SSID == null ? "?" : c.SSID.replace("\"", "").trim();
- Log.i(TAG, "enableNetwork accepted for " + name);
- return Result.attempting(name);
- }
- }
- return Result.REFUSED;
- } catch (SecurityException e) {
- Log.w(TAG, "Wi-Fi connection refused: permission denied");
- return Result.REFUSED;
- } catch (Throwable t) {
- Log.w(TAG, "connect attempt failed: " + t.getMessage());
- return Result.REFUSED;
- }
- }
-
- /** Outcome of one attempt. {@link #connected} is the only one that means we are online. */
- static final class Result {
- final boolean connected;
- final String ssid;
- final String message;
-
- private Result(boolean connected, String ssid, String message) {
- this.connected = connected;
- this.ssid = ssid;
- this.message = message;
- }
-
- static Result alreadyOn(String ssid) { return new Result(true, ssid, "Connected: " + ssid); }
- static Result attempting(String ssid) { return new Result(false, ssid, "Connecting: " + ssid); }
-
- static final Result UNSUPPORTED = new Result(false, null, "Wi-Fi cannot be managed");
- static final Result WAITING = new Result(false, null, "Waiting…");
- static final Result ENABLING = new Result(false, null, "Turning Wi-Fi on…");
- static final Result CANNOT_ENABLE = new Result(false, null, "Could not turn Wi-Fi on");
- static final Result NO_SAVED_NETWORKS = new Result(false, null, "No saved networks");
- static final Result REFUSED = new Result(false, null, "The system refused");
- }
-}
diff --git a/mise.toml b/mise.toml
index b9e8889..9e312ff 100644
--- a/mise.toml
+++ b/mise.toml
@@ -119,7 +119,7 @@ run = './gradlew -Dorg.gradle.java.home=$JAVA_HOME installStableDebug'
[tasks.logs]
description = "Follows the app's logs on the car"
-run = "adb logcat -s AbrpUploadService:V CarPropertyAdapter:V VhalProbe:V SecurePrefs:V OtaUpdater:V UpdateHook:V NilsMG4.Telemetry:V NilsMG4.Mongo:V NilsMG4.SteeringKey:V EVABRP.Boot:V EVABRP.Wifi:V"
+run = "adb logcat -s AbrpUploadService:V CarPropertyAdapter:V VhalProbe:V SecurePrefs:V OtaUpdater:V UpdateHook:V NilsMG4.Telemetry:V NilsMG4.Mongo:V NilsMG4.SteeringKey:V EVABRP.Boot:V"
[tasks.release]
description = "Release APKs of both channels (stable without OTA, unstable with OTA)"