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); /** Pre-filled so pairing needs no typing in the car; can still be changed. */ static final String DEFAULT_URL = "https://car.k8s.stinnesbeck.com"; /** 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(); } // 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; 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, 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); 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(" "); } /** * 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(); } }