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)"