Code review fixes: app 2.9.0, server 2.2.0
Telemetry no longer loses queued batches. The server drops a bad value, or a sample without a plausible time, instead of rejecting the batch, and answers 200 with what it dropped. The app sends only values inside the server's ranges (speed as a magnitude, so reversing is fine) and no samples before the head unit's clock is set. Server: - Pairing hands out and accepts a token only after the device is saved; a failed save leaves the request pending. - A paired token serves only the installation id it was paired with (403 otherwise); the shared INGEST_TOKEN still serves any. - Wrong-password lockouts and open pairing requests are limited per client address (last X-Forwarded-For entry), so a stranger cannot lock the owner out; a car starting again replaces its own request. - ABRP forwarding skips samples stamped ahead of the server's clock, which used to stop it until a restart. App: - Settings written to plaintext during a keystore failure move back into encrypted storage; the stored installation id wins. - The installation id is derived from ANDROID_ID on first start, so a reinstall finds its history; a stored or imported id still wins. - The scanned config file is deleted after import (it holds the token). - GPS stops while live telemetry is off or the server unconfigured, and pauses while parked and idle, keeping the fix the car parked with. - Range estimates use the monotonic clock. - One HTTP helper (ServerHttp) for telemetry, sync and pairing. - Last Turkish strings translated; mise logs shows the NilsMG4 tags. Grafana reads InfluxDB with its own read-only token (new grafana-influx Secret, created by hand as described in k8s/README.md). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
This commit is contained in:
@@ -49,6 +49,9 @@ final class ServerConfig {
|
||||
return !url.isEmpty() && !token.isEmpty();
|
||||
}
|
||||
|
||||
// ANDROID_ID on purpose: it is hashed into the installation id, so a reinstall keeps its
|
||||
// history (installationIdFor). It is never sent or logged as it is.
|
||||
@android.annotation.SuppressLint("HardwareIds")
|
||||
static ServerConfig get(Context context) {
|
||||
ServerConfig config = current;
|
||||
if (config != null) return config;
|
||||
@@ -57,8 +60,18 @@ final class ServerConfig {
|
||||
SharedPreferences prefs = SecurePrefs.get(context);
|
||||
String id = prefs.getString(KEY_INSTALLATION_ID, "");
|
||||
if (!isValidInstallationId(id)) {
|
||||
// First start: this installation gets its own identity, kept from now on.
|
||||
id = UUID.randomUUID().toString();
|
||||
// First start, or the first after a reinstall: derived from the head unit, so
|
||||
// the history stays under the same id (see installationIdFor).
|
||||
String androidId = null;
|
||||
try {
|
||||
androidId = android.provider.Settings.Secure.getString(
|
||||
context.getApplicationContext().getContentResolver(),
|
||||
android.provider.Settings.Secure.ANDROID_ID);
|
||||
} catch (RuntimeException ignored) {
|
||||
// no id from the platform: a random one below
|
||||
}
|
||||
id = installationIdFor(androidId);
|
||||
if (id == null) id = UUID.randomUUID().toString();
|
||||
prefs.edit().putString(KEY_INSTALLATION_ID, id).apply();
|
||||
}
|
||||
current = new ServerConfig(prefs.getString(KEY_URL, DEFAULT_URL), prefs.getString(KEY_TOKEN, ""), id);
|
||||
@@ -97,6 +110,22 @@ final class ServerConfig {
|
||||
return token != null && token.trim().length() >= MIN_TOKEN_LENGTH && !token.trim().contains(" ");
|
||||
}
|
||||
|
||||
/**
|
||||
* The installation id for a head unit, or null without a usable ANDROID_ID. From Android 8
|
||||
* ANDROID_ID is fixed per device and signing key and survives an uninstall, so a
|
||||
* reinstall of the same platform-signed build finds its history again without importing
|
||||
* the id. Hashed into a name-based UUID: the server expects a UUID, and the raw
|
||||
* ANDROID_ID never leaves the car. An id already stored (or imported) always wins over it.
|
||||
*/
|
||||
static String installationIdFor(String androidId) {
|
||||
if (androidId == null) return null;
|
||||
String trimmed = androidId.trim();
|
||||
// The emulator-era constant that many devices once shared; not an identity.
|
||||
if (trimmed.isEmpty() || trimmed.equalsIgnoreCase("9774d56d682e549c")) return null;
|
||||
byte[] name = ("nilsmg4:" + trimmed.toLowerCase(Locale.US)).getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
return UUID.nameUUIDFromBytes(name).toString();
|
||||
}
|
||||
|
||||
static boolean isValidInstallationId(String id) {
|
||||
return id != null && UUID_PATTERN.matcher(id.trim()).matches();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user