App 2.11.0, server 2.3.0: live data straight to ABRP; settings regrouped

ABRP keeps getting live data while the car server is down: with an ABRP
API key and user token set in the app (Live data tab, or abrp_api_key /
abrp_token in the config file), the car sends the newest sample to ABRP
itself at every upload. It tells the server the newest sample ABRP took
(abrpSentUtc), and the server forwards only newer ones, so ABRP gets
each sample once. Without credentials in the app the server forwards
everything, as before. The credentials sit in encrypted storage, go in
the request body, and neither they nor ABRP's answer are logged.

Settings are grouped by what they are for:
- Live data (was Upload): on/off, ABRP credentials, upload frequency.
- Settings (replaces the Wi-Fi tab): full screen and voice key,
  usable capacity and manual SoH (were on Consumption), charging price
  (was on Charging), Wi-Fi.
Charging and Live data get icons of their own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
This commit is contained in:
2026-10-18 12:15:21 +02:00
co-authored by Claude Opus 5.5
parent 087f193f8c
commit ca2f899403
36 changed files with 636 additions and 163 deletions
@@ -0,0 +1,63 @@
package com.evsuite.abrp;
import android.content.Context;
import android.content.SharedPreferences;
/**
* The ABRP credentials for sending live data straight from the car: the API key (which app is
* sending) and the user token (which car in which ABRP account). Kept in {@link SecurePrefs}
* next to the server settings, entered on the Live data tab or imported from a config file.
*
* With both set, the car sends to ABRP itself, so ABRP keeps getting live data when the car
* server is down; the server then only forwards what the car could not deliver. Without them,
* the server forwards as before.
*/
final class AbrpConfig {
static final String KEY_API_KEY = "abrp_api_key";
static final String KEY_USER_TOKEN = "abrp_user_token";
final String apiKey;
final String userToken;
private static volatile AbrpConfig current;
AbrpConfig(String apiKey, String userToken) {
this.apiKey = apiKey;
this.userToken = userToken;
}
boolean isConfigured() {
return !apiKey.isEmpty() && !userToken.isEmpty();
}
static AbrpConfig get(Context context) {
AbrpConfig config = current;
if (config != null) return config;
synchronized (AbrpConfig.class) {
if (current == null) {
SharedPreferences prefs = SecurePrefs.get(context);
current = new AbrpConfig(prefs.getString(KEY_API_KEY, ""), prefs.getString(KEY_USER_TOKEN, ""));
}
return current;
}
}
/** Stores trimmed values; empty strings switch direct sending off. */
static synchronized void save(Context context, String apiKey, String userToken) {
String key = apiKey == null ? "" : apiKey.trim();
String token = userToken == null ? "" : userToken.trim();
SecurePrefs.get(context).edit()
.putString(KEY_API_KEY, key)
.putString(KEY_USER_TOKEN, token)
.apply();
current = new AbrpConfig(key, token);
}
/** Plausible as an ABRP credential: no spaces, not absurdly short or long. */
static boolean isPlausible(String value) {
if (value == null) return false;
String v = value.trim();
return v.length() >= 8 && v.length() <= 200 && !v.contains(" ");
}
}