App 2.21.0: Overview style Dashboard, an instrument cluster

Power, speed and battery as gauges and a strip of gear, climate,
clock, usage and battery; Settings picks Tiles or Dashboard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6
This commit is contained in:
2026-10-24 12:21:58 +02:00
co-authored by Claude Opus 5.5
parent 7b557e8372
commit 234a7e1b60
10 changed files with 383 additions and 5 deletions
@@ -0,0 +1,55 @@
package com.evsuite.abrp;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.View;
/**
* The dashboard strip's battery: an outline with a terminal and vertical cells filled to the
* state of charge, in the accent colour. Null draws the empty outline, never an empty battery.
*/
public final class BatteryBarView extends View {
private static final int CELLS = 20;
private final Paint outline = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Paint cell = new Paint(Paint.ANTI_ALIAS_FLAG);
private final RectF rect = new RectF();
private Float soc;
public BatteryBarView(Context context, AttributeSet attrs) {
super(context, attrs);
int accent = AccentColor.resolve(context, R.attr.evAccent);
outline.setStyle(Paint.Style.STROKE);
outline.setColor(accent);
cell.setColor(accent);
}
void setSoc(Float percent) {
soc = percent == null || !Float.isFinite(percent) ? null : Math.max(0f, Math.min(100f, percent));
invalidate();
}
@Override
protected void onDraw(Canvas c) {
super.onDraw(c);
float h = getHeight(), w = getWidth();
float stroke = Math.max(2f, h * 0.06f);
outline.setStrokeWidth(stroke);
float tip = h * 0.12f;
rect.set(stroke / 2f, stroke / 2f, w - tip - stroke / 2f, h - stroke / 2f);
c.drawRoundRect(rect, h * 0.12f, h * 0.12f, outline);
c.drawRect(w - tip, h * 0.32f, w, h * 0.68f, cell);
if (soc == null) return;
float pad = stroke * 1.6f;
float left = rect.left + pad, right = rect.right - pad, top = rect.top + pad, bottom = rect.bottom - pad;
float pitch = (right - left) / CELLS;
int filled = Math.round(soc / 100f * CELLS);
for (int i = 0; i < filled; i++) {
float x = left + i * pitch;
c.drawRect(x, top, x + pitch * 0.6f, bottom, cell);
}
}
}
@@ -0,0 +1,195 @@
package com.evsuite.abrp;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.View;
import java.util.Locale;
/**
* A round instrument for the dashboard Overview: a 240° scale open at the bottom, major and minor
* ticks, numbers, a needle, and up to three lines of text in the middle — the unit, the value as
* digits, and a caption.
*
* Drawn on a Canvas with colours resolved from the accent theme, so it needs no gradient
* drawables (which crash Android 9 with theme colours). A null value draws no needle and a dash:
* an unread value is never shown as zero. The needle eases towards a new value instead of
* jumping, and stops redrawing once it is there.
*/
public final class GaugeView extends View {
private static final float START_DEG = 150f;
private static final float SWEEP_DEG = 240f;
private final Paint arc = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Paint glow = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Paint tick = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Paint warnTick = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Paint number = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Paint unitPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Paint digits = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Paint caption = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Paint needle = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Paint hub = new Paint(Paint.ANTI_ALIAS_FLAG);
private final RectF oval = new RectF();
private float min = 0f, max = 100f, major = 20f;
private int minorPerMajor = 4;
private boolean numbers = true;
private String lowLabel, highLabel;
private Float warnBelow;
private String unit = "", valueText = "—", captionText;
private Float target;
private float shown = Float.NaN;
public GaugeView(Context context, AttributeSet attrs) {
super(context, attrs);
int accent = AccentColor.resolve(context, R.attr.evAccent);
int light = AccentColor.resolve(context, R.attr.evAccentLight);
arc.setStyle(Paint.Style.STROKE);
arc.setStrokeCap(Paint.Cap.ROUND);
arc.setColor(accent);
glow.setStyle(Paint.Style.STROKE);
glow.setStrokeCap(Paint.Cap.ROUND);
glow.setColor(AccentColor.withAlpha(accent, 0x40));
tick.setColor(accent);
tick.setStrokeCap(Paint.Cap.ROUND);
warnTick.setColor(0xFFFFB4AB);
warnTick.setStrokeCap(Paint.Cap.ROUND);
number.setColor(0xFFF4F3EF);
number.setTextAlign(Paint.Align.CENTER);
unitPaint.setColor(0xFFBDC4C6);
unitPaint.setTextAlign(Paint.Align.CENTER);
digits.setColor(light);
digits.setTextAlign(Paint.Align.CENTER);
digits.setFontFeatureSettings("tnum");
caption.setColor(0xFFBDC4C6);
caption.setTextAlign(Paint.Align.CENTER);
caption.setFontFeatureSettings("tnum");
needle.setColor(0xFFF4F3EF);
needle.setStrokeCap(Paint.Cap.ROUND);
hub.setColor(accent);
}
/** The scale: range, spacing of the numbered ticks, and minor ticks between two of them. */
void configure(float min, float max, float major, int minorPerMajor, String unit) {
this.min = min;
this.max = max;
this.major = major;
this.minorPerMajor = minorPerMajor;
this.unit = unit;
invalidate();
}
/** Letters at the two ends (E and F) instead of numbers along the scale. */
void setEndLabels(String low, String high) {
lowLabel = low;
highLabel = high;
numbers = false;
invalidate();
}
/** Ticks below this value drawn in the warning colour (a low battery). */
void setWarnBelow(Float value) {
warnBelow = value;
invalidate();
}
/**
* The needle's value, null when unread, and its digits ("—" then) and caption. Drawing is
* left to the next frame, and only the needle animates.
*/
void setValue(Float value, String valueText, String captionText) {
target = value == null || !Float.isFinite(value) ? null : value;
this.valueText = valueText == null ? "—" : valueText;
this.captionText = captionText;
if (target != null && Float.isNaN(shown)) shown = target;
invalidate();
}
private float angleOf(float v) {
float f = (Math.max(min, Math.min(max, v)) - min) / (max - min);
return START_DEG + SWEEP_DEG * f;
}
@Override
protected void onDraw(Canvas c) {
super.onDraw(c);
float w = getWidth(), h = getHeight();
// Below the centre: the unit, the digits and two caption lines, down to 0.9 r.
float r = Math.min(w / 2f, h / 1.92f) * 0.95f;
if (r <= 0f) return;
float cx = w / 2f, cy = (h - 1.9f * r) / 2f + r;
float stroke = r * 0.035f;
arc.setStrokeWidth(stroke);
glow.setStrokeWidth(stroke * 2.5f);
oval.set(cx - r, cy - r, cx + r, cy + r);
c.drawArc(oval, START_DEG, SWEEP_DEG, false, glow);
c.drawArc(oval, START_DEG, SWEEP_DEG, false, arc);
// Ticks: every minor step; the numbered ones longer and thicker.
number.setTextSize(r * 0.105f);
float minor = major / Math.max(1, minorPerMajor);
int steps = Math.round((max - min) / minor);
for (int i = 0; i <= steps; i++) {
float v = min + i * minor;
boolean isMajor = i % Math.max(1, minorPerMajor) == 0;
double a = Math.toRadians(angleOf(v));
float outer = r * 0.90f, inner = isMajor ? r * 0.76f : r * 0.83f;
Paint p = warnBelow != null && v < warnBelow ? warnTick : tick;
p.setStrokeWidth(isMajor ? r * 0.022f : r * 0.012f);
float cos = (float) Math.cos(a), sin = (float) Math.sin(a);
c.drawLine(cx + outer * cos, cy + outer * sin, cx + inner * cos, cy + inner * sin, p);
if (isMajor && numbers) {
float lr = r * 0.62f;
c.drawText(String.format(Locale.US, "%.0f", v),
cx + lr * cos, cy + lr * sin + number.getTextSize() * 0.36f, number);
}
}
if (!numbers) {
drawEndLabel(c, cx, cy, r, min, lowLabel, warnBelow != null ? warnTick.getColor() : number.getColor());
drawEndLabel(c, cx, cy, r, max, highLabel, number.getColor());
}
// Under the hub, clear of the needle's sweep: unit, digits, caption.
unitPaint.setTextSize(r * 0.095f);
c.drawText(unit, cx, cy + r * 0.25f, unitPaint);
digits.setTextSize(r * 0.22f);
c.drawText(valueText, cx, cy + r * 0.50f, digits);
if (captionText != null) {
caption.setTextSize(r * 0.09f);
float y = cy + r * 0.68f;
for (String line : captionText.split("\n")) {
c.drawText(line, cx, y, caption);
y += caption.getTextSize() * 1.25f;
}
}
if (target != null) {
shown += (target - shown) * 0.25f;
if (Math.abs(target - shown) < (max - min) * 0.001f) shown = target;
double a = Math.toRadians(angleOf(shown));
needle.setStrokeWidth(r * 0.03f);
c.drawLine(cx, cy, cx + r * 0.80f * (float) Math.cos(a), cy + r * 0.80f * (float) Math.sin(a), needle);
c.drawCircle(cx, cy, r * 0.065f, hub);
if (shown != target) postInvalidateOnAnimation();
} else {
c.drawCircle(cx, cy, r * 0.065f, hub);
}
}
private void drawEndLabel(Canvas c, float cx, float cy, float r, float v, String label, int color) {
if (label == null) return;
double a = Math.toRadians(angleOf(v));
float lr = r * 0.62f;
int old = number.getColor();
number.setColor(color);
number.setTextSize(r * 0.16f);
c.drawText(label, cx + lr * (float) Math.cos(a), cy + lr * (float) Math.sin(a) + number.getTextSize() * 0.36f, number);
number.setColor(old);
}
}
@@ -66,6 +66,7 @@ public class MainActivity extends AppCompatActivity {
private View serverPane;
private View logPane;
private View vehiclePane;
private View dashboardPane;
private View consumptionPane;
private View chargingPane;
/** Display, battery, charging price and Wi-Fi. */
@@ -242,15 +243,24 @@ public class MainActivity extends AppCompatActivity {
servicePane = inflatePane(R.layout.pane_service);
serverPane = inflatePane(R.layout.pane_server);
vehiclePane = inflatePane(R.layout.pane_vehicle);
dashboardPane = inflatePane(R.layout.pane_dashboard);
// The Overview page holds both styles; Settings → Overview style picks the one shown.
android.widget.FrameLayout overviewPage = new android.widget.FrameLayout(this);
overviewPage.setLayoutParams(new android.view.ViewGroup.LayoutParams(
android.view.ViewGroup.LayoutParams.MATCH_PARENT,
android.view.ViewGroup.LayoutParams.MATCH_PARENT));
overviewPage.addView(vehiclePane);
overviewPage.addView(dashboardPane);
consumptionPane = inflatePane(R.layout.pane_consumption);
chargingPane = inflatePane(R.layout.pane_charging);
settingsPane = inflatePane(R.layout.pane_settings);
logPane = inflatePane(R.layout.pane_log);
panes = new View[] { vehiclePane, consumptionPane, chargingPane, servicePane, serverPane,
panes = new View[] { overviewPage, consumptionPane, chargingPane, servicePane, serverPane,
settingsPane, logPane };
UpdateLog.noteVersion(this, BuildConfig.VERSION_NAME);
bindVehiclePane();
bindDashboard();
bindConsumptionPane();
bindChargingPane();
bindServerPane();
@@ -1053,6 +1063,7 @@ public class MainActivity extends AppCompatActivity {
voiceKeySwitch.setOnCheckedChangeListener((btn, checked) ->
prefs.edit().putBoolean(UploadSettings.KEY_VOICE_KEY_OPENS, checked).apply());
bindTireUnit();
bindOverviewStyle();
bindAccentColor();
fullScreenSwitch = settingsPane.findViewById(R.id.full_screen_switch);
fullScreenSwitch.setChecked(UploadSettings.fullScreen(prefs));
@@ -1292,9 +1303,12 @@ public class MainActivity extends AppCompatActivity {
showEstimate(vehRangeTrip, vehRangeTripNote, vehUsageTrip, vehUsageTripNote, trip,
trip == null ? "" : getString(R.string.usage_over_distance, trip.distanceKm),
R.string.range_trip_waiting);
RangeEstimator.Estimate average = estimator.average(remaining, monotonicMs);
showEstimate(vehRangeAverage, vehRangeAverageNote, vehUsageAverage, vehUsageAverageNote,
estimator.average(remaining, monotonicMs), getString(R.string.usage_last_5_min),
R.string.range_waiting);
average, getString(R.string.usage_last_5_min), R.string.range_waiting);
dashTripKm = trip == null ? null : trip.distanceKm;
if (dashUsage != null) dashUsage.setText(average == null ? "—"
: String.format(java.util.Locale.US, "%.1f kWh/100 km", average.kwhPer100Km));
}
private void showEstimate(TextView range, TextView rangeNote, TextView usage, TextView usageNote,
@@ -1404,6 +1418,73 @@ public class MainActivity extends AppCompatActivity {
}
}
// ---------- Dashboard (Overview style) ----------
static final String PREF_OVERVIEW_STYLE = "overview_style";
private GaugeView dashPower, dashSpeed, dashBattery;
private BatteryBarView dashBatteryBar;
private TextView dashGear, dashClimate, dashUsage, dashBatteryText;
/** Kilometres since start, from the range estimator; null until it has a distance. */
private Double dashTripKm;
private boolean dashboardStyle() {
return "DASHBOARD".equals(prefs.getString(PREF_OVERVIEW_STYLE, "TILES"));
}
private void applyOverviewStyle() {
boolean dash = dashboardStyle();
dashboardPane.setVisibility(dash ? View.VISIBLE : View.GONE);
vehiclePane.setVisibility(dash ? View.GONE : View.VISIBLE);
}
/** Tiles (the cards) or Dashboard (gauges); both are fed on every reading. */
private void bindOverviewStyle() {
String[] labels = { getString(R.string.overview_style_tiles), getString(R.string.overview_style_dashboard) };
bindChoices(settingsPane.findViewById(R.id.overview_style_choices), labels, dashboardStyle() ? 1 : 0,
position -> {
prefs.edit().putString(PREF_OVERVIEW_STYLE, position == 1 ? "DASHBOARD" : "TILES").apply();
applyOverviewStyle();
});
}
private void bindDashboard() {
dashPower = dashboardPane.findViewById(R.id.dash_power);
dashSpeed = dashboardPane.findViewById(R.id.dash_speed);
dashBattery = dashboardPane.findViewById(R.id.dash_battery);
dashBatteryBar = dashboardPane.findViewById(R.id.dash_battery_bar);
dashGear = dashboardPane.findViewById(R.id.dash_gear);
dashClimate = dashboardPane.findViewById(R.id.dash_climate);
dashUsage = dashboardPane.findViewById(R.id.dash_usage);
dashBatteryText = dashboardPane.findViewById(R.id.dash_battery_text);
dashPower.configure(-50f, 150f, 50f, 5, getString(R.string.dash_unit_kw));
dashSpeed.configure(0f, 180f, 20f, 4, getString(R.string.dash_unit_kmh));
dashBattery.configure(0f, 100f, 25f, 5, getString(R.string.dash_unit_battery));
dashBattery.setEndLabels("E", "F");
dashBattery.setWarnBelow(20f);
applyOverviewStyle();
}
/** The gauges and the strip; every value null-safe ("—", no needle). */
private void showDashboard(EnergySnapshot s) {
if (dashPower == null) return;
Float power = s.getBatteryPowerKw();
dashPower.setValue(power, power == null ? null : String.format(java.util.Locale.US, "%.1f", power), null);
Float speed = s.getSpeedKmh();
String total = getString(R.string.dash_total, fmt(s.getOdometerKm(), "%,.0f km"));
String trip = getString(R.string.dash_trip, dashTripKm == null ? "—"
: String.format(java.util.Locale.US, "%.1f km", dashTripKm));
dashSpeed.setValue(speed, speed == null ? null : String.format(java.util.Locale.US, "%.0f", speed),
total + "\n" + trip);
Float soc = s.getSocPercent();
dashBattery.setValue(soc, soc == null ? null : String.format(java.util.Locale.US, "%.0f %%", soc),
fmt(s.getRangeKm(), "%.0f km"));
dashBatteryBar.setSoc(soc);
dashBatteryText.setText(fmt(soc, "%.0f %%"));
Boolean parked = s.getParked();
dashGear.setText(parked == null ? "—" : parked ? "P" : "D");
dashClimate.setText(fmt(s.getClimate().getDriverTargetCelsius(), "%.0f °C"));
}
private void showVehicle(EnergySnapshot s) {
try {
vehSoc.setText(fmt(s.getSocPercent(), "%.0f %%"));
@@ -1422,6 +1503,7 @@ public class MainActivity extends AppCompatActivity {
vehTireFr.setText(unit.format(s.getTirePressures().getFrontRightKpa()));
vehTireRl.setText(unit.format(s.getTirePressures().getRearLeftKpa()));
vehTireRr.setText(unit.format(s.getTirePressures().getRearRightKpa()));
showDashboard(s);
} catch (Throwable ignored) {
// Leave the last good reading on screen.
}
@@ -24,6 +24,10 @@
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</LinearLayout>
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/label_overview_style" android:textAppearance="@style/Text.EV.Body" android:layout_marginTop="8dp" />
<com.google.android.material.textfield.TextInputLayout style="@style/Widget.EV.Dropdown" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="4dp">
<com.google.android.material.textfield.MaterialAutoCompleteTextView android:id="@+id/overview_style_choices" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:inputType="none" android:textSize="20sp" app:simpleItemLayout="@layout/dropdown_item" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</LinearLayout>
<LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:orientation="vertical" android:layout_marginEnd="16dp">
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- The Overview as an instrument cluster (Settings → Overview style → Dashboard): power, speed
and battery as gauges, and a strip of gear, climate, clock, usage and battery below. -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:paddingStart="24dp" android:paddingEnd="24dp" android:paddingTop="8dp" android:paddingBottom="12dp">
<LinearLayout android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:orientation="horizontal" android:gravity="bottom" android:baselineAligned="false">
<com.evsuite.abrp.GaugeView android:id="@+id/dash_power" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="0.85" android:layout_marginTop="60dp" />
<com.evsuite.abrp.GaugeView android:id="@+id/dash_speed" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1.15" android:layout_marginStart="8dp" android:layout_marginEnd="8dp" />
<com.evsuite.abrp.GaugeView android:id="@+id/dash_battery" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="0.85" android:layout_marginTop="60dp" />
</LinearLayout>
<LinearLayout android:layout_width="match_parent" android:layout_height="76dp" android:layout_marginTop="12dp" android:orientation="horizontal" android:gravity="center_vertical" android:paddingStart="32dp" android:paddingEnd="32dp" android:background="@drawable/bg_glass">
<TextView android:id="@+id/dash_gear" android:layout_width="0dp" android:layout_weight="1" android:layout_height="wrap_content" android:gravity="center" android:text="@string/no_value" android:textColor="?attr/evAccentLight" android:textSize="26sp" android:fontFamily="sans-serif-medium" />
<TextView android:id="@+id/dash_climate" android:layout_width="0dp" android:layout_weight="1" android:layout_height="wrap_content" android:gravity="center" android:text="@string/no_value" android:textColor="@color/ev_text_primary" android:textSize="26sp" android:fontFeatureSettings="tnum" />
<TextClock android:id="@+id/dash_clock" android:layout_width="0dp" android:layout_weight="1.4" android:layout_height="wrap_content" android:gravity="center" android:format24Hour="HH:mm" android:format12Hour="HH:mm" android:textColor="?attr/evAccentLight" android:textSize="38sp" android:fontFamily="sans-serif-light" android:fontFeatureSettings="tnum" />
<TextView android:id="@+id/dash_usage" android:layout_width="0dp" android:layout_weight="1.4" android:layout_height="wrap_content" android:gravity="center" android:text="@string/no_value" android:textColor="@color/ev_text_primary" android:textSize="22sp" android:fontFeatureSettings="tnum" />
<LinearLayout android:layout_width="0dp" android:layout_weight="1.4" android:layout_height="wrap_content" android:orientation="horizontal" android:gravity="center">
<com.evsuite.abrp.BatteryBarView android:id="@+id/dash_battery_bar" android:layout_width="96dp" android:layout_height="32dp" />
<TextView android:id="@+id/dash_battery_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="12dp" android:text="@string/no_value" android:textColor="@color/ev_text_primary" android:textSize="22sp" android:fontFeatureSettings="tnum" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
@@ -15,6 +15,10 @@
<com.google.android.material.textfield.TextInputLayout style="@style/Widget.EV.Dropdown" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="12dp">
<com.google.android.material.textfield.MaterialAutoCompleteTextView android:id="@+id/accent_choices" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:inputType="none" android:textSize="20sp" app:simpleItemLayout="@layout/dropdown_item" />
</com.google.android.material.textfield.TextInputLayout>
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/label_overview_style" android:textAppearance="@style/Text.EV.Body" android:layout_marginTop="8dp" />
<com.google.android.material.textfield.TextInputLayout style="@style/Widget.EV.Dropdown" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="4dp">
<com.google.android.material.textfield.MaterialAutoCompleteTextView android:id="@+id/overview_style_choices" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:inputType="none" android:textSize="20sp" app:simpleItemLayout="@layout/dropdown_item" />
</com.google.android.material.textfield.TextInputLayout>
</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" />
+8
View File
@@ -119,6 +119,14 @@
<string name="label_full_screen">Full screen</string>
<string name="hint_full_screen">Hides the car\'s top and bottom bars while this app is open. Swipe in from the top or bottom edge to show them briefly, e.g. for the climate controls.</string>
<string name="label_overview_style">Overview style</string>
<string name="overview_style_tiles">Tiles</string>
<string name="overview_style_dashboard">Dashboard</string>
<string name="dash_unit_kw">kW</string>
<string name="dash_unit_kmh">km/h</string>
<string name="dash_unit_battery">SOC</string>
<string name="dash_total">TOTAL %1$s</string>
<string name="dash_trip">TRIP %1$s</string>
<string name="label_voice_key">Double-press the voice key to open this app</string>
<string name="hint_voice_key">Press the voice key on the steering wheel twice quickly. The voice assistant may still react to the presses.</string>
<string name="label_abrp_upload_enabled">Send live data</string>