App 2.17.5: Server tab fits; installation ID is read-only

- The "Your private connection" card is gone; its text was out of date
  (the ABRP credentials are no longer on the server).
- Import from file moved next to Save and Test, in one row.
- The installation ID is shown as text instead of an editable field:
  it is derived from the head unit, and changing it by hand only risked
  splitting the history. A config file can still carry an older one
  over, as before.
- The page fits the car's 1778x640 window without scrolling (checked
  on the emulator), and the subtitle no longer says the server passes
  data on to ABRP.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
This commit is contained in:
2026-10-18 20:53:05 +02:00
co-authored by Claude Opus 5.5
parent 5ead905e5f
commit 5a82fe3dc1
5 changed files with 27 additions and 41 deletions
+2 -2
View File
@@ -34,8 +34,8 @@ android {
applicationId "com.stinnesbeck.nilsmg4"
minSdk 27
targetSdk 36
versionCode 93
versionName "2.17.4"
versionCode 94
versionName "2.17.5"
// Nothing about the server ships in the APK: its URL and ingest token are entered in
// the app's Server tab (ServerConfig), and the database is only reachable by the server.
@@ -114,8 +114,10 @@ public class MainActivity extends AppCompatActivity {
private ChargingGraphView chargeGraph;
/** Server page. */
private TextInputLayout serverUrlLayout, serverTokenLayout, installationIdLayout;
private TextInputEditText serverUrlInput, serverTokenInput, installationIdInput;
private TextInputLayout serverUrlLayout, serverTokenLayout;
private TextInputEditText serverUrlInput, serverTokenInput;
/** Shown, not edited: derived from the head unit (ServerConfig.installationIdFor). */
private TextView installationIdText;
private View connectionStatusRow, connectionIndicator;
private TextView connectionStatusText;
@@ -460,19 +462,17 @@ public class MainActivity extends AppCompatActivity {
private void bindServerPane() {
serverUrlLayout = serverPane.findViewById(R.id.server_url_layout);
serverTokenLayout = serverPane.findViewById(R.id.server_token_layout);
installationIdLayout = serverPane.findViewById(R.id.installation_id_layout);
serverUrlInput = serverPane.findViewById(R.id.server_url_input);
serverTokenInput = serverPane.findViewById(R.id.server_token_input);
installationIdInput = serverPane.findViewById(R.id.installation_id_input);
installationIdText = serverPane.findViewById(R.id.installation_id_text);
connectionStatusRow = serverPane.findViewById(R.id.connection_status_row);
connectionIndicator = serverPane.findViewById(R.id.connection_indicator);
connectionStatusText = serverPane.findViewById(R.id.connection_status_text);
showServerConfig();
((TextView) serverPane.findViewById(R.id.import_hint))
.setText(getString(R.string.import_hint, configDirHint()));
serverPane.findViewById(R.id.server_save_button).setOnClickListener(v -> {
if (saveServerConfig(textOf(serverUrlInput), textOf(serverTokenInput), textOf(installationIdInput))) {
if (saveServerConfig(textOf(serverUrlInput), textOf(serverTokenInput),
ServerConfig.get(this).installationId)) {
setConnectionStatus(COLOR_OK, getString(R.string.server_saved));
}
});
@@ -485,7 +485,7 @@ public class MainActivity extends AppCompatActivity {
ServerConfig config = ServerConfig.get(this);
serverUrlInput.setText(config.url);
serverTokenInput.setText(config.token);
installationIdInput.setText(config.installationId);
installationIdText.setText(getString(R.string.installation_id_info, config.installationId));
}
/** Validates, marks the bad fields, and saves only when all three are good. */
@@ -495,9 +495,12 @@ public class MainActivity extends AppCompatActivity {
String id = rawId == null ? "" : rawId.trim().toLowerCase(java.util.Locale.US);
serverUrlLayout.setError(url == null ? getString(R.string.server_err_url) : null);
serverTokenLayout.setError(ServerConfig.isValidToken(token) ? null : getString(R.string.server_err_token));
installationIdLayout.setError(ServerConfig.isValidInstallationId(id)
? null : getString(R.string.server_err_installation_id));
if (url == null || !ServerConfig.isValidToken(token) || !ServerConfig.isValidInstallationId(id)) return false;
// Only an imported config file can bring a different id; the field is not editable.
if (!ServerConfig.isValidInstallationId(id)) {
setConnectionStatus(COLOR_ERROR, getString(R.string.server_err_installation_id));
return false;
}
if (url == null || !ServerConfig.isValidToken(token)) return false;
ServerConfig.save(this, url, token, id);
if (consumptionCloudClient != null) consumptionCloudClient.invalidateHistory();
showServerConfig();
@@ -513,15 +516,11 @@ public class MainActivity extends AppCompatActivity {
private void testConnection() {
String url = ServerConfig.normalizeUrl(textOf(serverUrlInput));
String token = textOf(serverTokenInput);
String id = textOf(installationIdInput).toLowerCase(java.util.Locale.US);
String id = ServerConfig.get(this).installationId;
if (url == null) {
serverUrlLayout.setError(getString(R.string.server_err_url));
return;
}
if (!ServerConfig.isValidInstallationId(id)) {
installationIdLayout.setError(getString(R.string.server_err_installation_id));
return;
}
setConnectionStatus(COLOR_PENDING, getString(R.string.conn_testing));
new Thread(() -> {
String message;
@@ -563,9 +562,7 @@ public class MainActivity extends AppCompatActivity {
return;
}
serverUrlLayout.setError(null);
String typedId = textOf(installationIdInput).toLowerCase(java.util.Locale.US);
String installationId = ServerConfig.isValidInstallationId(typedId)
? typedId : ServerConfig.get(this).installationId;
String installationId = ServerConfig.get(this).installationId;
PhoneQrDialog ui = new PhoneQrDialog(R.string.pair_title, R.string.pair_starting);
final boolean[] cancelled = ui.cancelled;
@@ -973,7 +970,6 @@ public class MainActivity extends AppCompatActivity {
// Show what the file said, so the marked field is the one to fix in the file.
if (config.serverUrl != null) serverUrlInput.setText(config.serverUrl);
if (config.token != null) serverTokenInput.setText(config.token);
if (config.installationId != null) installationIdInput.setText(config.installationId);
return false;
}
}
+7 -13
View File
@@ -1,6 +1,6 @@
<?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:paddingStart="24dp" android:paddingEnd="24dp" android:paddingTop="12dp" android:paddingBottom="12dp">
<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/server_card_title" android:textAppearance="@style/Text.EV.Eyebrow" />
<com.google.android.material.textfield.TextInputLayout android:id="@+id/server_url_layout" style="@style/Widget.EV.Input" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="18dp" android:hint="@string/hint_server_url" app:helperText="@string/helper_server_url">
@@ -11,22 +11,16 @@
<com.google.android.material.textfield.TextInputLayout android:id="@+id/server_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_server_token" app:endIconMode="password_toggle" app:endIconMinSize="72dp" app:helperText="@string/helper_server_token">
<com.google.android.material.textfield.TextInputEditText android:id="@+id/server_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.textfield.TextInputLayout android:id="@+id/installation_id_layout" style="@style/Widget.EV.Input" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="18dp" android:hint="@string/hint_installation_id" app:helperText="@string/helper_installation_id">
<com.google.android.material.textfield.TextInputEditText android:id="@+id/installation_id_input" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:inputType="text" android:singleLine="true" android:textSize="18sp" android:fontFamily="monospace" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton android:id="@+id/server_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/server_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/installation_id_text" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="14dp" android:textAppearance="@style/Text.EV.Secondary" android:textIsSelectable="true" />
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="16dp" android:orientation="horizontal">
<com.google.android.material.button.MaterialButton android:id="@+id/server_save_button" style="@style/Widget.EV.Button" android:layout_width="0dp" android:layout_weight="1" android:layout_height="wrap_content" android:text="@string/btn_save" android:layout_marginEnd="12dp" />
<com.google.android.material.button.MaterialButton android:id="@+id/server_test_button" style="@style/Widget.EV.Button.Outlined" android:layout_width="0dp" android:layout_weight="1" android:layout_height="wrap_content" android:text="@string/btn_test" android:layout_marginEnd="12dp" />
<com.google.android.material.button.MaterialButton android:id="@+id/import_button" style="@style/Widget.EV.Button.Outlined" android:layout_width="0dp" android:layout_weight="1" android:layout_height="wrap_content" android:text="@string/btn_import" />
</LinearLayout>
<LinearLayout android:id="@+id/connection_status_row" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="16dp" android:gravity="center_vertical" android:orientation="horizontal" android:visibility="gone">
<View android:id="@+id/connection_indicator" android:layout_width="10dp" android:layout_height="10dp" android:layout_marginEnd="12dp" android:background="@drawable/circle_indicator" />
<TextView android:id="@+id/connection_status_text" android:layout_width="match_parent" android:layout_height="wrap_content" android:textAppearance="@style/Text.EV.Body" />
</LinearLayout>
</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/design_private_title" android:textAppearance="@style/Text.EV.Title" />
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/design_private_body" android:textAppearance="@style/Text.EV.Secondary" android:layout_marginTop="16dp" android:lineSpacingMultiplier="1.3" />
<com.google.android.material.button.MaterialButton android:id="@+id/import_button" style="@style/Widget.EV.Button.Outlined" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/btn_import" android:layout_marginTop="24dp" />
<TextView android:id="@+id/import_hint" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="12dp" android:textAppearance="@style/Text.EV.Secondary" android:textIsSelectable="true" />
</LinearLayout>
</LinearLayout>
</com.evsuite.abrp.StableScrollView>
+1 -3
View File
@@ -60,6 +60,7 @@
<string name="btn_save">Save</string>
<string name="btn_test">Test connection</string>
<string name="btn_import">Import from file</string>
<string name="installation_id_info">Installation ID %1$s · derived from the head unit; your data is stored under it</string>
<!-- Server page -->
<string name="server_card_title">Car server</string>
@@ -67,8 +68,6 @@
<string name="helper_server_url">For example https://car.k8s.stinnesbeck.com</string>
<string name="hint_server_token">Ingest token</string>
<string name="helper_server_token">The server\'s INGEST_TOKEN, at least 24 characters</string>
<string name="hint_installation_id">Installation ID</string>
<string name="helper_installation_id">Your data is stored under this ID. Keep it when reinstalling.</string>
<string name="server_err_url">Enter an https:// URL</string>
<string name="btn_pair">Pair with phone</string>
<string name="pair_hint">Shows a QR code: scan it with your phone and approve with the approval password. The token below is then filled in for you.</string>
@@ -92,7 +91,6 @@
<string name="server_err_installation_id">Not a valid ID (UUID)</string>
<string name="server_saved">Server settings saved</string>
<string name="server_not_configured">Server not configured — open the Server tab</string>
<string name="import_hint">Put a text file with server_url and token in %1$s (on the head unit or a USB stick in the same folder), then press Import.</string>
<!-- Config file import -->
<string name="import_ok">Config imported and saved — press Test to verify</string>
+1 -3
View File
@@ -8,7 +8,7 @@
<string name="design_overview_title">Your drive at a glance.</string>
<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_server_subtitle">Your own server stores it; the car sends live data to ABRP itself.</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>
@@ -19,8 +19,6 @@
<string name="design_energy">VEHICLE INFO</string>
<string name="design_tires">TIRE PRESSURES</string>
<string name="design_account">SERVER LINK</string>
<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 &#x2014;.</string>
<string name="design_service_controls">LIVE DATA</string>
<string name="design_abrp_title">ABRP, DIRECTLY FROM THE CAR</string>