App 2.3.0: Server tab, live telemetry to the car server
- New Server tab: server URL, ingest token and installation id, with
Save, Test connection (health, then a read-only token check) and the
config-file import that upstream removed (app folder / USB stick first,
document picker as fallback). Settings are kept encrypted and read on
every use, so the service picks up a change without a restart. Each
installation generates its own id; the inherited upstream UUID and
the baked-in CONSUMPTION_API_* build fields are gone.
- Live telemetry restored from before ea8032a (TelemetryPayload,
UploadCadence, UploadSettings, GPS) and sent in batches to
POST /v1/telemetry instead of ABRP; the server forwards to ABRP. Adds
ext_temp, voltage and current. Unsent samples queue in memory (about
8 h) while offline.
- Upload tab: the live-telemetry switch and cadence controls work again;
the dead service and autostart switches are removed.
- Manifest: location permissions and foreground service types.
- Unit tests compile again: 114 (stable) / 136 (unstable) pass. Tests
for the deleted AbrpApi and HeadUnitWeather removed; new tests for
ServerConfig and TelemetryQueue. Lint passes.
Not yet tried on the car.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,8 @@ import android.widget.TextView;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
@@ -39,16 +41,18 @@ 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.tabWifi,
|
||||
R.id.tabLog };
|
||||
{ R.id.tabVehicle, R.id.tabConsumption, R.id.tabCharging, R.id.tabService, R.id.tabServer,
|
||||
R.id.tabWifi, 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_wifi_title, R.string.design_log_title
|
||||
R.string.design_service_title, R.string.design_server_title, R.string.design_wifi_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_wifi_subtitle, R.string.design_log_subtitle
|
||||
R.string.design_service_subtitle, R.string.design_server_subtitle, R.string.design_wifi_subtitle,
|
||||
R.string.design_log_subtitle
|
||||
};
|
||||
|
||||
// Status colours come from the palette, not from the Material swatches: #4CAF50 and
|
||||
@@ -61,13 +65,14 @@ public class MainActivity extends AppCompatActivity {
|
||||
private TextView statusText;
|
||||
private TextView callLogText;
|
||||
private View servicePane;
|
||||
private View serverPane;
|
||||
private View logPane;
|
||||
private View vehiclePane;
|
||||
private View consumptionPane;
|
||||
private View chargingPane;
|
||||
private View wifiPane;
|
||||
|
||||
/** The six pages in navigation order. Parallel to {@link #TAB_IDS}. */
|
||||
/** The pages in navigation order. Parallel to {@link #TAB_IDS}. */
|
||||
private View[] panes;
|
||||
|
||||
/** Wi-Fi page. The helper is the same one the upload service uses on its own tick. */
|
||||
@@ -102,6 +107,28 @@ public class MainActivity extends AppCompatActivity {
|
||||
private ChargeSessionTracker chargeSessionTracker;
|
||||
private ChargingGraphView chargeGraph;
|
||||
|
||||
/** Server page. */
|
||||
private TextInputLayout serverUrlLayout, serverTokenLayout, installationIdLayout;
|
||||
private TextInputEditText serverUrlInput, serverTokenInput, installationIdInput;
|
||||
private View connectionStatusRow, connectionIndicator;
|
||||
private TextView connectionStatusText;
|
||||
|
||||
/** Upload page: live telemetry switch and cadence. */
|
||||
private SwitchMaterial liveTelemetrySwitch, boostSwitch;
|
||||
private android.widget.Spinner intervalSpinner;
|
||||
private TextInputLayout lowSocLayout;
|
||||
private TextInputEditText lowSocInput;
|
||||
|
||||
/**
|
||||
* Picks a config text file. Only reachable on devices that ship a document picker — the
|
||||
* MG4 head unit does not, which is why {@link #startConfigImport()} scans the app's own
|
||||
* folders first and only falls back to this.
|
||||
*/
|
||||
private final ActivityResultLauncher<String[]> configPicker =
|
||||
registerForActivityResult(new ActivityResultContracts.OpenDocument(), uri -> {
|
||||
if (uri != null) importConfig(uri);
|
||||
});
|
||||
|
||||
/**
|
||||
* Inflates a page with no parent, then gives it the MATCH_PARENT/MATCH_PARENT layout
|
||||
* params ViewPager2 requires of its items — inflating detached leaves them null, and
|
||||
@@ -201,17 +228,22 @@ public class MainActivity extends AppCompatActivity {
|
||||
// in activity_main.xml — a recycled page would invalidate these references as
|
||||
// soon as the user swiped away from it.
|
||||
servicePane = inflatePane(R.layout.pane_service);
|
||||
serverPane = inflatePane(R.layout.pane_server);
|
||||
vehiclePane = inflatePane(R.layout.pane_vehicle);
|
||||
consumptionPane = inflatePane(R.layout.pane_consumption);
|
||||
chargingPane = inflatePane(R.layout.pane_charging);
|
||||
wifiPane = inflatePane(R.layout.pane_wifi);
|
||||
logPane = inflatePane(R.layout.pane_log);
|
||||
panes = new View[] { vehiclePane, consumptionPane, chargingPane, servicePane, wifiPane, logPane };
|
||||
panes = new View[] { vehiclePane, consumptionPane, chargingPane, servicePane, serverPane,
|
||||
wifiPane, logPane };
|
||||
|
||||
bindVehiclePane();
|
||||
bindConsumptionPane();
|
||||
bindChargingPane();
|
||||
bindWifiPane();
|
||||
bindServerPane();
|
||||
bindServicePane();
|
||||
requestLocationPermissionIfNeeded();
|
||||
|
||||
statusText = servicePane.findViewById(R.id.status_text);
|
||||
callLogText = logPane.findViewById(R.id.call_log_text);
|
||||
@@ -225,7 +257,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
// a no-op and does not even contain the updater.
|
||||
UpdateHook.checkInBackground(this);
|
||||
|
||||
// The service is always-on: it tracks consumption and syncs to MongoDB cloud.
|
||||
// The service is always-on: it tracks consumption and sends everything to the server.
|
||||
// startForegroundService is idempotent — if the service is already up this
|
||||
// is a no-op aside from delivering a new intent.
|
||||
startForegroundService(new Intent(this, AbrpUploadService.class));
|
||||
@@ -337,6 +369,327 @@ public class MainActivity extends AppCompatActivity {
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- Server page ----------
|
||||
|
||||
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);
|
||||
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))) {
|
||||
setConnectionStatus(COLOR_OK, getString(R.string.server_saved));
|
||||
}
|
||||
});
|
||||
serverPane.findViewById(R.id.server_test_button).setOnClickListener(v -> testConnection());
|
||||
serverPane.findViewById(R.id.import_button).setOnClickListener(v -> startConfigImport());
|
||||
}
|
||||
|
||||
private void showServerConfig() {
|
||||
ServerConfig config = ServerConfig.get(this);
|
||||
serverUrlInput.setText(config.url);
|
||||
serverTokenInput.setText(config.token);
|
||||
installationIdInput.setText(config.installationId);
|
||||
}
|
||||
|
||||
/** Validates, marks the bad fields, and saves only when all three are good. */
|
||||
private boolean saveServerConfig(String rawUrl, String rawToken, String rawId) {
|
||||
String url = ServerConfig.normalizeUrl(rawUrl);
|
||||
String token = rawToken == null ? "" : rawToken.trim();
|
||||
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;
|
||||
ServerConfig.save(this, url, token, id);
|
||||
if (consumptionCloudClient != null) consumptionCloudClient.invalidateHistory();
|
||||
showServerConfig();
|
||||
refreshStatus();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks reachability, then the token, without writing anything: the health endpoint
|
||||
* first, then the car's own history read, which needs the ingest token. Uses what is in
|
||||
* the fields, saved or not, so a typo can be caught before it is stored.
|
||||
*/
|
||||
private void testConnection() {
|
||||
String url = ServerConfig.normalizeUrl(textOf(serverUrlInput));
|
||||
String token = textOf(serverTokenInput);
|
||||
String id = textOf(installationIdInput).toLowerCase(java.util.Locale.US);
|
||||
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;
|
||||
int color;
|
||||
try {
|
||||
int health = httpGet(url + "/v1/health", null);
|
||||
if (health != 200) {
|
||||
message = getString(R.string.conn_err_http, health);
|
||||
color = COLOR_ERROR;
|
||||
} else {
|
||||
int auth = httpGet(url + "/v1/consumption/history/" + id, token);
|
||||
if (auth == 200) { message = getString(R.string.conn_ok); color = COLOR_OK; }
|
||||
else if (auth == 401) { message = getString(R.string.conn_err_auth); color = COLOR_ERROR; }
|
||||
else { message = getString(R.string.conn_err_http, auth); color = COLOR_ERROR; }
|
||||
}
|
||||
} catch (java.net.SocketTimeoutException e) {
|
||||
message = getString(R.string.conn_err_timeout);
|
||||
color = COLOR_ERROR;
|
||||
} catch (Exception e) {
|
||||
message = getString(R.string.conn_err_no_internet);
|
||||
color = COLOR_ERROR;
|
||||
}
|
||||
final String m = message;
|
||||
final int c = color;
|
||||
runOnUiThread(() -> setConnectionStatus(c, m));
|
||||
}, "server-test").start();
|
||||
}
|
||||
|
||||
private static int httpGet(String url, String token) throws java.io.IOException {
|
||||
java.net.HttpURLConnection connection = (java.net.HttpURLConnection) new java.net.URL(url).openConnection();
|
||||
try {
|
||||
connection.setConnectTimeout(8_000);
|
||||
connection.setReadTimeout(8_000);
|
||||
if (token != null) connection.setRequestProperty("Authorization", "Bearer " + token);
|
||||
return connection.getResponseCode();
|
||||
} finally {
|
||||
connection.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
private void setConnectionStatus(int color, String message) {
|
||||
connectionStatusRow.setVisibility(View.VISIBLE);
|
||||
connectionIndicator.getBackground().mutate().setTint(color);
|
||||
connectionStatusText.setText(message);
|
||||
connectionStatusText.setTextColor(color);
|
||||
}
|
||||
|
||||
// ---------- Config import ----------
|
||||
|
||||
/**
|
||||
* The MG4 head unit ships no document picker — SAF answers "FileManagement is no supported
|
||||
* on this device" — so the file is found by scanning instead. getExternalFilesDirs() returns
|
||||
* this app's own folder on internal storage and on every mounted volume including a USB
|
||||
* stick, and those need no storage permission at any API level. The picker stays as the
|
||||
* fallback for phones and tablets, where the user may keep the file anywhere.
|
||||
*/
|
||||
private void startConfigImport() {
|
||||
java.io.File found = findConfigFile();
|
||||
if (found != null) {
|
||||
importConfig(found);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// Any MIME: the MG4 Files app tags .txt inconsistently, so filtering by type hides
|
||||
// the very file the user is trying to pick. They select it by name instead.
|
||||
configPicker.launch(new String[]{"*/*"});
|
||||
} catch (android.content.ActivityNotFoundException e) {
|
||||
setConnectionStatus(COLOR_ERROR, getString(R.string.import_err_not_found, configDirHint()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* First readable file in an app-specific folder that parses as a config. Names are not
|
||||
* filtered: the user copies one file to the folder, whatever they called it.
|
||||
*/
|
||||
private java.io.File findConfigFile() {
|
||||
for (java.io.File dir : getExternalFilesDirs(null)) {
|
||||
if (dir == null) continue;
|
||||
java.io.File[] files = dir.listFiles();
|
||||
if (files == null) continue;
|
||||
for (java.io.File f : files) {
|
||||
if (f.isFile() && f.canRead() && f.length() > 0 && f.length() <= 64 * 1024
|
||||
&& !ConfigImport.parse(readText(f)).isEmpty()) {
|
||||
return f;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** File text, or "" when unreadable — callers treat that as "not a config". */
|
||||
private String readText(java.io.File file) {
|
||||
try (java.io.InputStream in = new java.io.FileInputStream(file)) {
|
||||
return readCapped(in);
|
||||
} catch (Exception e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/** Capped so a wrong file picked by mistake is not slurped whole into memory. */
|
||||
private String readCapped(java.io.InputStream in) throws java.io.IOException {
|
||||
java.io.ByteArrayOutputStream buf = new java.io.ByteArrayOutputStream();
|
||||
byte[] chunk = new byte[4096];
|
||||
int n;
|
||||
while ((n = in.read(chunk)) != -1 && buf.size() < 64 * 1024) buf.write(chunk, 0, n);
|
||||
return buf.toString(java.nio.charset.StandardCharsets.UTF_8.name());
|
||||
}
|
||||
|
||||
/** Where the user should drop the file. */
|
||||
private String configDirHint() {
|
||||
java.io.File[] dirs = getExternalFilesDirs(null);
|
||||
return dirs.length > 0 && dirs[0] != null ? dirs[0].getAbsolutePath() : "";
|
||||
}
|
||||
|
||||
private void importConfig(java.io.File file) {
|
||||
String text = readText(file);
|
||||
if (text.isEmpty()) {
|
||||
setConnectionStatus(COLOR_ERROR, getString(R.string.import_err_read));
|
||||
return;
|
||||
}
|
||||
applyConfig(text);
|
||||
}
|
||||
|
||||
private void importConfig(Uri uri) {
|
||||
String text;
|
||||
try (java.io.InputStream in = getContentResolver().openInputStream(uri)) {
|
||||
if (in == null) throw new java.io.IOException("no stream");
|
||||
text = readCapped(in);
|
||||
} catch (Exception e) {
|
||||
setConnectionStatus(COLOR_ERROR, getString(R.string.import_err_read));
|
||||
return;
|
||||
}
|
||||
applyConfig(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies whatever the file sets; absent keys keep their current value, so a file with
|
||||
* only the token does not wipe the URL. Server settings are validated and saved together;
|
||||
* cadence keys go to the plain prefs the same way the Upload page writes them.
|
||||
*/
|
||||
private void applyConfig(String text) {
|
||||
ConfigImport config = ConfigImport.parse(text);
|
||||
if (config.isEmpty()) {
|
||||
setConnectionStatus(COLOR_ERROR, getString(R.string.import_err_empty));
|
||||
return;
|
||||
}
|
||||
ServerConfig current = ServerConfig.get(this);
|
||||
if (config.serverUrl != null || config.token != null || config.installationId != null) {
|
||||
boolean saved = saveServerConfig(
|
||||
config.serverUrl != null ? config.serverUrl : current.url,
|
||||
config.token != null ? config.token : current.token,
|
||||
config.installationId != null ? config.installationId : current.installationId);
|
||||
if (!saved) {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
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);
|
||||
if (config.lowSocPercent != null) edit.putInt(UploadSettings.KEY_LOW_SOC_PERCENT, config.lowSocPercent);
|
||||
edit.apply();
|
||||
bindCadenceControls();
|
||||
setConnectionStatus(COLOR_OK, getString(R.string.import_ok));
|
||||
refreshStatus();
|
||||
}
|
||||
|
||||
// ---------- Upload page ----------
|
||||
|
||||
private void bindServicePane() {
|
||||
liveTelemetrySwitch = servicePane.findViewById(R.id.abrp_upload_switch);
|
||||
intervalSpinner = servicePane.findViewById(R.id.interval_spinner);
|
||||
boostSwitch = servicePane.findViewById(R.id.boost_switch);
|
||||
lowSocInput = servicePane.findViewById(R.id.low_soc_input);
|
||||
lowSocLayout = servicePane.findViewById(R.id.low_soc_layout);
|
||||
liveTelemetrySwitch.setChecked(UploadSettings.from(prefs).abrpUploadEnabled);
|
||||
liveTelemetrySwitch.setOnCheckedChangeListener((btn, checked) -> {
|
||||
prefs.edit().putBoolean(UploadSettings.KEY_ABRP_UPLOAD_ENABLED, checked).apply();
|
||||
refreshStatus();
|
||||
});
|
||||
bindCadenceControls();
|
||||
}
|
||||
|
||||
/** The service reads these settings on every tick, so a change applies without a restart. */
|
||||
private void bindCadenceControls() {
|
||||
UploadSettings current = UploadSettings.from(prefs);
|
||||
|
||||
String[] labels = new String[UploadSettings.INTERVAL_CHOICES_SEC.length];
|
||||
int selected = 0;
|
||||
for (int i = 0; i < UploadSettings.INTERVAL_CHOICES_SEC.length; i++) {
|
||||
int sec = UploadSettings.INTERVAL_CHOICES_SEC[i];
|
||||
labels[i] = sec >= 60
|
||||
? getResources().getQuantityString(R.plurals.interval_minutes, sec / 60, sec / 60)
|
||||
: getResources().getQuantityString(R.plurals.interval_seconds, sec, sec);
|
||||
if (sec == current.intervalSec) selected = i;
|
||||
}
|
||||
android.widget.ArrayAdapter<String> adapter = new android.widget.ArrayAdapter<>(
|
||||
this, R.layout.spinner_value, labels);
|
||||
adapter.setDropDownViewResource(R.layout.spinner_dropdown);
|
||||
intervalSpinner.setAdapter(adapter);
|
||||
intervalSpinner.setSelection(selected);
|
||||
intervalSpinner.setOnItemSelectedListener(new android.widget.AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(android.widget.AdapterView<?> parent, View view, int position, long id) {
|
||||
prefs.edit().putInt(UploadSettings.KEY_INTERVAL_SEC,
|
||||
UploadSettings.INTERVAL_CHOICES_SEC[position]).apply();
|
||||
}
|
||||
@Override public void onNothingSelected(android.widget.AdapterView<?> parent) { }
|
||||
});
|
||||
|
||||
boostSwitch.setOnCheckedChangeListener(null);
|
||||
boostSwitch.setChecked(current.boostLowSoc);
|
||||
lowSocLayout.setEnabled(current.boostLowSoc);
|
||||
lowSocInput.setEnabled(current.boostLowSoc);
|
||||
boostSwitch.setOnCheckedChangeListener((button, checked) -> {
|
||||
prefs.edit().putBoolean(UploadSettings.KEY_BOOST_LOW_SOC, checked).apply();
|
||||
lowSocLayout.setEnabled(checked);
|
||||
lowSocInput.setEnabled(checked);
|
||||
});
|
||||
|
||||
lowSocInput.setText(String.valueOf(current.lowSocPercent));
|
||||
lowSocInput.setOnFocusChangeListener((view, hasFocus) -> {
|
||||
if (!hasFocus) saveLowSocThreshold();
|
||||
});
|
||||
}
|
||||
|
||||
/** Clamped to 1-99: 0 would never trigger and 100 would boost permanently. */
|
||||
private void saveLowSocThreshold() {
|
||||
CharSequence raw = lowSocInput.getText();
|
||||
int value;
|
||||
try {
|
||||
value = Integer.parseInt(raw == null ? "" : raw.toString().trim());
|
||||
} catch (NumberFormatException e) {
|
||||
value = UploadSettings.DEFAULT_LOW_SOC_PERCENT;
|
||||
}
|
||||
value = Math.max(1, Math.min(99, value));
|
||||
lowSocInput.setText(String.valueOf(value));
|
||||
prefs.edit().putInt(UploadSettings.KEY_LOW_SOC_PERCENT, value).apply();
|
||||
}
|
||||
|
||||
/**
|
||||
* GPS for the telemetry position. On the car the app runs as the system user and holds
|
||||
* it already; this only asks where it does not, such as a phone running the stable build.
|
||||
*/
|
||||
private void requestLocationPermissionIfNeeded() {
|
||||
if (checkSelfPermission(android.Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
requestPermissions(new String[]{android.Manifest.permission.ACCESS_FINE_LOCATION,
|
||||
android.Manifest.permission.ACCESS_COARSE_LOCATION}, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Wi-Fi page ----------
|
||||
|
||||
private void bindWifiPane() {
|
||||
@@ -764,8 +1117,16 @@ public class MainActivity extends AppCompatActivity {
|
||||
break;
|
||||
case RUNNING:
|
||||
case STARTING:
|
||||
statusText.setText("Consumption tracking active");
|
||||
statusText.setTextColor(COLOR_OK);
|
||||
if (!ServerConfig.get(this).isConfigured()) {
|
||||
statusText.setText(R.string.server_not_configured);
|
||||
statusText.setTextColor(COLOR_ERROR);
|
||||
} else if (!UploadSettings.from(prefs).abrpUploadEnabled) {
|
||||
statusText.setText(R.string.state_tracking_locally);
|
||||
statusText.setTextColor(COLOR_PENDING);
|
||||
} else {
|
||||
statusText.setText(R.string.state_running);
|
||||
statusText.setTextColor(COLOR_OK);
|
||||
}
|
||||
break;
|
||||
case STOPPED:
|
||||
default:
|
||||
|
||||
Reference in New Issue
Block a user