App 2.43.5: update checks log what happens

Each check writes down the networks Android sees, the server's name lookup,
how a failure happened and how many reports wait, and on a failure whether
the internet answers at all, to tell a SIM without data from a server that is
down. The report goes to a USB stick and, once online, the server. No address
or token is written. Tried on the emulator. Not tried on the car.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6
This commit is contained in:
2026-11-01 10:21:15 +01:00
co-authored by Claude Opus 5.5
parent 6b55bec419
commit e52d7f01fc
14 changed files with 301 additions and 15 deletions
+9 -1
View File
@@ -79,9 +79,17 @@ analyser's peak, bass, mid, treble and rms, and a result line. Levels only, no s
to move for music to count as heard. Ask what was playing when the check ran, and whether the to move for music to count as heard. Ask what was playing when the check ran, and whether the
music kept playing and any prompt came up. music kept playing and any prompt came up.
## Update checks
`fetch-logs` also brings `update-check-YYYYMMDD-HHMMSS.txt` (each Settings → Check for updates,
`UpdateHook`, 2.43.5 on): the networks Android sees, whether the server's name resolves, the
result and, on a failure, whether `www.gstatic.com/generate_204` answers (204: the internet
works, so look at the server). A report written offline arrives once the car is back online, or
is on a USB stick that was plugged in.
## Rules ## Rules
- Recordings and checks are the car's data: `logs/`, `/signal-recording-*.txt` and - Recordings and checks are the car's data: `logs/`, `/signal-recording-*.txt` and
`/media-probe-*.txt`, `/sound-probe-*.txt` are gitignored. Never `/media-probe-*.txt`, `/sound-probe-*.txt`, `/update-check-*.txt` are gitignored. Never
commit, publish or upload them anywhere. commit, publish or upload them anywhere.
- Findings that change what the project knows go into a `SPECS.md` done-log entry, as usual. - Findings that change what the project knows go into a `SPECS.md` done-log entry, as usual.
+1
View File
@@ -47,3 +47,4 @@ mise.local.toml
/signal-recording-*.txt /signal-recording-*.txt
/media-probe-*.txt /media-probe-*.txt
/sound-probe-*.txt /sound-probe-*.txt
/update-check-*.txt
+1 -1
View File
@@ -82,7 +82,7 @@ Repository: `ssh://git@git.stinnesbeck.com:2222/nils/mg4.git` (remote `origin`),
| Live data | on/off, ABRP API key and user token, upload frequency | | Live data | on/off, ABRP API key and user token, upload frequency |
| Server | URL, pairing by QR, ingest token, installation ID (read-only), config import | | Server | URL, pairing by QR, ingest token, installation ID (read-only), config import |
| Settings | drive mode (Eco, Normal, Sport, Snow, Custom; the lit one is what the car reports; changes only in P at a standstill, `DriveModeWrite`; "Set at start" sets the last confirmed choice again when the service starts with the head unit), full screen, voice key, Overview style with the accent colour beside it (offered only for styles drawn in it, `OverviewStyle.usesAccent`; Retro, Terminal, Grid (and Grid with map, sharing Grid's choice) and Visor add "Original" for their own palette, `hasOriginal`), tyre unit, battery and charging price (one card on wide screens), version, update check, install from USB, About | | Settings | drive mode (Eco, Normal, Sport, Snow, Custom; the lit one is what the car reports; changes only in P at a standstill, `DriveModeWrite`; "Set at start" sets the last confirmed choice again when the service starts with the head unit), full screen, voice key, Overview style with the accent colour beside it (offered only for styles drawn in it, `OverviewStyle.usesAccent`; Retro, Terminal, Grid (and Grid with map, sharing Grid's choice) and Visor add "Original" for their own palette, `hasOriginal`), tyre unit, battery and charging price (one card on wide screens), version, update check, install from USB, About |
| Logs | signal recorder (`SignalRecorder`/`SignalRecording`: every property and vendor getter via `VendorSurfaceProbe.capture`, again and again for 3 minutes, plus change events of the driver-assistance block it subscribes to, `EVHardware.listenToProperties` with `AssistBlock`, and steering-wheel key broadcasts; lists what changed, rarest first; saved to the app, a USB stick and the server, through `LogOutbox`, which sends it again later when the car is offline; how to find where the car publishes something), music-sources check (`MediaProbe`: audio players and the active media sessions, CarPlay's and Bluetooth's among them, with their song, and the media browser services by name — never connected to: binding them stopped the music in 2.42.0; saved and sent like a recording), sound check (`SoundProbe`, `SoundLevels`: on a tap, 10 seconds of levels from Android's audio analyser, to learn whether it hears what plays; saved and sent like a recording), update log (every install step, with the platform's words), upload log | | Logs | signal recorder (`SignalRecorder`/`SignalRecording`: every property and vendor getter via `VendorSurfaceProbe.capture`, again and again for 3 minutes, plus change events of the driver-assistance block it subscribes to, `EVHardware.listenToProperties` with `AssistBlock`, and steering-wheel key broadcasts; lists what changed, rarest first; saved to the app, a USB stick and the server, through `LogOutbox`, which sends it again later when the car is offline; how to find where the car publishes something), music-sources check (`MediaProbe`: audio players and the active media sessions, CarPlay's and Bluetooth's among them, with their song, and the media browser services by name — never connected to: binding them stopped the music in 2.42.0; saved and sent like a recording), sound check (`SoundProbe`, `SoundLevels`: on a tap, 10 seconds of levels from Android's audio analyser, to learn whether it hears what plays; saved and sent like a recording), update log (every check and install step, with the platform's words; a check also writes down the networks, the server's name lookup and, when it fails, an internet test, `UpdateNetworkCheck`, `UpdateDiagnosis`, and saves a report like a recording), upload log |
Key classes (`app/src/main/java/com/evsuite/abrp/`): `AbrpUploadService` (foreground service, Key classes (`app/src/main/java/com/evsuite/abrp/`): `AbrpUploadService` (foreground service,
15 s tick), `TelemetryUploader` (+ `TelemetryQueue`, persisted), `TelemetryPayload` (server 15 s tick), `TelemetryUploader` (+ `TelemetryQueue`, persisted), `TelemetryPayload` (server
+5 -1
View File
@@ -89,7 +89,11 @@ platform's own words, is on the Logs tab under "Updates".
| `Installation failed: INSTALL_FAILED_… The APK is in Downloads.` | The package manager's own words. A signature error means the APK was not signed by `build-car`. | | `Installation failed: INSTALL_FAILED_… The APK is in Downloads.` | The package manager's own words. A signature error means the APK was not signed by `build-car`. |
| `… is in Downloads — tap the file to install` | A session could not start; install from Downloads by hand. | | `… is in Downloads — tap the file to install` | A session could not start; install from Downloads by hand. |
On the car's Logs tab, or with a cable: `adb logcat -s EVABRP.Update OtaUpdater`. On the car's Logs tab, or with a cable: `adb logcat -s EVABRP.Update OtaUpdater`. Each check
writes down the networks Android sees, whether the server's name resolves, its result and, when
it fails, whether `www.gstatic.com/generate_204` answers, which tells a SIM without data from a
server that is down. That report also goes to a USB stick and, once online, to the server
(`update-check-*.txt`, read with `fetch-logs`). No address or token is written.
## Why a PackageInstaller session, not `pm install` ## Why a PackageInstaller session, not `pm install`
+1
View File
@@ -140,6 +140,7 @@ tyre pressures, GPS (lat, lon, elevation, heading) and the is_* flags. It does *
confirmed car facts, app structure, UI conventions, build/test/ship, documents. It replaces confirmed car facts, app structure, UI conventions, build/test/ship, documents. It replaces
upstream's EVABRPUploader text. The test emulator is now in the flake: `nix run .#car-emulator` upstream's EVABRPUploader text. The test emulator is now in the flake: `nix run .#car-emulator`
(Android 9 AOSP at 1920×720, 160 dpi; the car build installs as the system user — checked). (Android 9 AOSP at 1920×720, 160 dpi; the car build installs as the system user — checked).
- 2026-11-01 — App 2.43.5: **update checks log what happens** (`UpdateNetworkCheck`, `UpdateDiagnosis`; owner: updating from the car fails, "Can we log what happens? Maybe my SIM card has no data left"). Each check on Settings writes to the Logs tab: the networks Android sees (type, in use, internet, validated, roaming), whether the server's name resolves, how a failure happened, in words from its type or status only, and how many reports wait. On a failure it also asks `https://www.gstatic.com/generate_204`, the one request to anyone but the car server, to tell no data from a server that is down. A report goes to a USB stick and, when online, the server (`update-check-*.txt`; only the newest waits, so it cannot push out a recording). A check at start logs only a failure. The update log keeps 40 lines, not 10. An out-of-data SIM would also explain why the Bluetooth and CarPlay sound reports did not arrive; not confirmed. Tested: unit tests, lint, emulator with a fake server (online: the name fails, the internet test says it works; airplane mode: both fail, "none in use"; the real build installs and starts, its check not run there, having no server). Not tried on the car.
- 2026-11-01 — App 2.43.4 published (`car-2.43.4`). - 2026-11-01 — App 2.43.4 published (`car-2.43.4`).
- 2026-11-01 — App 2.43.4: **the Halloween pumpkin's light follows the music** (`MusicTap`, `MusicPulse`; owner: "make the light in the pumpkin react to the equalizer/sound of the music"). The bass, 25 times a second off the main thread, drives the glow and brightens the carving on the beat; yellow and green still mean drawing and regenerating, the embers still the power, and no signal leaves the light as it was. It listens only with the app in front, on the Overview, in Halloween. The owner reports the sound check worked with Bluetooth and CarPlay; those two reports never reached the server (why not is unexplained), so their levels are unseen and the pulse's feel is untuned for the car. Tested: unit tests, lint, emulator (a tone already playing when the analyser starts: the carving and glow move with it; no sound: unchanged; the analyser is released on another tab and when the app leaves the front; no crash). Not tried on the car; how it looks there, and its effect on the head unit over a long drive, are unknown. - 2026-11-01 — App 2.43.4: **the Halloween pumpkin's light follows the music** (`MusicTap`, `MusicPulse`; owner: "make the light in the pumpkin react to the equalizer/sound of the music"). The bass, 25 times a second off the main thread, drives the glow and brightens the carving on the beat; yellow and green still mean drawing and regenerating, the embers still the power, and no signal leaves the light as it was. It listens only with the app in front, on the Overview, in Halloween. The owner reports the sound check worked with Bluetooth and CarPlay; those two reports never reached the server (why not is unexplained), so their levels are unseen and the pulse's feel is untuned for the car. Tested: unit tests, lint, emulator (a tone already playing when the analyser starts: the carving and glow move with it; no sound: unchanged; the analyser is released on another tab and when the app leaves the front; no crash). Not tried on the car; how it looks there, and its effect on the head unit over a long drive, are unknown.
- 2026-10-31 — Sound check on the car (2.43.3), radio: `com.saicmotor.radio` playing, `isMusicActive` false, analyser enabled without error and silent for all 10 seconds (peak 0.00, -9600 mB). The audio analyser does not hear the radio. The owner ran Bluetooth and CarPlay too; those reports have not reached the server yet (the outbox sends oldest first and stops at the first failure, retrying while the service runs). - 2026-10-31 — Sound check on the car (2.43.3), radio: `com.saicmotor.radio` playing, `isMusicActive` false, analyser enabled without error and silent for all 10 seconds (peak 0.00, -9600 mB). The audio analyser does not hear the radio. The owner ran Bluetooth and CarPlay too; those reports have not reached the server yet (the outbox sends oldest first and stops at the first failure, retrying while the service runs).
+2 -2
View File
@@ -34,8 +34,8 @@ android {
applicationId "com.stinnesbeck.nilsmg4" applicationId "com.stinnesbeck.nilsmg4"
minSdk 27 minSdk 27
targetSdk 36 targetSdk 36
versionCode 157 versionCode 158
versionName "2.43.4" versionName "2.43.5"
// Nothing about the server ships in the APK: its URL and ingest token are entered in // 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. // the app's Server tab (ServerConfig), and the database is only reachable by the server.
@@ -85,6 +85,12 @@ final class LogOutbox {
return true; return true;
} }
/** Removes what waits under names starting with [prefix]. */
static synchronized void dropWaiting(Context context, String prefix) {
File dir = new File(context.getFilesDir(), DIR);
for (String name : waiting(dir)) if (name.startsWith(prefix)) new File(dir, name).delete();
}
/** Sends what waits, unless the last attempt failed less than {@link #RETRY_MS} ago. Blocking: /** Sends what waits, unless the last attempt failed less than {@link #RETRY_MS} ago. Blocking:
* call it off the main thread. */ * call it off the main thread. */
static void sendIfDue(Context context) { static void sendIfDue(Context context) {
@@ -0,0 +1,69 @@
package com.evsuite.abrp;
import java.io.IOException;
import java.net.ConnectException;
import java.net.NoRouteToHostException;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.net.ssl.SSLException;
/**
* Why an update check failed, in words for the update log, and what the car's networks look
* like. Built from an error's type and an HTTP status only, because an error's own text can carry
* the server's address: nothing here holds a URL or a credential. Android-free and unit-tested.
*/
final class UpdateDiagnosis {
private static final Pattern STATUS = Pattern.compile("^Release API returned (\\d{3})$");
private UpdateDiagnosis() {}
/** One line about a failed step. */
static String failure(Throwable t) {
if (t == null) return "unknown failure";
String type = " (" + t.getClass().getSimpleName() + ")";
if (t instanceof UnknownHostException) return "the name could not be looked up: no data connection, or no name service" + type;
if (t instanceof SocketTimeoutException) return "no answer in time: a very slow or blocked connection" + type;
if (t instanceof ConnectException || t instanceof NoRouteToHostException) return "no connection could be made" + type;
if (t instanceof SSLException) return "the secure connection failed: something in between may have answered, or the head unit's clock is wrong" + type;
if (t instanceof SocketException) return "the connection broke off" + type;
if (t instanceof IOException && t.getMessage() != null) {
Matcher status = STATUS.matcher(t.getMessage());
if (status.matches()) return status(Integer.parseInt(status.group(1)));
if (t.getMessage().equals("Invalid release response")) return "the answer was not a list of releases";
}
return "failed" + type;
}
/** What a status from the car server means here. */
static String status(int code) {
String what;
if (code >= 300 && code < 400) what = "something in between redirected it, as a SIM without data may";
else if (code == 401 || code == 403) what = "the server refuses the car's token";
else if (code == 404) what = "the server does not know this address";
else if (code >= 500) what = "the server or the forge behind it has a problem";
else what = "not what was expected";
return "the server answered HTTP " + code + ": " + what;
}
/** The outside-world test: a 204 is the internet working; anything else stands in between. */
static String internet(int code) {
if (code == 204) return "the internet works (HTTP 204)";
return "something in between answered HTTP " + code + " instead, as a SIM without data may";
}
/** One network as Android sees it. */
static String network(List<String> transports, boolean internet, boolean validated,
boolean active, boolean roaming) {
return (transports.isEmpty() ? "unknown type" : String.join("+", transports))
+ (active ? ", in use" : "")
+ (internet ? ", offers internet" : ", no internet")
+ (validated ? ", confirmed by Android" : ", not confirmed by Android")
+ (roaming ? ", roaming" : "");
}
}
@@ -21,7 +21,7 @@ import java.util.Locale;
*/ */
final class UpdateLog { final class UpdateLog {
static final int MAX_LINES = 10; static final int MAX_LINES = 40;
private static final String PREFS = "update_log"; private static final String PREFS = "update_log";
private static final String KEY = "lines"; private static final String KEY = "lines";
private static final String KEY_VERSION = "version"; private static final String KEY_VERSION = "version";
@@ -0,0 +1,67 @@
package com.evsuite.abrp;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
import java.io.IOException;
import java.net.ConnectException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.Collections;
import javax.net.ssl.SSLHandshakeException;
public class UpdateDiagnosisTest {
@Test
public void errorsAreToldApart() {
assertTrue(UpdateDiagnosis.failure(new UnknownHostException()).contains("no data connection"));
assertTrue(UpdateDiagnosis.failure(new SocketTimeoutException()).contains("in time"));
assertTrue(UpdateDiagnosis.failure(new ConnectException()).contains("no connection"));
assertTrue(UpdateDiagnosis.failure(new SSLHandshakeException("x")).contains("in between"));
assertTrue(UpdateDiagnosis.failure(new IOException("Release API returned 401")).contains("token"));
assertTrue(UpdateDiagnosis.failure(new IOException("Invalid release response")).contains("not a list"));
assertEquals("unknown failure", UpdateDiagnosis.failure(null));
}
@Test
public void anErrorsOwnTextNeverReachesTheLog() {
String secret = "https://car.example.com/v1/app/releases?token=abc";
for (Throwable t : Arrays.asList(new UnknownHostException(secret), new SocketTimeoutException(secret),
new ConnectException(secret), new SSLHandshakeException(secret), new IOException(secret),
new RuntimeException(secret))) {
String line = UpdateDiagnosis.failure(t);
assertFalse(line, line.contains("example"));
assertFalse(line, line.contains("abc"));
}
}
@Test
public void statusesSayWhoIsAtFault() {
assertTrue(UpdateDiagnosis.status(302).contains("SIM without data"));
assertTrue(UpdateDiagnosis.status(401).contains("token"));
assertTrue(UpdateDiagnosis.status(403).contains("token"));
assertTrue(UpdateDiagnosis.status(404).contains("does not know"));
assertTrue(UpdateDiagnosis.status(502).contains("forge"));
assertTrue(UpdateDiagnosis.status(418).contains("HTTP 418"));
}
@Test
public void onlyA204IsTheInternetWorking() {
assertTrue(UpdateDiagnosis.internet(204).startsWith("the internet works"));
assertTrue(UpdateDiagnosis.internet(200).contains("in between"));
assertTrue(UpdateDiagnosis.internet(302).contains("SIM without data"));
}
@Test
public void aNetworkInOneLine() {
assertEquals("cellular, in use, offers internet, not confirmed by Android, roaming",
UpdateDiagnosis.network(Collections.singletonList("cellular"), true, false, true, true));
assertEquals("unknown type, no internet, confirmed by Android",
UpdateDiagnosis.network(Collections.emptyList(), false, true, false, false));
}
}
+2
View File
@@ -18,6 +18,8 @@
android:name="android.permission.MEDIA_CONTENT_CONTROL" android:name="android.permission.MEDIA_CONTENT_CONTROL"
tools:ignore="ProtectedPermissions" /> tools:ignore="ProtectedPermissions" />
<!-- An update check writes down which networks Android sees (UpdateNetworkCheck). -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- The Logs tab's sound check (SoundProbe): Android's audio analyser asks for both. The <!-- The Logs tab's sound check (SoundProbe): Android's audio analyser asks for both. The
system user holds them without a prompt; the analyser reports levels only, no sound. --> system user holds them without a prompt; the analyser reports levels only, no sound. -->
<uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.RECORD_AUDIO" />
@@ -55,6 +55,9 @@ final class OtaUpdater {
private static final String TAG = "OtaUpdater"; private static final String TAG = "OtaUpdater";
private static final String CACHE_PREFIX = "NilsMG4-ota-"; private static final String CACHE_PREFIX = "NilsMG4-ota-";
/** Why the last download failed, for the update log. */
static volatile String downloadProblem = "unknown";
/** The release list on the car server, for the server configured in the app. */ /** The release list on the car server, for the server configured in the app. */
static String releasesUrl(ServerConfig config) { static String releasesUrl(ServerConfig config) {
return config.url + "/v1/app/releases"; return config.url + "/v1/app/releases";
@@ -366,6 +369,7 @@ final class OtaUpdater {
connection.setRequestProperty("User-Agent", "NilsMG4-Android"); connection.setRequestProperty("User-Agent", "NilsMG4-Android");
if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) { if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
Log.w(TAG, "Update download returned " + connection.getResponseCode()); Log.w(TAG, "Update download returned " + connection.getResponseCode());
downloadProblem = UpdateDiagnosis.status(connection.getResponseCode());
return null; return null;
} }
long total = connection.getContentLengthLong(); long total = connection.getContentLengthLong();
@@ -390,10 +394,14 @@ final class OtaUpdater {
out.getFD().sync(); out.getFD().sync();
} }
ok = hex(digest.digest()).equalsIgnoreCase(expectedSha256); ok = hex(digest.digest()).equalsIgnoreCase(expectedSha256);
if (!ok) Log.w(TAG, "Update APK does not match its SHA-256"); if (!ok) {
Log.w(TAG, "Update APK does not match its SHA-256");
downloadProblem = "it does not match its SHA-256";
}
return ok ? target : null; return ok ? target : null;
} catch (Exception e) { } catch (Exception e) {
Log.w(TAG, "Update download failed: " + e.getClass().getSimpleName()); Log.w(TAG, "Update download failed: " + e.getClass().getSimpleName());
downloadProblem = UpdateDiagnosis.failure(e);
return null; return null;
} finally { } finally {
if (connection != null) connection.disconnect(); if (connection != null) connection.disconnect();
@@ -5,6 +5,11 @@ import android.util.Log;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
/** /**
@@ -58,7 +63,8 @@ final class UpdateHook {
"Update available: " + update.versionName, "Update available: " + update.versionName,
android.widget.Toast.LENGTH_LONG).show()); android.widget.Toast.LENGTH_LONG).show());
} catch (Throwable t) { } catch (Throwable t) {
Log.w(TAG, "Startup update check failed", t); Log.w(TAG, "Startup update check failed: " + t.getClass().getSimpleName());
UpdateLog.record(app, "Check at start: " + UpdateDiagnosis.failure(t));
} }
}, "ota-startup-check").start(); }, "ota-startup-check").start();
} }
@@ -78,24 +84,46 @@ final class UpdateHook {
return; return;
} }
new Thread(() -> { new Thread(() -> {
long startedMs = System.currentTimeMillis();
List<String> steps = new ArrayList<>();
step(app, steps, "Check started, app " + BuildConfig.VERSION_NAME);
String message; String message;
try { try {
for (String network : UpdateNetworkCheck.networks(app)) step(app, steps, network);
ServerConfig config = ServerConfig.get(app);
String host = config.isConfigured() ? OtaUpdater.allowedHost(config) : null;
if (host != null) step(app, steps, UpdateNetworkCheck.lookup(host));
message = runCheck(app, listener, main); message = runCheck(app, listener, main);
} catch (Throwable t) { } catch (Throwable t) {
Log.w(TAG, "Update check threw", t); Log.w(TAG, "Update check threw: " + t.getClass().getSimpleName());
// The class only: a network error's text can carry the server's address. message = "Update check failed: " + UpdateDiagnosis.failure(t);
message = "Update check failed (" + t.getClass().getSimpleName() + ")"; // Is it the server or the connection? The answer to "is my SIM out of data".
step(app, steps, UpdateNetworkCheck.internet());
} finally { } finally {
checking.set(false); checking.set(false);
} }
UpdateLog.record(app, "Check: " + message); step(app, steps, "Check: " + message);
String waiting = LogOutbox.status(app);
if (waiting != null) step(app, steps, "Reports: " + waiting);
if (listener != null) { if (listener != null) {
final String result = message; final String result = message;
main.post(() -> listener.onResult(result)); main.post(() -> listener.onResult(result));
} }
// After the answer: without a connection this waits for the send to fail.
String name = "update-check-" + new SimpleDateFormat("yyyyMMdd-HHmmss", Locale.US)
.format(new Date(startedMs)) + ".txt";
// Only the newest waits: repeated checks offline must not push out a recording.
LogOutbox.dropWaiting(app, "update-check-");
SignalRecorder.saveCopies(app, "update_check.txt", name, steps);
}, "ota-check").start(); }, "ota-check").start();
} }
/** One step of a check: on the Logs tab now, and in the report saved when it ends. */
private static void step(Context app, List<String> steps, String what) {
UpdateLog.record(app, what);
steps.add(what);
}
/** /**
* Installs the newest car build from the top folder of a USB stick (UsbUpdate), with the * Installs the newest car build from the top folder of a USB stick (UsbUpdate), with the
* same checks as the over-the-air update except the server. Background thread; [listener] * same checks as the over-the-air update except the server. Background thread; [listener]
@@ -195,14 +223,14 @@ final class UpdateHook {
try { try {
expectedHash = OtaUpdater.fetchExpectedSha256(update, config); expectedHash = OtaUpdater.fetchExpectedSha256(update, config);
} catch (IOException e) { } catch (IOException e) {
Log.w(TAG, "OTA hash unavailable: " + e.getMessage()); Log.w(TAG, "OTA hash unavailable: " + e.getClass().getSimpleName());
return "Update refused: no SHA-256 verification"; return "Update refused, no SHA-256 verification: " + UpdateDiagnosis.failure(e);
} }
File apk = OtaUpdater.download(update, expectedHash, config, percent -> { File apk = OtaUpdater.download(update, expectedHash, config, percent -> {
if (listener != null) main.post(() -> listener.onProgress(percent)); if (listener != null) main.post(() -> listener.onProgress(percent));
}); });
if (apk == null) return "Download or SHA-256 verification failed (" if (apk == null) return "Download or SHA-256 verification failed ("
+ update.versionName + ")"; + update.versionName + "): " + OtaUpdater.downloadProblem;
switch (OtaUpdater.install(app, apk)) { switch (OtaUpdater.install(app, apk)) {
case SESSION_STARTED: case SESSION_STARTED:
@@ -0,0 +1,92 @@
package com.evsuite.abrp;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkInfo;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
/**
* What an update check writes down about the connection before and after it fails: the networks
* Android sees, whether the server's name can be looked up, and whether the internet answers at
* all, which tells a SIM without data from a server that is down. Blocking: background thread.
* Never logs the server's address or anything about the car. — UNSTABLE BUILDS ONLY.
*/
final class UpdateNetworkCheck {
/** Android's own connectivity test, as Android itself uses it; no data about the car is sent. */
private static final String INTERNET_TEST = "https://www.gstatic.com/generate_204";
private static final int TIMEOUT_MS = 10_000;
private UpdateNetworkCheck() {}
/** One line per network, or one saying there is none. */
static List<String> networks(Context app) {
List<String> out = new ArrayList<>();
try {
ConnectivityManager cm = (ConnectivityManager) app.getSystemService(Context.CONNECTIVITY_SERVICE);
if (cm == null) {
out.add("Network: no connectivity service");
return out;
}
Network active = cm.getActiveNetwork();
if (active == null) out.add("Network: none in use");
for (Network network : cm.getAllNetworks()) {
NetworkCapabilities caps = cm.getNetworkCapabilities(network);
if (caps == null) continue;
NetworkInfo info = cm.getNetworkInfo(network);
out.add("Network: " + UpdateDiagnosis.network(transports(caps),
caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET),
caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED),
network.equals(active), info != null && info.isRoaming()));
}
} catch (RuntimeException e) {
out.add("Network: unreadable (" + e.getClass().getSimpleName() + ")");
}
return out;
}
private static List<String> transports(NetworkCapabilities caps) {
List<String> out = new ArrayList<>();
if (caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) out.add("cellular");
if (caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) out.add("Wi-Fi");
if (caps.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) out.add("ethernet");
if (caps.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH)) out.add("Bluetooth");
if (caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) out.add("VPN");
return out;
}
/** Whether the host's name resolves, without saying the name. */
static String lookup(String host) {
long started = System.currentTimeMillis();
try {
InetAddress.getByName(host);
return "Server's name: found in " + (System.currentTimeMillis() - started) + " ms";
} catch (IOException | RuntimeException e) {
return "Server's name: " + UpdateDiagnosis.failure(e);
}
}
/** Whether the world outside the car server answers. */
static String internet() {
HttpURLConnection conn = null;
try {
conn = (HttpURLConnection) new URL(INTERNET_TEST).openConnection();
conn.setInstanceFollowRedirects(false);
conn.setConnectTimeout(TIMEOUT_MS);
conn.setReadTimeout(TIMEOUT_MS);
return "Internet test: " + UpdateDiagnosis.internet(conn.getResponseCode());
} catch (IOException | RuntimeException e) {
return "Internet test: " + UpdateDiagnosis.failure(e);
} finally {
if (conn != null) conn.disconnect();
}
}
}