App 2.3.0: Server tab, live telemetry to the car server
- New Server tab: server URL, ingest token and installation id, with
Save, Test connection (health, then a read-only token check) and the
config-file import that upstream removed (app folder / USB stick first,
document picker as fallback). Settings are kept encrypted and read on
every use, so the service picks up a change without a restart. Each
installation generates its own id; the inherited upstream UUID and
the baked-in CONSUMPTION_API_* build fields are gone.
- Live telemetry restored from before ea8032a (TelemetryPayload,
UploadCadence, UploadSettings, GPS) and sent in batches to
POST /v1/telemetry instead of ABRP; the server forwards to ABRP. Adds
ext_temp, voltage and current. Unsent samples queue in memory (about
8 h) while offline.
- Upload tab: the live-telemetry switch and cadence controls work again;
the dead service and autostart switches are removed.
- Manifest: location permissions and foreground service types.
- Unit tests compile again: 114 (stable) / 136 (unstable) pass. Tests
for the deleted AbrpApi and HeadUnitWeather removed; new tests for
ServerConfig and TelemetryQueue. Lint passes.
Not yet tried on the car.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Where the car's data goes: the server URL, its ingest token, and the installation id the
|
||||
* data is stored under. Entered in the Server tab or imported from a config file, kept in
|
||||
* {@link SecurePrefs}.
|
||||
*
|
||||
* Read through {@link #get}, which caches one immutable snapshot for the process and is
|
||||
* replaced on {@link #save}: the service and the activity both see a change at once, without
|
||||
* a restart.
|
||||
*/
|
||||
final class ServerConfig {
|
||||
|
||||
static final String KEY_URL = "server_url";
|
||||
static final String KEY_TOKEN = "ingest_token";
|
||||
static final String KEY_INSTALLATION_ID = "installation_id";
|
||||
|
||||
/** Same rule as the server: a version 1–5 UUID with the RFC 4122 variant. */
|
||||
private static final Pattern UUID_PATTERN = Pattern.compile(
|
||||
"^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
|
||||
Pattern.CASE_INSENSITIVE);
|
||||
|
||||
/** The server refuses shorter ingest tokens outright. */
|
||||
static final int MIN_TOKEN_LENGTH = 24;
|
||||
|
||||
final String url;
|
||||
final String token;
|
||||
final String installationId;
|
||||
|
||||
private static volatile ServerConfig current;
|
||||
|
||||
ServerConfig(String url, String token, String installationId) {
|
||||
this.url = url;
|
||||
this.token = token;
|
||||
this.installationId = installationId;
|
||||
}
|
||||
|
||||
boolean isConfigured() {
|
||||
return !url.isEmpty() && !token.isEmpty();
|
||||
}
|
||||
|
||||
static ServerConfig get(Context context) {
|
||||
ServerConfig config = current;
|
||||
if (config != null) return config;
|
||||
synchronized (ServerConfig.class) {
|
||||
if (current != null) return current;
|
||||
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();
|
||||
prefs.edit().putString(KEY_INSTALLATION_ID, id).apply();
|
||||
}
|
||||
current = new ServerConfig(prefs.getString(KEY_URL, ""), prefs.getString(KEY_TOKEN, ""), id);
|
||||
return current;
|
||||
}
|
||||
}
|
||||
|
||||
/** Stores already-validated values; see {@link #validate}. */
|
||||
static synchronized void save(Context context, String url, String token, String installationId) {
|
||||
SecurePrefs.get(context).edit()
|
||||
.putString(KEY_URL, url)
|
||||
.putString(KEY_TOKEN, token)
|
||||
.putString(KEY_INSTALLATION_ID, installationId)
|
||||
.apply();
|
||||
current = new ServerConfig(url, token, installationId);
|
||||
}
|
||||
|
||||
// ---------- Validation (pure, unit-tested) ----------
|
||||
|
||||
/**
|
||||
* The URL as it will be used: trimmed, without a trailing slash. Null when it is not an
|
||||
* https URL with a host — the token travels in a header, so plain http is refused.
|
||||
*/
|
||||
static String normalizeUrl(String raw) {
|
||||
if (raw == null) return null;
|
||||
String url = raw.trim();
|
||||
while (url.endsWith("/")) url = url.substring(0, url.length() - 1);
|
||||
if (!url.toLowerCase(Locale.US).startsWith("https://")) return null;
|
||||
String rest = url.substring("https://".length());
|
||||
if (rest.isEmpty() || rest.startsWith("/") || rest.contains(" ") || rest.contains("?")
|
||||
|| rest.contains("#") || rest.contains("@")) return null;
|
||||
return url;
|
||||
}
|
||||
|
||||
static boolean isValidToken(String token) {
|
||||
return token != null && token.trim().length() >= MIN_TOKEN_LENGTH && !token.trim().contains(" ");
|
||||
}
|
||||
|
||||
static boolean isValidInstallationId(String id) {
|
||||
return id != null && UUID_PATTERN.matcher(id.trim()).matches();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user