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:
@@ -0,0 +1,59 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* ABRP's telemetry API, for sending live data straight from the car.
|
||||
*
|
||||
* Credentials go in the POST body, never in the query string: a URL travels through proxies,
|
||||
* access logs and crash reports, a body does not. Neither is ever logged.
|
||||
*/
|
||||
final class AbrpApi {
|
||||
|
||||
/** No query string: credentials travel in the body, so the URL is a constant. */
|
||||
static final String SEND_URL = "https://api.iternio.com/1/tlm/send";
|
||||
|
||||
/**
|
||||
* Read-only endpoint used to check credentials. The Test button must never touch
|
||||
* /tlm/send: a placeholder sample would tell ABRP the car really is in that state and
|
||||
* corrupt a live route plan.
|
||||
*/
|
||||
static final String VERIFY_URL = "https://api.iternio.com/1/tlm/get_next_charge";
|
||||
|
||||
private AbrpApi() { }
|
||||
|
||||
/** Posts one tlm sample. Throws on a network failure. */
|
||||
static ServerHttp.Response send(AbrpConfig config, String tlmJson) throws Exception {
|
||||
byte[] body = formBody(config.apiKey, config.userToken, tlmJson).getBytes(StandardCharsets.UTF_8);
|
||||
return ServerHttp.send("POST", SEND_URL, null, "application/x-www-form-urlencoded", body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the credentials without writing anything. This endpoint only takes the token as a
|
||||
* query parameter; the API key goes in the Authorization header. One token in one URL when
|
||||
* the user presses Test beats writing fake telemetry on every press.
|
||||
*/
|
||||
static ServerHttp.Response verify(AbrpConfig config) throws Exception {
|
||||
String url = VERIFY_URL + "?token=" + URLEncoder.encode(config.userToken, StandardCharsets.UTF_8.name());
|
||||
return ServerHttp.send("GET", url, "APIKEY " + config.apiKey, null, null);
|
||||
}
|
||||
|
||||
/** The form body: credentials and the sample, each URL-encoded. */
|
||||
static String formBody(String apiKey, String userToken, String tlmJson) throws Exception {
|
||||
String utf8 = StandardCharsets.UTF_8.name();
|
||||
return "api_key=" + URLEncoder.encode(apiKey, utf8)
|
||||
+ "&token=" + URLEncoder.encode(userToken, utf8)
|
||||
+ "&tlm=" + URLEncoder.encode(tlmJson, utf8);
|
||||
}
|
||||
|
||||
/** ABRP answers 200 with {"status":"ok"}; anything else is a rejection. */
|
||||
static boolean accepted(ServerHttp.Response response) {
|
||||
if (!response.ok()) return false;
|
||||
try {
|
||||
return "ok".equals(new org.json.JSONObject(response.body).optString("status"));
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(" ");
|
||||
}
|
||||
}
|
||||
@@ -173,7 +173,9 @@ public class AbrpUploadService extends Service {
|
||||
}
|
||||
|
||||
UploadSettings settings = UploadSettings.from(prefs);
|
||||
if (!ServerConfig.get(this).isConfigured()) {
|
||||
// Live data goes to the server, to ABRP directly, or both; with neither set up there is
|
||||
// nowhere to send it.
|
||||
if (!ServerConfig.get(this).isConfigured() && !AbrpConfig.get(this).isConfigured()) {
|
||||
telemetryUploader.stop(); // no live telemetry, so no GPS either
|
||||
updateNotification(getString(R.string.server_not_configured));
|
||||
} else if (!settings.abrpUploadEnabled) {
|
||||
|
||||
@@ -13,6 +13,8 @@ package com.evsuite.abrp;
|
||||
* interval_sec = 60
|
||||
* boost_low_soc = true
|
||||
* low_soc_percent = 20
|
||||
* abrp_api_key = your-abrp-api-key (optional: send live data to ABRP directly)
|
||||
* abrp_token = your-abrp-user-token
|
||||
* </pre>
|
||||
*
|
||||
* Usually only server_url and token are needed. installation_id is for a reinstall: it puts
|
||||
@@ -29,25 +31,29 @@ final class ConfigImport {
|
||||
final Integer intervalSec;
|
||||
final Boolean boostLowSoc;
|
||||
final Integer lowSocPercent;
|
||||
final String abrpApiKey;
|
||||
final String abrpToken;
|
||||
|
||||
private ConfigImport(String serverUrl, String token, String installationId, Integer intervalSec,
|
||||
Boolean boostLowSoc, Integer lowSocPercent) {
|
||||
Boolean boostLowSoc, Integer lowSocPercent, String abrpApiKey, String abrpToken) {
|
||||
this.serverUrl = serverUrl;
|
||||
this.token = token;
|
||||
this.installationId = installationId;
|
||||
this.intervalSec = intervalSec;
|
||||
this.boostLowSoc = boostLowSoc;
|
||||
this.lowSocPercent = lowSocPercent;
|
||||
this.abrpApiKey = abrpApiKey;
|
||||
this.abrpToken = abrpToken;
|
||||
}
|
||||
|
||||
/** True when the file yielded nothing usable — an empty or malformed file, not a config. */
|
||||
boolean isEmpty() {
|
||||
return serverUrl == null && token == null && installationId == null && intervalSec == null
|
||||
&& boostLowSoc == null && lowSocPercent == null;
|
||||
&& boostLowSoc == null && lowSocPercent == null && abrpApiKey == null && abrpToken == null;
|
||||
}
|
||||
|
||||
static ConfigImport parse(String text) {
|
||||
String serverUrl = null, token = null, installationId = null;
|
||||
String serverUrl = null, token = null, installationId = null, abrpApiKey = null, abrpToken = null;
|
||||
Integer intervalSec = null, lowSocPercent = null;
|
||||
Boolean boostLowSoc = null;
|
||||
|
||||
@@ -85,13 +91,21 @@ final class ConfigImport {
|
||||
case "low_soc_percent":
|
||||
lowSocPercent = clampPercent(value);
|
||||
break;
|
||||
case "abrp_api_key":
|
||||
abrpApiKey = value;
|
||||
break;
|
||||
case "abrp_token":
|
||||
case "abrp_user_token":
|
||||
abrpToken = value;
|
||||
break;
|
||||
default:
|
||||
// Unknown keys are ignored rather than rejected, so a file can carry
|
||||
// comments or future keys without breaking an older build.
|
||||
break;
|
||||
}
|
||||
}
|
||||
return new ConfigImport(serverUrl, token, installationId, intervalSec, boostLowSoc, lowSocPercent);
|
||||
return new ConfigImport(serverUrl, token, installationId, intervalSec, boostLowSoc, lowSocPercent,
|
||||
abrpApiKey, abrpToken);
|
||||
}
|
||||
|
||||
/** Snaps to the nearest offered choice: the UI spinner has no free-form interval. */
|
||||
|
||||
@@ -42,16 +42,16 @@ public class MainActivity extends AppCompatActivity {
|
||||
/** Navigation destinations in page order. Parallel to {@link #panes}. */
|
||||
private static final int[] TAB_IDS =
|
||||
{ R.id.tabVehicle, R.id.tabConsumption, R.id.tabCharging, R.id.tabService, R.id.tabServer,
|
||||
R.id.tabWifi, R.id.tabLog };
|
||||
R.id.tabSettings, R.id.tabLog };
|
||||
|
||||
private static final int[] PAGE_TITLES = {
|
||||
R.string.design_overview_title, R.string.consumption_title, R.string.charging_title,
|
||||
R.string.design_service_title, R.string.design_server_title, R.string.design_wifi_title,
|
||||
R.string.design_service_title, R.string.design_server_title, R.string.design_settings_title,
|
||||
R.string.design_log_title
|
||||
};
|
||||
private static final int[] PAGE_SUBTITLES = {
|
||||
R.string.design_overview_subtitle, R.string.consumption_subtitle, R.string.charging_subtitle,
|
||||
R.string.design_service_subtitle, R.string.design_server_subtitle, R.string.design_wifi_subtitle,
|
||||
R.string.design_service_subtitle, R.string.design_server_subtitle, R.string.design_settings_subtitle,
|
||||
R.string.design_log_subtitle
|
||||
};
|
||||
|
||||
@@ -70,7 +70,8 @@ public class MainActivity extends AppCompatActivity {
|
||||
private View vehiclePane;
|
||||
private View consumptionPane;
|
||||
private View chargingPane;
|
||||
private View wifiPane;
|
||||
/** Display, battery, charging price and Wi-Fi. */
|
||||
private View settingsPane;
|
||||
|
||||
/** The pages in navigation order. Parallel to {@link #TAB_IDS}. */
|
||||
private View[] panes;
|
||||
@@ -241,10 +242,10 @@ public class MainActivity extends AppCompatActivity {
|
||||
vehiclePane = inflatePane(R.layout.pane_vehicle);
|
||||
consumptionPane = inflatePane(R.layout.pane_consumption);
|
||||
chargingPane = inflatePane(R.layout.pane_charging);
|
||||
wifiPane = inflatePane(R.layout.pane_wifi);
|
||||
settingsPane = inflatePane(R.layout.pane_settings);
|
||||
logPane = inflatePane(R.layout.pane_log);
|
||||
panes = new View[] { vehiclePane, consumptionPane, chargingPane, servicePane, serverPane,
|
||||
wifiPane, logPane };
|
||||
settingsPane, logPane };
|
||||
|
||||
bindVehiclePane();
|
||||
bindConsumptionPane();
|
||||
@@ -836,6 +837,14 @@ public class MainActivity extends AppCompatActivity {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (config.abrpApiKey != null || config.abrpToken != null) {
|
||||
AbrpConfig abrp = AbrpConfig.get(this);
|
||||
if (!saveAbrpConfig(config.abrpApiKey != null ? config.abrpApiKey : abrp.apiKey,
|
||||
config.abrpToken != null ? config.abrpToken : abrp.userToken)) {
|
||||
setConnectionStatus(COLOR_ERROR, getString(R.string.abrp_invalid));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
SharedPreferences.Editor edit = prefs.edit();
|
||||
if (config.intervalSec != null) edit.putInt(UploadSettings.KEY_INTERVAL_SEC, config.intervalSec);
|
||||
if (config.boostLowSoc != null) edit.putBoolean(UploadSettings.KEY_BOOST_LOW_SOC, config.boostLowSoc);
|
||||
@@ -850,11 +859,11 @@ public class MainActivity extends AppCompatActivity {
|
||||
// ---------- Upload page ----------
|
||||
|
||||
private void bindServicePane() {
|
||||
voiceKeySwitch = servicePane.findViewById(R.id.voice_key_switch);
|
||||
voiceKeySwitch = settingsPane.findViewById(R.id.voice_key_switch);
|
||||
voiceKeySwitch.setChecked(UploadSettings.voiceKeyOpensApp(prefs));
|
||||
voiceKeySwitch.setOnCheckedChangeListener((btn, checked) ->
|
||||
prefs.edit().putBoolean(UploadSettings.KEY_VOICE_KEY_OPENS, checked).apply());
|
||||
fullScreenSwitch = servicePane.findViewById(R.id.full_screen_switch);
|
||||
fullScreenSwitch = settingsPane.findViewById(R.id.full_screen_switch);
|
||||
fullScreenSwitch.setChecked(UploadSettings.fullScreen(prefs));
|
||||
fullScreenSwitch.setOnCheckedChangeListener((btn, checked) -> {
|
||||
prefs.edit().putBoolean(UploadSettings.KEY_FULL_SCREEN, checked).apply();
|
||||
@@ -871,6 +880,66 @@ public class MainActivity extends AppCompatActivity {
|
||||
refreshStatus();
|
||||
});
|
||||
bindCadenceControls();
|
||||
bindAbrpControls();
|
||||
}
|
||||
|
||||
// ---------- ABRP, directly from the car ----------
|
||||
|
||||
private TextInputEditText abrpApiKeyInput, abrpTokenInput;
|
||||
private TextView abrpStatusText;
|
||||
|
||||
private void bindAbrpControls() {
|
||||
abrpApiKeyInput = servicePane.findViewById(R.id.abrp_api_key_input);
|
||||
abrpTokenInput = servicePane.findViewById(R.id.abrp_token_input);
|
||||
abrpStatusText = servicePane.findViewById(R.id.abrp_status_text);
|
||||
AbrpConfig current = AbrpConfig.get(this);
|
||||
abrpApiKeyInput.setText(current.apiKey);
|
||||
abrpTokenInput.setText(current.userToken);
|
||||
servicePane.findViewById(R.id.abrp_save_button).setOnClickListener(v ->
|
||||
saveAbrpConfig(text(abrpApiKeyInput), text(abrpTokenInput)));
|
||||
servicePane.findViewById(R.id.abrp_test_button).setOnClickListener(v -> testAbrpConfig());
|
||||
}
|
||||
|
||||
private static String text(TextInputEditText input) {
|
||||
CharSequence raw = input.getText();
|
||||
return raw == null ? "" : raw.toString().trim();
|
||||
}
|
||||
|
||||
/** Both set and plausible, or both empty (direct sending off). Returns whether it saved. */
|
||||
private boolean saveAbrpConfig(String apiKey, String token) {
|
||||
boolean clear = apiKey.isEmpty() && token.isEmpty();
|
||||
if (!clear && !(AbrpConfig.isPlausible(apiKey) && AbrpConfig.isPlausible(token))) {
|
||||
abrpStatusText.setText(R.string.abrp_invalid);
|
||||
return false;
|
||||
}
|
||||
AbrpConfig.save(this, apiKey, token);
|
||||
abrpApiKeyInput.setText(apiKey);
|
||||
abrpTokenInput.setText(token);
|
||||
abrpStatusText.setText(clear ? R.string.abrp_cleared : R.string.abrp_saved);
|
||||
refreshStatus();
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Read-only check against ABRP; never sends telemetry. Off the main thread. */
|
||||
private void testAbrpConfig() {
|
||||
AbrpConfig config = new AbrpConfig(text(abrpApiKeyInput), text(abrpTokenInput));
|
||||
if (!config.isConfigured()) {
|
||||
abrpStatusText.setText(R.string.abrp_not_set);
|
||||
return;
|
||||
}
|
||||
abrpStatusText.setText(R.string.abrp_testing);
|
||||
new Thread(() -> {
|
||||
String message;
|
||||
try {
|
||||
ServerHttp.Response response = AbrpApi.verify(config);
|
||||
message = response.ok() ? getString(R.string.abrp_test_ok)
|
||||
: getString(R.string.abrp_test_failed, response.status);
|
||||
} catch (Exception e) {
|
||||
message = getString(R.string.abrp_test_offline, e.getClass().getSimpleName());
|
||||
}
|
||||
String shown = message;
|
||||
runOnUiThread(() -> abrpStatusText.setText(shown));
|
||||
}, "abrp-test").start();
|
||||
}
|
||||
|
||||
/** The service reads these settings on every tick, so a change applies without a restart. */
|
||||
@@ -946,16 +1015,16 @@ public class MainActivity extends AppCompatActivity {
|
||||
|
||||
private void bindWifiPane() {
|
||||
wifi = new WifiAutoConnect(getApplicationContext());
|
||||
wifiSsid = wifiPane.findViewById(R.id.wifi_ssid);
|
||||
wifiState = wifiPane.findViewById(R.id.wifi_state);
|
||||
wifiSavedList = wifiPane.findViewById(R.id.wifi_saved_list);
|
||||
wifiSsid = settingsPane.findViewById(R.id.wifi_ssid);
|
||||
wifiState = settingsPane.findViewById(R.id.wifi_state);
|
||||
wifiSavedList = settingsPane.findViewById(R.id.wifi_saved_list);
|
||||
|
||||
SwitchMaterial auto = wifiPane.findViewById(R.id.wifi_auto_switch);
|
||||
SwitchMaterial auto = settingsPane.findViewById(R.id.wifi_auto_switch);
|
||||
auto.setChecked(prefs.getBoolean(WIFI_AUTO_KEY, true));
|
||||
auto.setOnCheckedChangeListener((btn, checked) ->
|
||||
prefs.edit().putBoolean(WIFI_AUTO_KEY, checked).apply());
|
||||
|
||||
wifiPane.findViewById(R.id.wifi_connect_button).setOnClickListener(v -> {
|
||||
settingsPane.findViewById(R.id.wifi_connect_button).setOnClickListener(v -> {
|
||||
// force: the driver is asking now, so the rate limit does not apply.
|
||||
WifiAutoConnect.Result r = wifi.ensureConnected(true);
|
||||
wifiState.setText(r.message);
|
||||
@@ -1099,16 +1168,16 @@ public class MainActivity extends AppCompatActivity {
|
||||
consumptionDateButton.setOnClickListener(v -> showConsumptionDatePicker());
|
||||
consumptionPane.findViewById(R.id.consumption_refresh).setOnClickListener(v -> refreshFromCloud());
|
||||
consumptionReset = consumptionPane.findViewById(R.id.consumption_reset);
|
||||
consumptionSohLayout = consumptionPane.findViewById(R.id.consumption_soh_layout);
|
||||
consumptionSohInput = consumptionPane.findViewById(R.id.consumption_soh_input);
|
||||
consumptionSohLayout = settingsPane.findViewById(R.id.consumption_soh_layout);
|
||||
consumptionSohInput = settingsPane.findViewById(R.id.consumption_soh_input);
|
||||
Float savedSoh = consumptionTracker.sohPercent();
|
||||
if (savedSoh != null) consumptionSohInput.setText(String.format(
|
||||
java.util.Locale.US, "%.1f", savedSoh));
|
||||
consumptionPane.findViewById(R.id.consumption_soh_save).setOnClickListener(v -> saveSoh());
|
||||
usableCapacityLayout = consumptionPane.findViewById(R.id.usable_capacity_layout);
|
||||
usableCapacityInput = consumptionPane.findViewById(R.id.usable_capacity_input);
|
||||
settingsPane.findViewById(R.id.consumption_soh_save).setOnClickListener(v -> saveSoh());
|
||||
usableCapacityLayout = settingsPane.findViewById(R.id.usable_capacity_layout);
|
||||
usableCapacityInput = settingsPane.findViewById(R.id.usable_capacity_input);
|
||||
usableCapacityInput.setText(String.format(java.util.Locale.US, "%.1f", usableKwh()));
|
||||
consumptionPane.findViewById(R.id.usable_capacity_save).setOnClickListener(v -> saveUsableCapacity());
|
||||
settingsPane.findViewById(R.id.usable_capacity_save).setOnClickListener(v -> saveUsableCapacity());
|
||||
bindPeriodButton(R.id.consumption_lifetime, ConsumptionTracker.Period.LIFETIME);
|
||||
bindPeriodButton(R.id.consumption_week, ConsumptionTracker.Period.WEEK);
|
||||
bindPeriodButton(R.id.consumption_month, ConsumptionTracker.Period.MONTH);
|
||||
@@ -1192,11 +1261,11 @@ public class MainActivity extends AppCompatActivity {
|
||||
chargeTotalCost = chargingPane.findViewById(R.id.charge_total_cost);
|
||||
chargeGraph = chargingPane.findViewById(R.id.charge_graph);
|
||||
chargeCurveTable = chargingPane.findViewById(R.id.charge_curve_table);
|
||||
chargePriceLayout = chargingPane.findViewById(R.id.charge_price_layout);
|
||||
chargePriceInput = chargingPane.findViewById(R.id.charge_price_input);
|
||||
chargePriceLayout = settingsPane.findViewById(R.id.charge_price_layout);
|
||||
chargePriceInput = settingsPane.findViewById(R.id.charge_price_input);
|
||||
chargePriceInput.setText(String.format(java.util.Locale.US, "%.2f",
|
||||
chargeSessionTracker.pricePerKwh()));
|
||||
chargingPane.findViewById(R.id.charge_price_save).setOnClickListener(v -> saveChargePrice());
|
||||
settingsPane.findViewById(R.id.charge_price_save).setOnClickListener(v -> saveChargePrice());
|
||||
}
|
||||
|
||||
private void saveChargePrice() {
|
||||
@@ -1446,9 +1515,13 @@ public class MainActivity extends AppCompatActivity {
|
||||
break;
|
||||
case RUNNING:
|
||||
case STARTING:
|
||||
if (!ServerConfig.get(this).isConfigured()) {
|
||||
boolean server = ServerConfig.get(this).isConfigured();
|
||||
if (!server && !AbrpConfig.get(this).isConfigured()) {
|
||||
statusText.setText(R.string.server_not_configured);
|
||||
statusText.setTextColor(COLOR_ERROR);
|
||||
} else if (!server && UploadSettings.from(prefs).abrpUploadEnabled) {
|
||||
statusText.setText(R.string.state_abrp_only);
|
||||
statusText.setTextColor(COLOR_PENDING);
|
||||
} else if (!UploadSettings.from(prefs).abrpUploadEnabled) {
|
||||
statusText.setText(R.string.state_tracking_locally);
|
||||
statusText.setTextColor(COLOR_PENDING);
|
||||
|
||||
@@ -10,7 +10,8 @@ import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* One JSON request to the car server, shared by telemetry, consumption sync and pairing.
|
||||
* One JSON request to the car server, shared by telemetry, consumption sync and pairing (and,
|
||||
* through {@link #send}, the direct ABRP upload).
|
||||
* Blocking, with short timeouts — call it off the main thread.
|
||||
*
|
||||
* The token goes in the Authorization header, never the URL, and is never logged. The answer
|
||||
@@ -48,17 +49,26 @@ final class ServerHttp {
|
||||
* {@code token} may be null for the unauthenticated pairing calls.
|
||||
*/
|
||||
static Response request(String method, String url, String token, JSONObject json) throws Exception {
|
||||
byte[] body = json == null ? null : json.toString().getBytes(StandardCharsets.UTF_8);
|
||||
return send(method, url, token == null ? null : "Bearer " + token, "application/json", body);
|
||||
}
|
||||
|
||||
/**
|
||||
* The request underneath, also used for ABRP ({@link AbrpApi}): {@code authorization} is the
|
||||
* whole header value or null, {@code body} null for none.
|
||||
*/
|
||||
static Response send(String method, String url, String authorization, String contentType, byte[] body)
|
||||
throws Exception {
|
||||
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
|
||||
try {
|
||||
connection.setRequestMethod(method);
|
||||
connection.setConnectTimeout(TIMEOUT_MS);
|
||||
connection.setReadTimeout(TIMEOUT_MS);
|
||||
if (token != null) connection.setRequestProperty("Authorization", "Bearer " + token);
|
||||
if (json != null) {
|
||||
byte[] body = json.toString().getBytes(StandardCharsets.UTF_8);
|
||||
if (authorization != null) connection.setRequestProperty("Authorization", authorization);
|
||||
if (body != null) {
|
||||
connection.setDoOutput(true);
|
||||
connection.setFixedLengthStreamingMode(body.length);
|
||||
connection.setRequestProperty("Content-Type", "application/json");
|
||||
connection.setRequestProperty("Content-Type", contentType);
|
||||
try (OutputStream out = connection.getOutputStream()) { out.write(body); }
|
||||
}
|
||||
int status = connection.getResponseCode();
|
||||
|
||||
@@ -52,6 +52,9 @@ final class TelemetryQueue {
|
||||
for (int i = 0; i < count && !samples.isEmpty(); i++) samples.removeFirst();
|
||||
}
|
||||
|
||||
/** The newest sample, or null when the queue is empty; it stays queued. */
|
||||
synchronized String newest() { return samples.peekLast(); }
|
||||
|
||||
synchronized int size() { return samples.size(); }
|
||||
|
||||
/** Samples lost to the capacity limit since the queue was created. */
|
||||
|
||||
@@ -152,9 +152,12 @@ final class TelemetryUploader {
|
||||
// ---------- Upload ----------
|
||||
|
||||
private void flush() {
|
||||
// ABRP first and on its own: live data must not wait for, or fail with, the server.
|
||||
sendNewestToAbrp();
|
||||
ServerConfig config = ServerConfig.get(context);
|
||||
if (!config.isConfigured()) {
|
||||
Log.i(TAG, "Telemetry not sent: server not configured (" + queue.size() + " queued)");
|
||||
// Kept for the server once one is set up; the queue is bounded.
|
||||
Log.i(TAG, "Telemetry not sent to a server: none configured (" + queue.size() + " queued)");
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < MAX_BATCHES_PER_TICK && queue.size() > 0; i++) {
|
||||
@@ -190,9 +193,53 @@ final class TelemetryUploader {
|
||||
JSONObject body = new JSONObject()
|
||||
.put("installationId", config.installationId)
|
||||
.put("samples", samples);
|
||||
// What the car already delivered to ABRP itself; the server forwards only newer samples.
|
||||
if (lastAbrpUtc > 0) body.put("abrpSentUtc", lastAbrpUtc);
|
||||
return ServerHttp.request("POST", config.url + "/v1/telemetry", config.token, body);
|
||||
}
|
||||
|
||||
// ---------- ABRP, directly ----------
|
||||
|
||||
/** Older than this, a sample is history rather than live state, and ABRP gets nothing. */
|
||||
private static final long ABRP_MAX_AGE_S = 5 * 60;
|
||||
|
||||
/** Time of the newest sample ABRP accepted from the car; 0 before the first. */
|
||||
private volatile long lastAbrpUtc;
|
||||
|
||||
/**
|
||||
* Sends the newest queued sample straight to ABRP when ABRP credentials are set: once per
|
||||
* upload, only if it is newer than the last one sent and still live. It stays queued for
|
||||
* the server either way.
|
||||
*/
|
||||
private void sendNewestToAbrp() {
|
||||
AbrpConfig abrp = AbrpConfig.get(context);
|
||||
if (!abrp.isConfigured()) return;
|
||||
String newest = queue.newest();
|
||||
if (newest == null) return;
|
||||
long utc = utcOf(newest);
|
||||
if (utc <= lastAbrpUtc || System.currentTimeMillis() / 1000 - utc > ABRP_MAX_AGE_S) return;
|
||||
String summary = TelemetryPayload.summarize(newest);
|
||||
try {
|
||||
ServerHttp.Response response = AbrpApi.send(abrp, newest);
|
||||
boolean ok = AbrpApi.accepted(response);
|
||||
if (ok) lastAbrpUtc = utc;
|
||||
// ABRP's answer is not quoted: it can echo the request, credentials included.
|
||||
AbrpUploadService.log().record(new UploadLog.Entry(System.currentTimeMillis(), response.status, ok,
|
||||
ok ? "ABRP: live data sent" : "ABRP rejected: HTTP " + response.status, summary));
|
||||
} catch (Exception e) {
|
||||
AbrpUploadService.log().record(new UploadLog.Entry(System.currentTimeMillis(), 0, false,
|
||||
"ABRP offline: " + e.getClass().getSimpleName(), summary));
|
||||
}
|
||||
}
|
||||
|
||||
static long utcOf(String sampleJson) {
|
||||
try {
|
||||
return new JSONObject(sampleJson).optLong("utc", 0L);
|
||||
} catch (Exception e) {
|
||||
return 0L;
|
||||
}
|
||||
}
|
||||
|
||||
/** ", 2 value(s) dropped by the server" when it left anything out, for the upload log. */
|
||||
static String droppedNote(ServerHttp.Response response) {
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Lightning bolt: the Charging tab. Tinted by @color/nav_tab_foreground. -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M13,2L4,14H11L10,22L20,9H13L13,2Z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Broadcast waves around a dot: the Live data tab. Tinted by @color/nav_tab_foreground. -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M12,10a2,2 0,1 1,0 4a2,2 0,1 1,0 -4z" />
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M8.5,8.5A5,5 0,0 0,8.5 15.5M15.5,8.5A5,5 0,0 1,15.5 15.5M5.6,5.6A9,9 0,0 0,5.6 18.4M18.4,5.6A9,9 0,0 1,18.4 18.4" />
|
||||
</vector>
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Engrenage : onglet Service. Teinté par @color/nav_tab_foreground. -->
|
||||
<!-- Gear: the Settings tab. Tinted by @color/nav_tab_foreground. -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Wi-Fi arcs, for the Wi-Fi page. -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M1,9l2,2c4.97,-4.97 13.03,-4.97 18,0l2,-2C16.93,2.93 7.08,2.93 1,9zM9,17l3,3 3,-3c-1.65,-1.66 -4.34,-1.66 -6,0zM5,13l2,2c2.76,-2.76 7.24,-2.76 10,0l2,-2C15.14,9.14 8.87,9.14 5,13z" />
|
||||
</vector>
|
||||
@@ -8,10 +8,10 @@
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/design_brand_caption" android:textAppearance="@style/Text.EV.Secondary" android:paddingStart="14dp" android:layout_marginBottom="24dp" />
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/tabVehicle" style="@style/Widget.EV.NavTab" android:layout_width="match_parent" android:layout_height="64dp" android:layout_marginBottom="6dp" android:text="@string/tab_vehicle" app:icon="@drawable/ic_tab_vehicle" />
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/tabConsumption" style="@style/Widget.EV.NavTab" android:layout_width="match_parent" android:layout_height="64dp" android:layout_marginBottom="6dp" android:text="@string/tab_consumption" app:icon="@drawable/ic_tab_vehicle" />
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/tabCharging" style="@style/Widget.EV.NavTab" android:layout_width="match_parent" android:layout_height="64dp" android:layout_marginBottom="6dp" android:text="@string/tab_charging" app:icon="@drawable/ic_tab_service" />
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/tabService" style="@style/Widget.EV.NavTab" android:layout_width="match_parent" android:layout_height="64dp" android:layout_marginBottom="6dp" android:text="@string/tab_service" app:icon="@drawable/ic_tab_service" />
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/tabCharging" style="@style/Widget.EV.NavTab" android:layout_width="match_parent" android:layout_height="64dp" android:layout_marginBottom="6dp" android:text="@string/tab_charging" app:icon="@drawable/ic_tab_charging" />
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/tabService" style="@style/Widget.EV.NavTab" android:layout_width="match_parent" android:layout_height="64dp" android:layout_marginBottom="6dp" android:text="@string/tab_service" app:icon="@drawable/ic_tab_live" />
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/tabServer" style="@style/Widget.EV.NavTab" android:layout_width="match_parent" android:layout_height="64dp" android:layout_marginBottom="6dp" android:text="@string/tab_server" app:icon="@drawable/ic_tab_server" />
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/tabWifi" style="@style/Widget.EV.NavTab" android:layout_width="match_parent" android:layout_height="64dp" android:layout_marginBottom="6dp" android:text="@string/tab_wifi" app:icon="@drawable/ic_tab_wifi" />
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/tabSettings" style="@style/Widget.EV.NavTab" android:layout_width="match_parent" android:layout_height="64dp" android:layout_marginBottom="6dp" android:text="@string/tab_settings" app:icon="@drawable/ic_tab_service" />
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/tabLog" style="@style/Widget.EV.NavTab" android:layout_width="match_parent" android:layout_height="64dp" android:layout_marginBottom="6dp" android:text="@string/tab_log" app:icon="@drawable/ic_tab_log" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
@@ -1,25 +1,38 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.evsuite.abrp.StableScrollView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:fillViewport="true" android:clipToPadding="false">
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:padding="24dp" android:baselineAligned="false">
|
||||
<LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:orientation="vertical" android:layout_marginEnd="16dp" android:padding="24dp" android:background="@drawable/bg_glass">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/design_service_controls" android:textAppearance="@style/Text.EV.Eyebrow" />
|
||||
<TextView android:id="@+id/status_text" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@drawable/bg_input_panel" android:padding="16dp" android:layout_marginTop="20dp" android:layout_marginBottom="12dp" android:text="@string/state_stopped" android:textAppearance="@style/Text.EV.Body" />
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/abrp_upload_switch" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:text="@string/label_abrp_upload_enabled" android:textAppearance="@style/Text.EV.Body" android:paddingTop="8dp" android:paddingBottom="8dp" app:useMaterialThemeColors="true" app:thumbTint="@color/switch_thumb" app:trackTint="@color/switch_track" android:textColor="@color/ev_text_primary" />
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/hint_abrp_upload_enabled" android:textAppearance="@style/Text.EV.Secondary" />
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/full_screen_switch" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:text="@string/label_full_screen" android:textAppearance="@style/Text.EV.Body" android:paddingTop="8dp" android:paddingBottom="8dp" app:useMaterialThemeColors="true" app:thumbTint="@color/switch_thumb" app:trackTint="@color/switch_track" android:textColor="@color/ev_text_primary" />
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/hint_full_screen" android:textAppearance="@style/Text.EV.Secondary" />
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/voice_key_switch" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:text="@string/label_voice_key" android:textAppearance="@style/Text.EV.Body" android:paddingTop="8dp" android:paddingBottom="8dp" app:useMaterialThemeColors="true" app:thumbTint="@color/switch_thumb" app:trackTint="@color/switch_track" android:textColor="@color/ev_text_primary" />
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/hint_voice_key" android:textAppearance="@style/Text.EV.Secondary" />
|
||||
<LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:orientation="vertical" android:layout_marginEnd="16dp">
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="24dp" android:background="@drawable/bg_glass">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/design_service_controls" android:textAppearance="@style/Text.EV.Eyebrow" />
|
||||
<TextView android:id="@+id/status_text" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@drawable/bg_input_panel" android:padding="16dp" android:layout_marginTop="20dp" android:layout_marginBottom="12dp" android:text="@string/state_stopped" android:textAppearance="@style/Text.EV.Body" />
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/abrp_upload_switch" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:text="@string/label_abrp_upload_enabled" android:textAppearance="@style/Text.EV.Body" android:paddingTop="8dp" android:paddingBottom="8dp" app:useMaterialThemeColors="true" app:thumbTint="@color/switch_thumb" app:trackTint="@color/switch_track" android:textColor="@color/ev_text_primary" />
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/hint_abrp_upload_enabled" android:textAppearance="@style/Text.EV.Secondary" />
|
||||
</LinearLayout>
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="24dp" android:background="@drawable/bg_glass" android:layout_marginTop="16dp">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/design_abrp_title" android:textAppearance="@style/Text.EV.Eyebrow" />
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="12dp" android:text="@string/hint_abrp_direct" android:textAppearance="@style/Text.EV.Secondary" />
|
||||
<com.google.android.material.textfield.TextInputLayout android:id="@+id/abrp_api_key_layout" style="@style/Widget.EV.Input" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="18dp" android:hint="@string/hint_abrp_api_key" app:endIconMode="password_toggle" app:endIconMinSize="72dp">
|
||||
<com.google.android.material.textfield.TextInputEditText android:id="@+id/abrp_api_key_input" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:inputType="textPassword" android:singleLine="true" android:textSize="20sp" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
<com.google.android.material.textfield.TextInputLayout android:id="@+id/abrp_token_layout" style="@style/Widget.EV.Input" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="18dp" android:hint="@string/hint_abrp_token" app:endIconMode="password_toggle" app:endIconMinSize="72dp">
|
||||
<com.google.android.material.textfield.TextInputEditText android:id="@+id/abrp_token_input" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:inputType="textPassword" android:singleLine="true" android:textSize="20sp" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/abrp_save_button" style="@style/Widget.EV.Button" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/btn_save" android:layout_marginTop="20dp" />
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/abrp_test_button" style="@style/Widget.EV.Button.Outlined" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/btn_test" android:layout_marginTop="8dp" />
|
||||
<TextView android:id="@+id/abrp_status_text" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="12dp" android:textAppearance="@style/Text.EV.Body" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
<LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:orientation="vertical" android:padding="24dp" android:background="@drawable/bg_glass">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/design_service_timing" android:textAppearance="@style/Text.EV.Eyebrow" />
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/label_frequency" android:textAppearance="@style/Text.EV.Body" android:layout_marginTop="20dp" />
|
||||
<Spinner android:id="@+id/interval_spinner" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:layout_marginTop="12dp" android:background="@drawable/bg_input_panel" android:popupBackground="@color/ev_surface_raised" android:contentDescription="@string/label_frequency" android:spinnerMode="dropdown" />
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/hint_frequency" android:textAppearance="@style/Text.EV.Secondary" android:layout_marginTop="12dp" />
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/boost_switch" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:text="@string/label_boost_low_soc" android:textAppearance="@style/Text.EV.Body" android:paddingTop="8dp" android:paddingBottom="8dp" app:useMaterialThemeColors="true" app:thumbTint="@color/switch_thumb" app:trackTint="@color/switch_track" android:textColor="@color/ev_text_primary" />
|
||||
<com.google.android.material.textfield.TextInputLayout android:id="@+id/low_soc_layout" style="@style/Widget.EV.Input" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="12dp" android:hint="@string/label_low_soc_threshold">
|
||||
<com.google.android.material.textfield.TextInputEditText android:id="@+id/low_soc_input" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:inputType="number" android:maxLength="2" android:textSize="20sp" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
<LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:orientation="vertical">
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="24dp" android:background="@drawable/bg_glass">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/design_service_timing" android:textAppearance="@style/Text.EV.Eyebrow" />
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/label_frequency" android:textAppearance="@style/Text.EV.Body" android:layout_marginTop="20dp" />
|
||||
<Spinner android:id="@+id/interval_spinner" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:layout_marginTop="12dp" android:background="@drawable/bg_input_panel" android:popupBackground="@color/ev_surface_raised" android:contentDescription="@string/label_frequency" android:spinnerMode="dropdown" />
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/hint_frequency" android:textAppearance="@style/Text.EV.Secondary" android:layout_marginTop="12dp" />
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/boost_switch" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:text="@string/label_boost_low_soc" android:textAppearance="@style/Text.EV.Body" android:paddingTop="8dp" android:paddingBottom="8dp" app:useMaterialThemeColors="true" app:thumbTint="@color/switch_thumb" app:trackTint="@color/switch_track" android:textColor="@color/ev_text_primary" />
|
||||
<com.google.android.material.textfield.TextInputLayout android:id="@+id/low_soc_layout" style="@style/Widget.EV.Input" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="12dp" android:hint="@string/label_low_soc_threshold">
|
||||
<com.google.android.material.textfield.TextInputEditText android:id="@+id/low_soc_input" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:inputType="number" android:maxLength="2" android:textSize="20sp" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</com.evsuite.abrp.StableScrollView>
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.evsuite.abrp.StableScrollView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:fillViewport="true" android:clipToPadding="false">
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:padding="24dp" android:baselineAligned="false">
|
||||
<LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:orientation="vertical" android:layout_marginEnd="16dp">
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="24dp" android:background="@drawable/bg_glass">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/design_display_title" android:textAppearance="@style/Text.EV.Eyebrow" />
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/full_screen_switch" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:text="@string/label_full_screen" android:textAppearance="@style/Text.EV.Body" android:paddingTop="8dp" android:paddingBottom="8dp" app:useMaterialThemeColors="true" app:thumbTint="@color/switch_thumb" app:trackTint="@color/switch_track" android:textColor="@color/ev_text_primary" />
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/hint_full_screen" android:textAppearance="@style/Text.EV.Secondary" />
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/voice_key_switch" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:text="@string/label_voice_key" android:textAppearance="@style/Text.EV.Body" android:paddingTop="8dp" android:paddingBottom="8dp" app:useMaterialThemeColors="true" app:thumbTint="@color/switch_thumb" app:trackTint="@color/switch_track" android:textColor="@color/ev_text_primary" />
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/hint_voice_key" android:textAppearance="@style/Text.EV.Secondary" />
|
||||
</LinearLayout>
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="24dp" android:background="@drawable/bg_glass" android:layout_marginTop="16dp">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/design_battery_settings_title" android:textAppearance="@style/Text.EV.Eyebrow" />
|
||||
<com.google.android.material.textfield.TextInputLayout android:id="@+id/consumption_soh_layout" style="@style/Widget.Material3.TextInputLayout.OutlinedBox" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="12dp" android:hint="Manual SoH (%)" app:suffixText="%">
|
||||
<com.google.android.material.textfield.TextInputEditText android:id="@+id/consumption_soh_input" android:layout_width="match_parent" android:layout_height="wrap_content" android:inputType="numberDecimal" android:maxLength="5" android:minHeight="72dp"/>
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/consumption_soh_save" style="@style/Widget.EV.Button" android:layout_width="match_parent" android:layout_height="72dp" android:layout_marginTop="8dp" android:text="Save SoH value"/>
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="This manual value is stored on the device and sent to your server with the daily totals." android:textAppearance="@style/Text.EV.Secondary"/>
|
||||
<com.google.android.material.textfield.TextInputLayout android:id="@+id/usable_capacity_layout" style="@style/Widget.Material3.TextInputLayout.OutlinedBox" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="20dp" android:hint="@string/hint_usable_capacity" app:suffixText="kWh">
|
||||
<com.google.android.material.textfield.TextInputEditText android:id="@+id/usable_capacity_input" android:layout_width="match_parent" android:layout_height="wrap_content" android:inputType="numberDecimal" android:maxLength="5" android:minHeight="72dp"/>
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/usable_capacity_save" style="@style/Widget.EV.Button" android:layout_width="match_parent" android:layout_height="72dp" android:layout_marginTop="8dp" android:text="@string/btn_save_capacity"/>
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="@string/helper_usable_capacity" android:textAppearance="@style/Text.EV.Secondary"/>
|
||||
</LinearLayout>
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="24dp" android:background="@drawable/bg_glass" android:layout_marginTop="16dp">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/design_price_title" android:textAppearance="@style/Text.EV.Eyebrow" />
|
||||
<com.google.android.material.textfield.TextInputLayout android:id="@+id/charge_price_layout" style="@style/Widget.Material3.TextInputLayout.OutlinedBox" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="12dp" android:hint="Unit price" app:suffixText="€/kWh">
|
||||
<com.google.android.material.textfield.TextInputEditText android:id="@+id/charge_price_input" android:layout_width="match_parent" android:layout_height="wrap_content" android:inputType="numberDecimal" android:maxLength="10" android:minHeight="72dp"/>
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/charge_price_save" style="@style/Widget.EV.Button" android:layout_width="match_parent" android:layout_height="72dp" android:layout_marginTop="8dp" android:text="Save price"/>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
<LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:orientation="vertical">
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="24dp" android:background="@drawable/bg_glass">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/wifi_card_title" android:textAppearance="@style/Text.EV.Eyebrow" />
|
||||
<TextView android:id="@+id/wifi_ssid" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="—" android:textAppearance="@style/Text.EV.Metric" android:layout_marginTop="20dp" />
|
||||
<TextView android:id="@+id/wifi_state" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="—" android:textAppearance="@style/Text.EV.Secondary" android:layout_marginTop="12dp" />
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/wifi_connect_button" style="@style/Widget.EV.Button" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/wifi_connect_now" android:layout_marginTop="24dp" />
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/wifi_auto_switch" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:text="@string/wifi_auto_label" android:textAppearance="@style/Text.EV.Body" android:paddingTop="8dp" android:paddingBottom="8dp" app:useMaterialThemeColors="true" app:thumbTint="@color/switch_thumb" app:trackTint="@color/switch_track" android:textColor="@color/ev_text_primary" />
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/wifi_auto_hint" android:textAppearance="@style/Text.EV.Secondary" />
|
||||
</LinearLayout>
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="24dp" android:background="@drawable/bg_glass" android:layout_marginTop="16dp">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/wifi_saved_title" android:textAppearance="@style/Text.EV.Eyebrow" />
|
||||
<TextView android:id="@+id/wifi_saved_list" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="20dp" android:textAppearance="@style/Text.EV.Body" android:lineSpacingExtra="16dp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</com.evsuite.abrp.StableScrollView>
|
||||
@@ -1,17 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.evsuite.abrp.StableScrollView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:fillViewport="true" android:clipToPadding="false">
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:padding="24dp" android:baselineAligned="false">
|
||||
<LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1.3" android:orientation="vertical" android:layout_marginEnd="16dp" android:padding="24dp" android:background="@drawable/bg_glass">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/wifi_card_title" android:textAppearance="@style/Text.EV.Eyebrow" />
|
||||
<TextView android:id="@+id/wifi_ssid" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="—" android:textAppearance="@style/Text.EV.Metric" android:layout_marginTop="20dp" />
|
||||
<TextView android:id="@+id/wifi_state" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="—" android:textAppearance="@style/Text.EV.Secondary" android:layout_marginTop="12dp" />
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/wifi_connect_button" style="@style/Widget.EV.Button" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/wifi_connect_now" android:layout_marginTop="24dp" />
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/wifi_auto_switch" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:text="@string/wifi_auto_label" android:textAppearance="@style/Text.EV.Body" android:paddingTop="8dp" android:paddingBottom="8dp" app:useMaterialThemeColors="true" app:thumbTint="@color/switch_thumb" app:trackTint="@color/switch_track" android:textColor="@color/ev_text_primary" />
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/wifi_auto_hint" android:textAppearance="@style/Text.EV.Secondary" />
|
||||
</LinearLayout>
|
||||
<LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:orientation="vertical" android:padding="24dp" android:background="@drawable/bg_glass">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/wifi_saved_title" android:textAppearance="@style/Text.EV.Eyebrow" />
|
||||
<TextView android:id="@+id/wifi_saved_list" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="20dp" android:textAppearance="@style/Text.EV.Body" android:lineSpacingExtra="16dp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</com.evsuite.abrp.StableScrollView>
|
||||
@@ -15,10 +15,10 @@
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:padding="8dp" android:background="@drawable/bg_sidebar">
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/tabVehicle" style="@style/Widget.EV.NavTab.Compact" android:layout_width="0dp" android:layout_height="76dp" android:layout_weight="1" android:text="@string/tab_vehicle" app:icon="@drawable/ic_tab_vehicle"/>
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/tabConsumption" style="@style/Widget.EV.NavTab.Compact" android:layout_width="0dp" android:layout_height="76dp" android:layout_weight="1" android:text="@string/tab_consumption" app:icon="@drawable/ic_tab_vehicle"/>
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/tabCharging" style="@style/Widget.EV.NavTab.Compact" android:layout_width="0dp" android:layout_height="76dp" android:layout_weight="1" android:text="@string/tab_charging" app:icon="@drawable/ic_tab_service"/>
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/tabService" style="@style/Widget.EV.NavTab.Compact" android:layout_width="0dp" android:layout_height="76dp" android:layout_weight="1" android:text="@string/tab_service" app:icon="@drawable/ic_tab_service"/>
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/tabCharging" style="@style/Widget.EV.NavTab.Compact" android:layout_width="0dp" android:layout_height="76dp" android:layout_weight="1" android:text="@string/tab_charging" app:icon="@drawable/ic_tab_charging"/>
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/tabService" style="@style/Widget.EV.NavTab.Compact" android:layout_width="0dp" android:layout_height="76dp" android:layout_weight="1" android:text="@string/tab_service" app:icon="@drawable/ic_tab_live"/>
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/tabServer" style="@style/Widget.EV.NavTab.Compact" android:layout_width="0dp" android:layout_height="76dp" android:layout_weight="1" android:text="@string/tab_server" app:icon="@drawable/ic_tab_server"/>
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/tabWifi" style="@style/Widget.EV.NavTab.Compact" android:layout_width="0dp" android:layout_height="76dp" android:layout_weight="1" android:text="@string/tab_wifi" app:icon="@drawable/ic_tab_wifi"/>
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/tabSettings" style="@style/Widget.EV.NavTab.Compact" android:layout_width="0dp" android:layout_height="76dp" android:layout_weight="1" android:text="@string/tab_settings" app:icon="@drawable/ic_tab_service"/>
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/tabLog" style="@style/Widget.EV.NavTab.Compact" android:layout_width="0dp" android:layout_height="76dp" android:layout_weight="1" android:text="@string/tab_log" app:icon="@drawable/ic_tab_log"/>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
@@ -15,13 +15,6 @@
|
||||
<TextView android:layout_width="0dp" android:layout_columnWeight="1" android:padding="10dp" android:text="Total cost" android:textAppearance="@style/DataLabel"/><TextView android:id="@+id/charge_total_cost" android:layout_width="0dp" android:layout_columnWeight="1" android:padding="10dp" android:text="—" android:textAppearance="@style/DataValue"/>
|
||||
</GridLayout>
|
||||
</LinearLayout>
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="20dp" android:layout_marginTop="16dp" android:background="@drawable/bg_glass">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="CHARGING PRICE" android:textAppearance="@style/Text.EV.Eyebrow"/>
|
||||
<com.google.android.material.textfield.TextInputLayout android:id="@+id/charge_price_layout" style="@style/Widget.Material3.TextInputLayout.OutlinedBox" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="12dp" android:hint="Unit price" app:suffixText="€/kWh">
|
||||
<com.google.android.material.textfield.TextInputEditText android:id="@+id/charge_price_input" android:layout_width="match_parent" android:layout_height="wrap_content" android:inputType="numberDecimal" android:maxLength="10" android:minHeight="72dp"/>
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/charge_price_save" style="@style/Widget.EV.Button" android:layout_width="match_parent" android:layout_height="72dp" android:layout_marginTop="8dp" android:text="Save price"/>
|
||||
</LinearLayout>
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="20dp" android:layout_marginTop="16dp" android:background="@drawable/bg_glass">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="CHARGING CURVE" android:textAppearance="@style/Text.EV.Eyebrow"/>
|
||||
<com.evsuite.abrp.ChargingGraphView android:id="@+id/charge_graph" android:layout_width="match_parent" android:layout_height="280dp" android:layout_marginTop="12dp"/>
|
||||
|
||||
@@ -26,19 +26,6 @@
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/consumption_reset" style="@style/Widget.EV.Button.Outlined" android:layout_width="match_parent" android:layout_height="64dp" android:layout_marginTop="12dp" android:text="Reset selected trip" android:visibility="gone"/>
|
||||
</LinearLayout>
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="12dp" android:text="Last 7/30 days, lifetime and Trip A/B are stored permanently on the device; app and OTA updates do not erase them." android:textAppearance="@style/Text.EV.Secondary"/>
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="20dp" android:layout_marginTop="16dp" android:background="@drawable/bg_glass">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="BATTERY HEALTH (SOH)" android:textAppearance="@style/Text.EV.Eyebrow"/>
|
||||
<com.google.android.material.textfield.TextInputLayout android:id="@+id/consumption_soh_layout" style="@style/Widget.Material3.TextInputLayout.OutlinedBox" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="12dp" android:hint="Manual SoH (%)" app:suffixText="%">
|
||||
<com.google.android.material.textfield.TextInputEditText android:id="@+id/consumption_soh_input" android:layout_width="match_parent" android:layout_height="wrap_content" android:inputType="numberDecimal" android:maxLength="5" android:minHeight="72dp"/>
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/consumption_soh_save" style="@style/Widget.EV.Button" android:layout_width="match_parent" android:layout_height="72dp" android:layout_marginTop="8dp" android:text="Save SoH value"/>
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="This manual value is stored on the device and sent to your server with the daily totals." android:textAppearance="@style/Text.EV.Secondary"/>
|
||||
<com.google.android.material.textfield.TextInputLayout android:id="@+id/usable_capacity_layout" style="@style/Widget.Material3.TextInputLayout.OutlinedBox" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="20dp" android:hint="@string/hint_usable_capacity" app:suffixText="kWh">
|
||||
<com.google.android.material.textfield.TextInputEditText android:id="@+id/usable_capacity_input" android:layout_width="match_parent" android:layout_height="wrap_content" android:inputType="numberDecimal" android:maxLength="5" android:minHeight="72dp"/>
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/usable_capacity_save" style="@style/Widget.EV.Button" android:layout_width="match_parent" android:layout_height="72dp" android:layout_marginTop="8dp" android:text="@string/btn_save_capacity"/>
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="@string/helper_usable_capacity" android:textAppearance="@style/Text.EV.Secondary"/>
|
||||
</LinearLayout>
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="20dp" android:layout_marginTop="16dp" android:background="@drawable/bg_glass">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="DAILY LOG · LAST 4 MONTHS" android:textAppearance="@style/Text.EV.Eyebrow"/>
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/consumption_date_button" style="@style/Widget.EV.Button.Outlined" android:layout_width="match_parent" android:layout_height="56dp" android:layout_marginTop="12dp" android:text="Pick a date"/>
|
||||
|
||||
@@ -6,13 +6,21 @@
|
||||
<TextView android:id="@+id/status_text" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@drawable/bg_input_panel" android:padding="16dp" android:layout_marginTop="20dp" android:layout_marginBottom="12dp" android:text="@string/state_stopped" android:textAppearance="@style/Text.EV.Body" />
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/abrp_upload_switch" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:text="@string/label_abrp_upload_enabled" android:textAppearance="@style/Text.EV.Body" android:paddingTop="8dp" android:paddingBottom="8dp" app:useMaterialThemeColors="true" app:thumbTint="@color/switch_thumb" app:trackTint="@color/switch_track" android:textColor="@color/ev_text_primary" />
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/hint_abrp_upload_enabled" android:textAppearance="@style/Text.EV.Secondary" />
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/full_screen_switch" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:text="@string/label_full_screen" android:textAppearance="@style/Text.EV.Body" android:paddingTop="8dp" android:paddingBottom="8dp" app:useMaterialThemeColors="true" app:thumbTint="@color/switch_thumb" app:trackTint="@color/switch_track" android:textColor="@color/ev_text_primary" />
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/hint_full_screen" android:textAppearance="@style/Text.EV.Secondary" />
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/voice_key_switch" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:text="@string/label_voice_key" android:textAppearance="@style/Text.EV.Body" android:paddingTop="8dp" android:paddingBottom="8dp" app:useMaterialThemeColors="true" app:thumbTint="@color/switch_thumb" app:trackTint="@color/switch_track" android:textColor="@color/ev_text_primary" />
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/hint_voice_key" android:textAppearance="@style/Text.EV.Secondary" />
|
||||
</LinearLayout>
|
||||
<Space android:layout_width="1dp" android:layout_height="20dp" />
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="24dp" android:background="@drawable/bg_glass">
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="24dp" android:background="@drawable/bg_glass" android:layout_marginTop="16dp">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/design_abrp_title" android:textAppearance="@style/Text.EV.Eyebrow" />
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="12dp" android:text="@string/hint_abrp_direct" android:textAppearance="@style/Text.EV.Secondary" />
|
||||
<com.google.android.material.textfield.TextInputLayout android:id="@+id/abrp_api_key_layout" style="@style/Widget.EV.Input" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="18dp" android:hint="@string/hint_abrp_api_key" app:endIconMode="password_toggle" app:endIconMinSize="72dp">
|
||||
<com.google.android.material.textfield.TextInputEditText android:id="@+id/abrp_api_key_input" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:inputType="textPassword" android:singleLine="true" android:textSize="20sp" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
<com.google.android.material.textfield.TextInputLayout android:id="@+id/abrp_token_layout" style="@style/Widget.EV.Input" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="18dp" android:hint="@string/hint_abrp_token" app:endIconMode="password_toggle" app:endIconMinSize="72dp">
|
||||
<com.google.android.material.textfield.TextInputEditText android:id="@+id/abrp_token_input" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:inputType="textPassword" android:singleLine="true" android:textSize="20sp" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/abrp_save_button" style="@style/Widget.EV.Button" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/btn_save" android:layout_marginTop="20dp" />
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/abrp_test_button" style="@style/Widget.EV.Button.Outlined" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/btn_test" android:layout_marginTop="8dp" />
|
||||
<TextView android:id="@+id/abrp_status_text" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="12dp" android:textAppearance="@style/Text.EV.Body" />
|
||||
</LinearLayout>
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="24dp" android:background="@drawable/bg_glass" android:layout_marginTop="16dp">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/design_service_timing" android:textAppearance="@style/Text.EV.Eyebrow" />
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/label_frequency" android:textAppearance="@style/Text.EV.Body" android:layout_marginTop="20dp" />
|
||||
<Spinner android:id="@+id/interval_spinner" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:layout_marginTop="12dp" android:background="@drawable/bg_input_panel" android:popupBackground="@color/ev_surface_raised" android:contentDescription="@string/label_frequency" android:spinnerMode="dropdown" />
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.evsuite.abrp.StableScrollView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:fillViewport="true" android:clipToPadding="false">
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="24dp">
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="24dp" android:background="@drawable/bg_glass">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/design_display_title" android:textAppearance="@style/Text.EV.Eyebrow" />
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/full_screen_switch" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:text="@string/label_full_screen" android:textAppearance="@style/Text.EV.Body" android:paddingTop="8dp" android:paddingBottom="8dp" app:useMaterialThemeColors="true" app:thumbTint="@color/switch_thumb" app:trackTint="@color/switch_track" android:textColor="@color/ev_text_primary" />
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/hint_full_screen" android:textAppearance="@style/Text.EV.Secondary" />
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/voice_key_switch" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:text="@string/label_voice_key" android:textAppearance="@style/Text.EV.Body" android:paddingTop="8dp" android:paddingBottom="8dp" app:useMaterialThemeColors="true" app:thumbTint="@color/switch_thumb" app:trackTint="@color/switch_track" android:textColor="@color/ev_text_primary" />
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/hint_voice_key" android:textAppearance="@style/Text.EV.Secondary" />
|
||||
</LinearLayout>
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="24dp" android:background="@drawable/bg_glass" android:layout_marginTop="16dp">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/design_battery_settings_title" android:textAppearance="@style/Text.EV.Eyebrow" />
|
||||
<com.google.android.material.textfield.TextInputLayout android:id="@+id/consumption_soh_layout" style="@style/Widget.Material3.TextInputLayout.OutlinedBox" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="12dp" android:hint="Manual SoH (%)" app:suffixText="%">
|
||||
<com.google.android.material.textfield.TextInputEditText android:id="@+id/consumption_soh_input" android:layout_width="match_parent" android:layout_height="wrap_content" android:inputType="numberDecimal" android:maxLength="5" android:minHeight="72dp"/>
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/consumption_soh_save" style="@style/Widget.EV.Button" android:layout_width="match_parent" android:layout_height="72dp" android:layout_marginTop="8dp" android:text="Save SoH value"/>
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="This manual value is stored on the device and sent to your server with the daily totals." android:textAppearance="@style/Text.EV.Secondary"/>
|
||||
<com.google.android.material.textfield.TextInputLayout android:id="@+id/usable_capacity_layout" style="@style/Widget.Material3.TextInputLayout.OutlinedBox" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="20dp" android:hint="@string/hint_usable_capacity" app:suffixText="kWh">
|
||||
<com.google.android.material.textfield.TextInputEditText android:id="@+id/usable_capacity_input" android:layout_width="match_parent" android:layout_height="wrap_content" android:inputType="numberDecimal" android:maxLength="5" android:minHeight="72dp"/>
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/usable_capacity_save" style="@style/Widget.EV.Button" android:layout_width="match_parent" android:layout_height="72dp" android:layout_marginTop="8dp" android:text="@string/btn_save_capacity"/>
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="@string/helper_usable_capacity" android:textAppearance="@style/Text.EV.Secondary"/>
|
||||
</LinearLayout>
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="24dp" android:background="@drawable/bg_glass" android:layout_marginTop="16dp">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/design_price_title" android:textAppearance="@style/Text.EV.Eyebrow" />
|
||||
<com.google.android.material.textfield.TextInputLayout android:id="@+id/charge_price_layout" style="@style/Widget.Material3.TextInputLayout.OutlinedBox" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="12dp" android:hint="Unit price" app:suffixText="€/kWh">
|
||||
<com.google.android.material.textfield.TextInputEditText android:id="@+id/charge_price_input" android:layout_width="match_parent" android:layout_height="wrap_content" android:inputType="numberDecimal" android:maxLength="10" android:minHeight="72dp"/>
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/charge_price_save" style="@style/Widget.EV.Button" android:layout_width="match_parent" android:layout_height="72dp" android:layout_marginTop="8dp" android:text="Save price"/>
|
||||
</LinearLayout>
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="24dp" android:background="@drawable/bg_glass" android:layout_marginTop="16dp">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/wifi_card_title" android:textAppearance="@style/Text.EV.Eyebrow" />
|
||||
<TextView android:id="@+id/wifi_ssid" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="—" android:textAppearance="@style/Text.EV.Metric" android:layout_marginTop="20dp" />
|
||||
<TextView android:id="@+id/wifi_state" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="—" android:textAppearance="@style/Text.EV.Secondary" android:layout_marginTop="12dp" />
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/wifi_connect_button" style="@style/Widget.EV.Button" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/wifi_connect_now" android:layout_marginTop="24dp" />
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/wifi_auto_switch" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:text="@string/wifi_auto_label" android:textAppearance="@style/Text.EV.Body" android:paddingTop="8dp" android:paddingBottom="8dp" app:useMaterialThemeColors="true" app:thumbTint="@color/switch_thumb" app:trackTint="@color/switch_track" android:textColor="@color/ev_text_primary" />
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/wifi_auto_hint" android:textAppearance="@style/Text.EV.Secondary" />
|
||||
</LinearLayout>
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="24dp" android:background="@drawable/bg_glass" android:layout_marginTop="16dp">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/wifi_saved_title" android:textAppearance="@style/Text.EV.Eyebrow" />
|
||||
<TextView android:id="@+id/wifi_saved_list" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="20dp" android:textAppearance="@style/Text.EV.Body" android:lineSpacingExtra="16dp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</com.evsuite.abrp.StableScrollView>
|
||||
@@ -1,18 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.evsuite.abrp.StableScrollView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:fillViewport="true" android:clipToPadding="false">
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="24dp">
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="24dp" android:background="@drawable/bg_glass">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/wifi_card_title" android:textAppearance="@style/Text.EV.Eyebrow" />
|
||||
<TextView android:id="@+id/wifi_ssid" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="—" android:textAppearance="@style/Text.EV.Metric" android:layout_marginTop="20dp" />
|
||||
<TextView android:id="@+id/wifi_state" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="—" android:textAppearance="@style/Text.EV.Secondary" android:layout_marginTop="12dp" />
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/wifi_connect_button" style="@style/Widget.EV.Button" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/wifi_connect_now" android:layout_marginTop="24dp" />
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/wifi_auto_switch" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:text="@string/wifi_auto_label" android:textAppearance="@style/Text.EV.Body" android:paddingTop="8dp" android:paddingBottom="8dp" app:useMaterialThemeColors="true" app:thumbTint="@color/switch_thumb" app:trackTint="@color/switch_track" android:textColor="@color/ev_text_primary" />
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/wifi_auto_hint" android:textAppearance="@style/Text.EV.Secondary" />
|
||||
</LinearLayout>
|
||||
<Space android:layout_width="1dp" android:layout_height="20dp" />
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="24dp" android:background="@drawable/bg_glass">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/wifi_saved_title" android:textAppearance="@style/Text.EV.Eyebrow" />
|
||||
<TextView android:id="@+id/wifi_saved_list" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="20dp" android:textAppearance="@style/Text.EV.Body" android:lineSpacingExtra="16dp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</com.evsuite.abrp.StableScrollView>
|
||||
@@ -2,7 +2,7 @@
|
||||
<string name="app_name">Nils\' MG4</string>
|
||||
|
||||
<!-- Main screen -->
|
||||
<string name="tab_service">Upload</string>
|
||||
<string name="tab_service">Live data</string>
|
||||
<string name="tab_vehicle">Overview</string>
|
||||
<string name="tab_consumption">Consumption</string>
|
||||
<string name="tab_charging">Charging</string>
|
||||
@@ -11,17 +11,17 @@
|
||||
<string name="charging_title">Charging</string>
|
||||
<string name="charging_subtitle">Live battery values and SOC/power curve</string>
|
||||
<string name="tab_server">Server</string>
|
||||
<string name="tab_wifi">Wi-Fi</string>
|
||||
<string name="tab_settings">Settings</string>
|
||||
<string name="tab_log">Logs</string>
|
||||
|
||||
<!-- Wi-Fi page -->
|
||||
<string name="wifi_card_title">Wi-Fi Connection</string>
|
||||
<string name="wifi_card_title">WI-FI</string>
|
||||
<string name="wifi_current">Connected network</string>
|
||||
<string name="wifi_state">Status</string>
|
||||
<string name="wifi_connect_now">Connect Now</string>
|
||||
<string name="wifi_auto_label">Auto-connect in the background</string>
|
||||
<string name="wifi_auto_hint">While the upload service is running, it tries to reconnect to saved networks on its own when the connection drops.</string>
|
||||
<string name="wifi_saved_title">Saved Networks</string>
|
||||
<string name="wifi_saved_title">SAVED NETWORKS</string>
|
||||
<string name="wifi_saved_empty">Could not read saved networks</string>
|
||||
<string name="wifi_not_connected">Not connected</string>
|
||||
|
||||
@@ -121,8 +121,19 @@
|
||||
<string name="label_steering_keys">Steering-wheel keys</string>
|
||||
<string name="hint_steering_keys">The last key reports the car sent, newest first. If double-pressing the voice key does not open the app, a photo of this list shows how the car reports it.</string>
|
||||
<string name="steering_keys_empty">No key reports received yet. Press a steering-wheel key.</string>
|
||||
<string name="label_abrp_upload_enabled">Send live telemetry to the server</string>
|
||||
<string name="hint_abrp_upload_enabled">The server stores it and forwards it to ABRP. Turn this off when another app feeds ABRP; consumption and charging tracking keep running.</string>
|
||||
<string name="label_abrp_upload_enabled">Send live data</string>
|
||||
<string name="hint_abrp_upload_enabled">To your server, which stores it, and to ABRP: straight from the car when the ABRP credentials below are set, otherwise forwarded by the server. Consumption and charging tracking keep running when this is off.</string>
|
||||
<string name="hint_abrp_direct">With your ABRP API key and user token here, the car sends live data to ABRP itself, so ABRP keeps getting it while your server is down. The server then forwards only what the car could not send. Leave both empty to let the server forward everything.</string>
|
||||
<string name="hint_abrp_api_key">ABRP API key</string>
|
||||
<string name="hint_abrp_token">ABRP user token</string>
|
||||
<string name="abrp_saved">Saved: the car sends live data to ABRP directly</string>
|
||||
<string name="abrp_cleared">Cleared: the server forwards live data to ABRP</string>
|
||||
<string name="abrp_invalid">Enter both, without spaces, or leave both empty</string>
|
||||
<string name="abrp_testing">Checking with ABRP…</string>
|
||||
<string name="abrp_test_ok">ABRP accepted these credentials</string>
|
||||
<string name="abrp_test_failed">ABRP refused: HTTP %1$d</string>
|
||||
<string name="abrp_test_offline">ABRP not reachable: %1$s</string>
|
||||
<string name="abrp_not_set">Set both first</string>
|
||||
|
||||
|
||||
<!-- Cadence configuration -->
|
||||
@@ -143,6 +154,7 @@
|
||||
<string name="state_stopped">Stopped</string>
|
||||
<string name="state_starting">Starting…</string>
|
||||
<string name="state_running">Running</string>
|
||||
<string name="state_abrp_only">Running — live data to ABRP only, no server set up</string>
|
||||
<string name="state_tracking_locally">Tracking locally — live telemetry off</string>
|
||||
<string name="notif_tracking_locally">Tracking locally — live telemetry off</string>
|
||||
<string name="state_error">In error — last %1$d uploads failed</string>
|
||||
|
||||
@@ -9,10 +9,10 @@
|
||||
<string name="design_overview_subtitle">Data from your car, all in one place.</string>
|
||||
<string name="design_server_title">Where your data goes.</string>
|
||||
<string name="design_server_subtitle">Your own server stores it and passes it on to ABRP.</string>
|
||||
<string name="design_service_title">You focus on the road.</string>
|
||||
<string name="design_service_subtitle">Your data stays up to date in the background.</string>
|
||||
<string name="design_wifi_title">Stay connected.</string>
|
||||
<string name="design_wifi_subtitle">The internet connection for your journey.</string>
|
||||
<string name="design_service_title">Live data.</string>
|
||||
<string name="design_service_subtitle">Where your car\'s live data goes, and how often.</string>
|
||||
<string name="design_settings_title">Settings.</string>
|
||||
<string name="design_settings_subtitle">Display, battery, charging price and Wi-Fi.</string>
|
||||
<string name="design_log_title">Every upload, right here.</string>
|
||||
<string name="design_log_subtitle">Status and details of recent uploads.</string>
|
||||
<string name="design_battery">BATTERY</string>
|
||||
@@ -22,7 +22,11 @@
|
||||
<string name="design_private_title">Your private connection.</string>
|
||||
<string name="design_private_body">The token lets the car write to your server. It is stored encrypted on this device; your ABRP credentials stay on the server.</string>
|
||||
<string name="design_no_data">Values that cannot be read are shown as —.</string>
|
||||
<string name="design_service_controls">UPLOAD CONTROL</string>
|
||||
<string name="design_service_controls">LIVE DATA</string>
|
||||
<string name="design_abrp_title">ABRP, DIRECTLY FROM THE CAR</string>
|
||||
<string name="design_display_title">DISPLAY AND STEERING WHEEL</string>
|
||||
<string name="design_battery_settings_title">BATTERY</string>
|
||||
<string name="design_price_title">CHARGING PRICE</string>
|
||||
<string name="design_service_timing">UPDATE FREQUENCY</string>
|
||||
<string name="design_car_desc">Tire pressure positions on the vehicle</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user