feat: add ota and ui changes
This commit is contained in:
@@ -5,6 +5,15 @@ Context for AI agents and new contributors working in this repository.
|
||||
EVABRPUploader is part of **EVSuite**. The workspace `AGENTS.md` and normative
|
||||
workspace `DESIGN.md` apply; this file contains only telemetry-specific additions.
|
||||
|
||||
## App-specific visual direction (2.2.6)
|
||||
|
||||
The user explicitly requested a permanent OLED-black theme, charcoal glass-like cards,
|
||||
muted gold and cyan accents. These app resources intentionally override the shared
|
||||
DESIGN.md palette, DayNight preference and flat-surface rules. Do not overwrite them
|
||||
with the shared token generator. Use static gradients rather than continuous effects;
|
||||
keep large touch targets, readable text and null telemetry shown as a dash.
|
||||
Wide layouts start at 1000dp; narrower windows use a bottom navigation bar.
|
||||
|
||||
## What this is
|
||||
|
||||
An Android Automotive app for the **MG4 (SAIC eh32)** that reads vehicle telemetry and
|
||||
@@ -15,11 +24,8 @@ Fork of Leon Kernan's `ABRP_Uploader`. See `LICENSE.md` — the licence status i
|
||||
|
||||
## Non-negotiables
|
||||
|
||||
1. **Telemetry is read-only.** The explicitly requested SWI69 WinClose integration is
|
||||
the only vehicle-write exception: an opt-in service closes the four windows on the
|
||||
WinClose door callback, requiring P and readable 0 km/h before and during the pulse.
|
||||
It uses WinClose's CarState/VSM interfaces and the shared VehicleWriteGate; no other
|
||||
vehicle settings may be written. Always send STOP when an active pulse exits.
|
||||
1. **Telemetry is read-only.** No vehicle settings may be written. Window automation
|
||||
was removed in 2.2.5 at the user's request, including its service and door listener.
|
||||
2. **A failed read is not a zero.** Getters return `Integer`/`Float`/`Boolean` and `null`
|
||||
means "could not read". Null fields are omitted from the payload. Sending 0 for SOC
|
||||
tells ABRP the battery is empty and wrecks a live route plan.
|
||||
@@ -43,9 +49,6 @@ UploadLog last 20 attempts + derived service state — pure, unit-test
|
||||
SecurePrefs credentials in EncryptedSharedPreferences, migrates plaintext
|
||||
AbrpApi HTTP; credentials in the POST body, never the query string
|
||||
MainActivity configuration, service state, upload log
|
||||
WindowCloseService independent, opt-in foreground service; no ABRP/GPS dependency
|
||||
WinCloseHardware SWI69 adapter derived from the sibling winclose/WindowHardware.kt
|
||||
WindowClosePulse bounded UP/STOP sequence; pure, unit-tested
|
||||
```
|
||||
|
||||
**Put logic in the pure classes.** Anything Android-free can be unit-tested on the JVM;
|
||||
@@ -57,7 +60,7 @@ anything inside the service effectively cannot be tested without a car.
|
||||
|---|---|---|
|
||||
| `main` | everything shared | all builds |
|
||||
| `stable` | `UpdateHook` no-op | stable channel |
|
||||
| `unstable` | updater policy code; trigger suspended during the suite audit | unstable channel |
|
||||
| `unstable` | active GitHub pre-release updater and PackageInstaller result receiver | unstable channel |
|
||||
| `debug` | `VhalProbe` — enumerates the whole VHAL | debug builds only |
|
||||
| `testUnstable` | OTA policy tests | test only |
|
||||
|
||||
|
||||
@@ -4,6 +4,36 @@ All notable changes to this project are documented here. Format follows
|
||||
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow
|
||||
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [2.2.6] - 2026-09-29
|
||||
|
||||
### Changed
|
||||
|
||||
- Redesigned all five pages with an OLED-black background, charcoal translucent panels,
|
||||
muted gold navigation and cyan controls. Static gradients avoid continuous rendering.
|
||||
- Vehicle overview is now the first page. Energy, temperatures and tyre pressures appear
|
||||
at the top, with compact battery, range, power, speed and energy cards below.
|
||||
Unknown data stays a dash.
|
||||
- Added a wide-screen navigation rail and paired cards, with stacked pages and bottom
|
||||
navigation for narrower windows. Headers follow the selected page.
|
||||
- Restyled credentials, transfer controls, frequency picker, Wi-Fi, logs and About;
|
||||
the interface stays dark regardless of the system theme.
|
||||
|
||||
## [2.2.5] - 2026-09-29
|
||||
|
||||
### Removed
|
||||
|
||||
- Window closing: its page, service, door listener, pulse logic, resources and dedicated
|
||||
permissions are removed. Previous opt-in preferences cannot restart the removed service.
|
||||
|
||||
### Fixed
|
||||
|
||||
- OTA checks now report API, network and response errors instead of saying the app is current.
|
||||
- Release selection considers every asset, choosing the newest eligible version even when
|
||||
it appears later in the same release. Drafts and stable releases are excluded.
|
||||
- Concurrent launch/button update checks no longer start duplicate downloads. Installer
|
||||
session handles are closed after commit; failed sessions are abandoned.
|
||||
- Wi-Fi permission refusals are handled explicitly, satisfying the Android lint checks.
|
||||
|
||||
## [2.2.4] - 2026-09-25
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -3,6 +3,25 @@
|
||||
How to ship an update the car will install by itself. Written for whoever does this
|
||||
next — probably me, having forgotten the details.
|
||||
|
||||
## First upgrade from 2.2.3 or older
|
||||
|
||||
Install the platform-signed 2.2.6 APK manually once if the car still runs 2.2.3 or older.
|
||||
Those builds use the old `pm install` path; publishing a newer APK does not repair the
|
||||
installer already running on the car. Builds from 2.2.4 use `PackageInstaller` sessions.
|
||||
Keep the unstable package ID, system shared UID and signing certificate when upgrading.
|
||||
|
||||
The 2.2.5 build removes window automation. It also distinguishes failed release checks
|
||||
from a successful check with no newer release. On 2026-09-29, GitHub still published only
|
||||
2.2.3 at the last live check. The local 2.2.6 build includes these fixes and the redesigned
|
||||
UI; it becomes available to OTA only after publication as a pre-release asset named
|
||||
`FatihsMG4-unstable-2.2.6.apk`.
|
||||
|
||||
The vehicle artifact uses `com.evsuite.abrp.unstable`, `android.uid.system`, and the
|
||||
DriveHub_Kamera platform key. Its certificate SHA-256 must be
|
||||
`c8a2e9bccf597c2fb6dc66bee293fc13f2fc47ec77bc6b2b0d52c11f51192ab8`.
|
||||
The stable build used for a Samsung UI preview is a separate package without OTA or the
|
||||
vehicle's shared UID; do not use that preview APK for the MG4 upgrade.
|
||||
|
||||
## What the updater actually looks for
|
||||
|
||||
`OtaUpdater.check()` reads `https://api.github.com/repos/fatihdonmezdev/MG4ABRP/releases`
|
||||
@@ -30,11 +49,11 @@ The version comes from the **asset filename**, not the git tag. The tag can stay
|
||||
1. **Raise the version.** `app/build.gradle`, `defaultConfig`:
|
||||
|
||||
```gradle
|
||||
versionCode 20 // must increase — Android orders updates by this, not by name
|
||||
versionName "2.2.3"
|
||||
versionCode 23 // must increase — Android orders updates by this, not by name
|
||||
versionName "2.2.6"
|
||||
```
|
||||
|
||||
The unstable flavour appends its own suffix, so `2.2.3` becomes `2.2.3.0-unstable`.
|
||||
The unstable flavour appends its own suffix, so `2.2.6` becomes `2.2.6.0-unstable`.
|
||||
Check what the installed build reports before picking a number: the app's About
|
||||
dialog shows it, and `isNewer` compares numerically segment by segment.
|
||||
|
||||
@@ -57,7 +76,7 @@ The version comes from the **asset filename**, not the git tag. The tag can stay
|
||||
"$BT/apksigner" sign \
|
||||
--key "$KEYS/platform.pk8" \
|
||||
--cert "$KEYS/platform.x509.pem" \
|
||||
--out FatihsMG4-unstable-2.2.3.apk \
|
||||
--out FatihsMG4-unstable-2.2.6.apk \
|
||||
aligned.apk
|
||||
|
||||
rm aligned.apk
|
||||
@@ -69,7 +88,7 @@ The version comes from the **asset filename**, not the git tag. The tag can stay
|
||||
4. **Verify the signer** before uploading. It must say `CN=Android`:
|
||||
|
||||
```bash
|
||||
"$BT/apksigner" verify --print-certs FatihsMG4-unstable-2.2.3.apk | grep DN:
|
||||
"$BT/apksigner" verify --print-certs FatihsMG4-unstable-2.2.6.apk
|
||||
```
|
||||
|
||||
5. **Publish.** On github.com/fatihdonmezdev/MG4ABRP → Releases → Draft a new release:
|
||||
|
||||
@@ -42,9 +42,15 @@ unit and talks to ABRP directly.
|
||||
- [Credits](#credits)
|
||||
|
||||
## Screenshots
|
||||
|
||||
Version 2.2.6 introduces an OLED-black interface with charcoal cards, muted gold and
|
||||
cyan accents, a vehicle overview, and responsive side or bottom navigation. This
|
||||
app-specific theme intentionally overrides the shared EVSuite visual tokens.
|
||||
Tablet UI previews at 1280×720; vehicle readings are unavailable on this device.
|
||||
|
||||
<p align="center">
|
||||
<img src="screenshots/evabrp1.png" width="280" alt="EVABRPUploader screenshot 1">
|
||||
<img src="screenshots/evabrp2.png" width="280" alt="EVABRPUploader screenshot 2">
|
||||
<img src="screenshots/evabrp-2.2.6-overview.png" width="760" alt="OLED vehicle overview with energy and tyre panels above compact metrics">
|
||||
<img src="screenshots/evabrp-2.2.6-abrp.png" width="760" alt="Dark ABRP account configuration">
|
||||
</p>
|
||||
|
||||
---
|
||||
@@ -78,9 +84,8 @@ same rule in time as well as in value: a GPS fix older than `max(5 min, 3 x your
|
||||
interval)` is dropped rather than resent, so a stale fix cannot report the car back at the
|
||||
place it set off from.
|
||||
|
||||
Telemetry only reads the car. An optional SWI69 WinClose integration can close the four
|
||||
windows while parked; this is the only vehicle-write feature. See configuration below
|
||||
and [`SECURITY.md`](SECURITY.md).
|
||||
Telemetry only reads the car. Version 2.2.5 removes window automation entirely; there
|
||||
is no window service, door listener or window command path in the app.
|
||||
|
||||
## Install
|
||||
The MG4 head unit hides Settings and APK install. The known route in:
|
||||
@@ -136,39 +141,6 @@ put them in a text file and tap **Import file** — see [Config file](#config-fi
|
||||
|
||||
## Configuration
|
||||
|
||||
On its own **Pencereler** page — not under Upload Service, since it shares nothing with
|
||||
ABRP — **Kapı açılınca camları kapat** enables the WinClose integration. It is off by
|
||||
default and restarts with the head unit once enabled; no ABRP token, GPS permission,
|
||||
speed threshold or driving timer is needed.
|
||||
|
||||
The integration is adapted from the sibling WinClose `WindowHardware.kt`: SWI69
|
||||
`CarStateClient` door callback (transaction 6, value 0) triggers the VSM's
|
||||
`setVehicleWindowStatus` on all four areas. WinClose describes this as the driver-door
|
||||
signal; passenger-door coverage has not been established. No alternative door-property
|
||||
source is used. The car must be in P with readable speed exactly 0 km/h. Comfort windows
|
||||
receive UP (1) every 120 ms for five seconds, then STOP (0). P and speed are checked
|
||||
during the pulse too. Cancellation, a failed command, or leaving P/stationary state ends
|
||||
the pulse and attempts STOP on all windows. Duplicate door events and openings during
|
||||
an active pulse do not extend it, and WinClose's 60-second cooldown means reaching back
|
||||
into the car does not run a second pulse. A partial wake lock is held across the pulse,
|
||||
because the close fires exactly as the head unit is heading for suspend.
|
||||
|
||||
The feature is SWI69-only and needs the platform signature: the service stops itself on
|
||||
any other known generation, and the switch is disabled when the process does not hold
|
||||
system uid (only the unstable flavor declares `android.uid.system`).
|
||||
|
||||
**No cancellation is possible on a Comfort trim.** Its window motors carry no position
|
||||
sensor, so nothing can observe you working the switch, and there is no way to abort a
|
||||
pulse once it starts. Verify the motors' pinch behaviour on a parked car before relying
|
||||
on this — the windows travel while the driver's door is open.
|
||||
|
||||
No position sensor is assumed: a completed command is not confirmation that the windows
|
||||
closed. The existing WinClose pulse duration is the starting value, not a manufacturer
|
||||
guarantee. Vehicle testing of timing, permissions and motor behavior is still required.
|
||||
The APK keeps its existing application IDs and signing configuration (the unstable flavor
|
||||
already uses `android.uid.system`). WinClose's source license is unspecified; see its
|
||||
README before redistribution.
|
||||
|
||||
| Setting | Default | Notes |
|
||||
|---|---|---|
|
||||
| Upload frequency | 60 s | 15 / 30 / 60 / 120 / 300 s. Applies while driving |
|
||||
|
||||
+1
-6
@@ -15,9 +15,7 @@ was moving. A proof of concept helps; a working exploit is not required.
|
||||
|
||||
- Anything that lets another app on the head unit read the stored ABRP credentials.
|
||||
- Anything that gets attacker-controlled code installed through the unstable updater.
|
||||
- Any vehicle write outside the explicitly enabled SWI69 WinClose feature, or a window
|
||||
close that bypasses P/readable-zero-speed checks, exceeds its five-second pulse, or
|
||||
fails to attempt STOP on every window after interruption or failure.
|
||||
- Any vehicle-setting write. The app is read-only; window automation was removed in 2.2.5.
|
||||
- Telemetry that leaks credentials into logs, URLs or crash reports.
|
||||
|
||||
## What is not in scope
|
||||
@@ -36,6 +34,3 @@ was moving. A proof of concept helps; a working exploit is not required.
|
||||
checks fail closed. The stable channel does not contain the updater at all.
|
||||
- The VHAL probe helpers, which enumerate the whole vehicle abstraction layer, exist only
|
||||
in debug builds.
|
||||
- Window closing uses WinClose's SWI69 CarState callback and VSM commands. Its service is
|
||||
not exported, has its own opt-in switch, and holds no wake lock or alarm. It does not
|
||||
assert that a sensorless window has physically closed or that anti-pinch is available.
|
||||
|
||||
+2
-2
@@ -34,8 +34,8 @@ android {
|
||||
applicationId "com.evsuite.abrp"
|
||||
minSdk 27
|
||||
targetSdk 36
|
||||
versionCode 21
|
||||
versionName "2.2.4"
|
||||
versionCode 23
|
||||
versionName "2.2.6"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
@@ -5,12 +5,6 @@
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<!-- Optional local window automation; independent of the location/upload service. -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||
<!-- The window close fires exactly as the driver leaves and the head unit heads for
|
||||
suspend. Without a wake lock held across the pulse, the unit can sleep partway
|
||||
through and leave the glass halfway up with no STOP ever sent. -->
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<!-- Required from API 33 to post the foreground-service notification, which is the
|
||||
only status UI the service has. Without it the notification is silently dropped. -->
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
@@ -72,15 +66,6 @@
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="location" />
|
||||
|
||||
<service
|
||||
android:name=".WindowCloseService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="specialUse">
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
android:value="Local MG4 parked front-door monitoring and bounded window closing" />
|
||||
</service>
|
||||
|
||||
<!-- The head unit resumes far more often than it cold-boots, and announces the
|
||||
resume with QUICKBOOT_POWERON. Listening only for BOOT_COMPLETED is why
|
||||
uploading did not start with the car. MY_PACKAGE_REPLACED covers the other
|
||||
|
||||
@@ -39,9 +39,6 @@ public class BootReceiver extends BroadcastReceiver {
|
||||
String action = intent.getAction();
|
||||
if (action == null || !START_ACTIONS.contains(action)) return;
|
||||
|
||||
// Window closing has its own switch and never requires ABRP credentials.
|
||||
WindowCloseService.startIfEnabled(context);
|
||||
|
||||
SharedPreferences prefs = context.getSharedPreferences("abrp_prefs", Context.MODE_PRIVATE);
|
||||
boolean enabled = prefs.getBoolean("service_enabled", false);
|
||||
boolean autostart = prefs.getBoolean(UploadSettings.KEY_AUTOSTART, UploadSettings.DEFAULT_AUTOSTART);
|
||||
|
||||
@@ -12,7 +12,6 @@ import android.view.View;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.Button;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
@@ -21,6 +20,7 @@ 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;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
@@ -44,17 +44,26 @@ public class MainActivity extends AppCompatActivity {
|
||||
static final String WIFI_AUTO_KEY = "wifi_auto_connect";
|
||||
private static final String REPOSITORY_URL = "https://github.com/fatihdonmezdev/MG4ABRP";
|
||||
|
||||
/** Top-bar tabs, in page order. Parallel to {@link #panes}. */
|
||||
/** Navigation destinations in page order. Parallel to {@link #panes}. */
|
||||
private static final int[] TAB_IDS =
|
||||
{ R.id.tabAbrp, R.id.tabService, R.id.tabVehicle, R.id.tabWindows, R.id.tabWifi,
|
||||
{ R.id.tabVehicle, R.id.tabAbrp, R.id.tabService, R.id.tabWifi,
|
||||
R.id.tabLog };
|
||||
|
||||
private static final int[] PAGE_TITLES = {
|
||||
R.string.design_overview_title, R.string.design_abrp_title,
|
||||
R.string.design_service_title, R.string.design_wifi_title, R.string.design_log_title
|
||||
};
|
||||
private static final int[] PAGE_SUBTITLES = {
|
||||
R.string.design_overview_subtitle, R.string.design_abrp_subtitle,
|
||||
R.string.design_service_subtitle, R.string.design_wifi_subtitle, R.string.design_log_subtitle
|
||||
};
|
||||
|
||||
// Status colours come from the palette, not from the Material swatches: #4CAF50 and
|
||||
// #F44336 sit around 4:1 on this background, which disappears behind a sunlit
|
||||
// reflection. The palette entries are the lightened variants (7:1 and above).
|
||||
private static final int COLOR_OK = 0xFF8FE6A6;
|
||||
private static final int COLOR_ERROR = 0xFFFF9A9A;
|
||||
private static final int COLOR_PENDING = 0xFFC6CFD8;
|
||||
private static final int COLOR_OK = 0xFFA4DCC1;
|
||||
private static final int COLOR_ERROR = 0xFFFFB4AB;
|
||||
private static final int COLOR_PENDING = 0xFFBDC4C6;
|
||||
|
||||
private TextInputLayout apiKeyLayout;
|
||||
private TextInputEditText apiKeyInput;
|
||||
@@ -62,10 +71,6 @@ public class MainActivity extends AppCompatActivity {
|
||||
private TextInputEditText tokenInput;
|
||||
private SwitchMaterial serviceSwitch;
|
||||
private SwitchMaterial autostartSwitch;
|
||||
private SwitchMaterial windowCloseSwitch;
|
||||
private CompoundButton.OnCheckedChangeListener windowCloseListener;
|
||||
private TextView windowCloseStatus;
|
||||
private TextView windowCloseLog;
|
||||
private TextView statusText;
|
||||
private Button testButton;
|
||||
private View connectionStatusRow;
|
||||
@@ -80,10 +85,9 @@ public class MainActivity extends AppCompatActivity {
|
||||
private View servicePane;
|
||||
private View logPane;
|
||||
private View vehiclePane;
|
||||
private View windowsPane;
|
||||
private View wifiPane;
|
||||
|
||||
/** The six pages, in the order the top bar lists them. Parallel to {@link #TAB_IDS}. */
|
||||
/** The five 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. */
|
||||
@@ -117,7 +121,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
/**
|
||||
* Wires the top bar to the pager: every tab is a page, and every page is a tab.
|
||||
* Wires the navigation to the pager: every tab is a page, and every page is a tab.
|
||||
*
|
||||
* Each page has its own view type, so the pager asks for it once and keeps it: these
|
||||
* views are the activity's own, held in {@link #panes}, not rows to be recycled.
|
||||
@@ -140,10 +144,8 @@ public class MainActivity extends AppCompatActivity {
|
||||
// state, exactly as they did when they were siblings in the layout.
|
||||
}
|
||||
});
|
||||
// All of them stay alive. They are cheap, and the service switch, the call log, the
|
||||
// window-close status and the vehicle readings are refreshed by the tick whether or
|
||||
// not their page is on screen — refreshWindowClose in particular dereferences views
|
||||
// the pager would otherwise be free to detach.
|
||||
// Keep all pages alive: the service switch, log and vehicle readings are refreshed
|
||||
// by the tick even when their page is off screen.
|
||||
pager.setOffscreenPageLimit(panes.length - 1);
|
||||
pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
|
||||
@Override public void onPageSelected(int position) { markCurrentPage(position); }
|
||||
@@ -159,7 +161,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the top-bar tab of the page on screen.
|
||||
* Marks the destination and labels the page on screen.
|
||||
*
|
||||
* Only isSelected is set: fill, text, icon and stroke come from the
|
||||
* res/color/nav_tab_*.xml selectors applied by the Widget.EV.NavTab style. isSelected
|
||||
@@ -169,6 +171,8 @@ public class MainActivity extends AppCompatActivity {
|
||||
for (int i = 0; i < TAB_IDS.length; i++) {
|
||||
findViewById(TAB_IDS[i]).setSelected(i == position);
|
||||
}
|
||||
((TextView) findViewById(R.id.header_title)).setText(PAGE_TITLES[position]);
|
||||
((TextView) findViewById(R.id.header_subtitle)).setText(PAGE_SUBTITLES[position]);
|
||||
}
|
||||
|
||||
/** Refreshes state + call log while the screen is visible. */
|
||||
@@ -179,7 +183,6 @@ public class MainActivity extends AppCompatActivity {
|
||||
refreshCallLog();
|
||||
refreshVehicle();
|
||||
refreshWifi();
|
||||
refreshWindowClose();
|
||||
uiHandler.postDelayed(this, 2_000L);
|
||||
}
|
||||
};
|
||||
@@ -200,13 +203,14 @@ public class MainActivity extends AppCompatActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
getDelegate().setLocalNightMode(AppCompatDelegate.MODE_NIGHT_YES);
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_main);
|
||||
|
||||
prefs = getSharedPreferences("abrp_prefs", MODE_PRIVATE);
|
||||
securePrefs = SecurePrefs.get(this);
|
||||
|
||||
// The three pages are inflated here, once, and handed to the pager as fixed,
|
||||
// The pages are inflated here, once, and handed to the pager as fixed,
|
||||
// non-recycled items. That is what lets every widget below be looked up now and
|
||||
// held for the life of the activity, the way it was when the panes were siblings
|
||||
// in activity_main.xml — a recycled page would invalidate these references as
|
||||
@@ -214,14 +218,12 @@ public class MainActivity extends AppCompatActivity {
|
||||
abrpPane = inflatePane(R.layout.pane_abrp);
|
||||
servicePane = inflatePane(R.layout.pane_service);
|
||||
vehiclePane = inflatePane(R.layout.pane_vehicle);
|
||||
windowsPane = inflatePane(R.layout.pane_windows);
|
||||
wifiPane = inflatePane(R.layout.pane_wifi);
|
||||
logPane = inflatePane(R.layout.pane_log);
|
||||
panes = new View[] { abrpPane, servicePane, vehiclePane, windowsPane, wifiPane, logPane };
|
||||
panes = new View[] { vehiclePane, abrpPane, servicePane, wifiPane, logPane };
|
||||
|
||||
bindVehiclePane();
|
||||
bindWifiPane();
|
||||
bindWindowClose();
|
||||
|
||||
apiKeyLayout = abrpPane.findViewById(R.id.api_key_layout);
|
||||
apiKeyInput = abrpPane.findViewById(R.id.api_key_input);
|
||||
@@ -327,51 +329,13 @@ public class MainActivity extends AppCompatActivity {
|
||||
dialog.show();
|
||||
if (dialog.getWindow() != null) {
|
||||
dialog.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
|
||||
android.util.DisplayMetrics metrics = getResources().getDisplayMetrics();
|
||||
int width = Math.min((int) (760 * metrics.density),
|
||||
metrics.widthPixels - (int) (32 * metrics.density));
|
||||
dialog.getWindow().setLayout(width, android.view.ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
}
|
||||
}
|
||||
|
||||
private void bindWindowClose() {
|
||||
windowCloseSwitch = windowsPane.findViewById(R.id.window_close_switch);
|
||||
windowCloseStatus = windowsPane.findViewById(R.id.window_close_status);
|
||||
windowCloseLog = windowsPane.findViewById(R.id.window_close_log);
|
||||
windowCloseListener = (button, enabled) -> {
|
||||
prefs.edit().putBoolean(WindowCloseService.KEY_ENABLED, enabled).apply();
|
||||
if (enabled) WindowCloseService.startIfEnabled(this);
|
||||
else stopService(new Intent(this, WindowCloseService.class));
|
||||
refreshWindowClose();
|
||||
};
|
||||
windowCloseSwitch.setChecked(prefs.getBoolean(WindowCloseService.KEY_ENABLED, false));
|
||||
windowCloseSwitch.setOnCheckedChangeListener(windowCloseListener);
|
||||
// A build that cannot reach the vehicle service would accept the toggle and then fail
|
||||
// every command, which reads as a broken feature rather than an inapplicable one.
|
||||
if (!WindowCloseService.canWriteToVehicle()) windowCloseSwitch.setEnabled(false);
|
||||
WindowCloseService.startIfEnabled(this);
|
||||
refreshWindowClose();
|
||||
}
|
||||
|
||||
private void refreshWindowClose() {
|
||||
if (windowCloseSwitch == null) return;
|
||||
boolean enabled = prefs.getBoolean(WindowCloseService.KEY_ENABLED, false);
|
||||
if (windowCloseSwitch.isChecked() != enabled) {
|
||||
// Detached first: setChecked re-enters the listener, which would write the pref
|
||||
// and restart the service on every one of these two-second refreshes.
|
||||
windowCloseSwitch.setOnCheckedChangeListener(null);
|
||||
windowCloseSwitch.setChecked(enabled);
|
||||
windowCloseSwitch.setOnCheckedChangeListener(windowCloseListener);
|
||||
}
|
||||
FirmwareInfo.Gen generation = FirmwareInfo.INSTANCE.getGeneration();
|
||||
if (!WindowCloseService.canWriteToVehicle()) {
|
||||
windowCloseStatus.setText(R.string.window_close_no_system_uid);
|
||||
} else if (!enabled) {
|
||||
windowCloseStatus.setText(R.string.window_close_stopped);
|
||||
} else if (generation != FirmwareInfo.Gen.UNKNOWN && generation != FirmwareInfo.Gen.SWI69) {
|
||||
windowCloseStatus.setText(R.string.window_close_unsupported);
|
||||
} else {
|
||||
windowCloseStatus.setText(WindowCloseService.statusResource());
|
||||
}
|
||||
windowCloseLog.setText(WindowCloseService.eventLog());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
@@ -638,8 +602,8 @@ public class MainActivity extends AppCompatActivity {
|
||||
if (sec == current.intervalSec) selected = i;
|
||||
}
|
||||
ArrayAdapter<String> adapter = new ArrayAdapter<>(
|
||||
this, android.R.layout.simple_spinner_item, labels);
|
||||
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
this, R.layout.spinner_value, labels);
|
||||
adapter.setDropDownViewResource(R.layout.spinner_dropdown);
|
||||
intervalSpinner.setAdapter(adapter);
|
||||
intervalSpinner.setSelection(selected);
|
||||
intervalSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@@ -690,7 +654,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
* Checks for a newer build, and installs one if it is there.
|
||||
*
|
||||
* The button is disabled for the duration rather than guarded by a flag: the whole
|
||||
* sequence — API call, download, {@code pm install} — runs on a background thread, and
|
||||
* sequence — API call, download, installer staging — runs on a background thread, and
|
||||
* a second press would start a second download of the same APK. It comes back on when
|
||||
* the result lands, whatever that result is.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.RadialGradient;
|
||||
import android.graphics.Shader;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
|
||||
/** Static, low-opacity light behind the glass panels; no blur passes or animation loop. */
|
||||
public final class PremiumBackdropView extends View {
|
||||
private final Paint cyan = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Paint gold = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
|
||||
public PremiumBackdropView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
|
||||
}
|
||||
|
||||
@Override protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||
float radius = Math.max(1, Math.max(w, h) * 0.65f);
|
||||
cyan.setShader(new RadialGradient(w * 0.70f, 0, radius,
|
||||
new int[]{0x2419565B, 0x00000000}, null, Shader.TileMode.CLAMP));
|
||||
gold.setShader(new RadialGradient(w, h, radius * 0.65f,
|
||||
new int[]{0x1C8B693A, 0x00000000}, null, Shader.TileMode.CLAMP));
|
||||
}
|
||||
|
||||
@Override protected void onDraw(Canvas canvas) {
|
||||
canvas.drawRect(0, 0, getWidth(), getHeight(), cyan);
|
||||
canvas.drawRect(0, 0, getWidth(), getHeight(), gold);
|
||||
}
|
||||
}
|
||||
@@ -78,6 +78,8 @@ final class WifiAutoConnect {
|
||||
String name = c.SSID.replace("\"", "").trim();
|
||||
if (!name.isEmpty() && !names.contains(name)) names.add(name);
|
||||
}
|
||||
} catch (SecurityException e) {
|
||||
Log.w(TAG, "Saved networks unavailable: permission denied");
|
||||
} catch (Throwable t) {
|
||||
Log.w(TAG, "getConfiguredNetworks refused: " + t.getMessage());
|
||||
}
|
||||
@@ -127,6 +129,9 @@ final class WifiAutoConnect {
|
||||
}
|
||||
}
|
||||
return Result.REFUSED;
|
||||
} catch (SecurityException e) {
|
||||
Log.w(TAG, "Wi-Fi connection refused: permission denied");
|
||||
return Result.REFUSED;
|
||||
} catch (Throwable t) {
|
||||
Log.w(TAG, "connect attempt failed: " + t.getMessage());
|
||||
return Result.REFUSED;
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
/** WinClose's TX_DOOR_SENSOR=6: zero is open; suppress repeated identical callbacks. */
|
||||
final class WinCloseDoorSignal {
|
||||
private int previous = -1;
|
||||
|
||||
boolean opening(int raw) {
|
||||
if (raw < 0) return false;
|
||||
boolean opening = raw == 0 && previous != 0;
|
||||
previous = raw;
|
||||
return opening;
|
||||
}
|
||||
|
||||
/**
|
||||
* Records a reading that can never count as an opening.
|
||||
*
|
||||
* A fresh listener registration replays the door's existing state. Treating that replay
|
||||
* as an edge would close the windows on a driver who is halfway out when the binder
|
||||
* reconnects — and the binder reconnects every five seconds while it is down.
|
||||
*/
|
||||
void seed(int raw) {
|
||||
if (raw >= 0) previous = raw;
|
||||
}
|
||||
|
||||
void reset() { previous = -1; }
|
||||
}
|
||||
@@ -1,221 +0,0 @@
|
||||
package com.evsuite.abrp
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Binder
|
||||
import android.os.Handler
|
||||
import android.os.HandlerThread
|
||||
import android.os.IBinder
|
||||
import android.os.Parcel
|
||||
import android.os.SystemClock
|
||||
import com.evsuite.hardware.EVHardware
|
||||
import com.evsuite.hardware.FirmwareInfo
|
||||
import com.evsuite.hardware.VehicleWriteGate
|
||||
import java.lang.reflect.Proxy
|
||||
|
||||
/**
|
||||
* Adapted from ../winclose/app/src/main/java/com/mg4/winclose/WindowHardware.kt.
|
||||
* Keeps WinClose's SWI69 CarAdapterClient clients, CarState transaction 6 (0=open),
|
||||
* and setVehicleWindowStatus(area, command). No alternative door property is used.
|
||||
* Speed/time arming, delay, beeps, AUTO and per-window settings are intentionally absent.
|
||||
* WinClose identifies this event as the driver door; it carries no per-door area ID.
|
||||
*/
|
||||
internal class WinCloseHardware(
|
||||
context: Context,
|
||||
private val onOpening: () -> Unit,
|
||||
private val report: (String) -> Unit,
|
||||
) : AutoCloseable {
|
||||
private val context = context.applicationContext
|
||||
private val thread = HandlerThread("winclose-hardware").apply { start() }
|
||||
private val handler = Handler(thread.looper)
|
||||
@Volatile private var closed = false
|
||||
@Volatile private var vsm: Any? = null
|
||||
@Volatile private var state: Any? = null
|
||||
private var vsmBinder: IBinder? = null
|
||||
private var stateBinder: IBinder? = null
|
||||
private var callbackProxy: Any? = null
|
||||
private val doorSignal = WinCloseDoorSignal()
|
||||
@Volatile private var registeredAt = Long.MIN_VALUE
|
||||
private var initialized = false
|
||||
private val retry = object : Runnable {
|
||||
override fun run() {
|
||||
if (closed) return
|
||||
try {
|
||||
if (!initialized) {
|
||||
EVHardware.init(context)
|
||||
initialized = true
|
||||
}
|
||||
if (FirmwareInfo.getGeneration() == FirmwareInfo.Gen.SWI69) connect()
|
||||
} catch (e: Exception) {
|
||||
report("WinClose connection: ${e.javaClass.simpleName}")
|
||||
}
|
||||
if (!closed) handler.postDelayed(this, 5_000L)
|
||||
}
|
||||
}
|
||||
|
||||
fun start() { handler.post(retry) }
|
||||
|
||||
private fun connect() {
|
||||
if (vsmBinder?.isBinderAlive == true && stateBinder?.isBinderAlive == true) return
|
||||
val launcher = context.createPackageContext("com.saicmotor.launcher",
|
||||
Context.CONTEXT_INCLUDE_CODE or Context.CONTEXT_IGNORE_SECURITY)
|
||||
val loader = launcher.classLoader
|
||||
val adapterClass = loader.loadClass("com.saicmotor.carapi.CarAdapterClient")
|
||||
val adapter = adapterClass.getMethod("getInstance", Context::class.java)
|
||||
.invoke(null, context) ?: return
|
||||
// The same adapter startup and queryClient codes as WinClose.
|
||||
adapterClass.getMethod("start").invoke(adapter)
|
||||
val query = adapterClass.getMethod("queryClient", INT)
|
||||
if (vsmBinder?.isBinderAlive != true) {
|
||||
vsm = null
|
||||
val binder = query.invoke(adapter, 0x8) as? IBinder
|
||||
if (binder != null) {
|
||||
val cls = loader.loadClass("com.saicmotor.carapi.client.CarVehicleSettingClient")
|
||||
vsm = cls.getConstructor(IBinder::class.java).newInstance(binder)
|
||||
vsmBinder = binder
|
||||
report("WinClose VSM ready")
|
||||
}
|
||||
}
|
||||
if (stateBinder?.isBinderAlive == true) return
|
||||
detachListener()
|
||||
val binder = query.invoke(adapter, 0xb) as? IBinder ?: return
|
||||
val cls = loader.loadClass("com.saicmotor.carapi.client.CarStateClient")
|
||||
val client = cls.getConstructor(IBinder::class.java).newInstance(binder)
|
||||
val register = cls.methods.firstOrNull {
|
||||
it.name in listOf("registerListener", "registListener") && it.parameterCount == 1
|
||||
} ?: return
|
||||
val iface = register.parameterTypes[0]
|
||||
if (!iface.isInterface) return
|
||||
val callback = object : Binder() {
|
||||
override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean {
|
||||
if (code == IBinder.INTERFACE_TRANSACTION) {
|
||||
reply?.writeString(DESCRIPTOR)
|
||||
return true
|
||||
}
|
||||
if (code !in IBinder.FIRST_CALL_TRANSACTION..IBinder.LAST_CALL_TRANSACTION)
|
||||
return super.onTransact(code, data, reply, flags)
|
||||
try {
|
||||
data.enforceInterface(DESCRIPTOR)
|
||||
// Acknowledge the other CarState callbacks as WinClose does, but only
|
||||
// TX_DOOR_SENSOR=6 is a trigger. It is not a generic property or bitmask.
|
||||
if (code == 6) {
|
||||
val raw = data.readInt()
|
||||
val receivedAt = SystemClock.elapsedRealtime()
|
||||
handler.post {
|
||||
if (closed || stateBinder !== binder) return@post
|
||||
val sinceRegistration = receivedAt - registeredAt
|
||||
if (sinceRegistration < REGISTRATION_REPLAY_MS) {
|
||||
// Whatever arrives this soon after registering describes the
|
||||
// door as it already is, not a door someone just opened.
|
||||
doorSignal.seed(raw)
|
||||
report("WinClose door state seeded: raw=$raw")
|
||||
return@post
|
||||
}
|
||||
val fresh = SystemClock.elapsedRealtime() - receivedAt < 1_500L
|
||||
val opening = doorSignal.opening(raw)
|
||||
if (opening) report("WinClose door opening: raw=$raw fresh=$fresh")
|
||||
if (fresh && opening) onOpening()
|
||||
}
|
||||
}
|
||||
reply?.writeNoException()
|
||||
} catch (_: Exception) {
|
||||
// An unreadable callback must not re-arm an already-open door.
|
||||
reply?.writeNoException()
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
val proxy = Proxy.newProxyInstance(loader, arrayOf(iface)) { self, method, args ->
|
||||
when (method.name) {
|
||||
"asBinder" -> callback
|
||||
"hashCode" -> System.identityHashCode(self)
|
||||
"equals" -> self === args?.firstOrNull()
|
||||
"toString" -> "WinCloseCarStateListener"
|
||||
else -> when (method.returnType) {
|
||||
java.lang.Boolean.TYPE -> false
|
||||
java.lang.Integer.TYPE -> 0
|
||||
java.lang.Long.TYPE -> 0L
|
||||
java.lang.Float.TYPE -> 0f
|
||||
java.lang.Double.TYPE -> 0.0
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
doorSignal.reset()
|
||||
state = client
|
||||
stateBinder = binder
|
||||
callbackProxy = proxy
|
||||
// Stamped before the call: the replay can arrive inside register.invoke itself.
|
||||
registeredAt = SystemClock.elapsedRealtime()
|
||||
try {
|
||||
register.invoke(client, proxy)
|
||||
} catch (e: Exception) {
|
||||
detachListener()
|
||||
throw e
|
||||
}
|
||||
report("WinClose CarState door listener ready")
|
||||
}
|
||||
|
||||
fun isReady(): Boolean = vsm != null && state != null
|
||||
|
||||
fun isParked(): Boolean? {
|
||||
val client = state ?: return null
|
||||
return try {
|
||||
val gear = client.javaClass.getMethod("getGearState").invoke(client) as? Int
|
||||
gear?.takeIf { it >= 0 }?.let { it == 1 }
|
||||
} catch (_: Exception) { null }
|
||||
}
|
||||
|
||||
fun closeAllWindowsPulsed(keepRunning: () -> Boolean): Boolean {
|
||||
val client = vsm ?: return false
|
||||
val setter = try {
|
||||
client.javaClass.getMethod("setVehicleWindowStatus", INT, INT)
|
||||
} catch (_: Exception) { return false }
|
||||
return WindowClosePulse.run({
|
||||
!closed && keepRunning() && FirmwareInfo.getGeneration() == FirmwareInfo.Gen.SWI69
|
||||
&& isParked() == true && VehicleWriteGate.allow("WinClose setVehicleWindowStatus")
|
||||
}, { area, command ->
|
||||
try {
|
||||
setter.invoke(client, area, command)
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
report("WinClose command failed: area=$area command=$command ${e.javaClass.simpleName}")
|
||||
false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun detachListener() {
|
||||
val client = state
|
||||
val proxy = callbackProxy
|
||||
if (client != null && proxy != null) runCatching {
|
||||
client.javaClass.methods.firstOrNull {
|
||||
it.name in listOf("unregisterListener", "unregistListener")
|
||||
&& it.parameterCount == 1 && it.parameterTypes[0].isInstance(proxy)
|
||||
}?.invoke(client, proxy)
|
||||
}
|
||||
state = null
|
||||
stateBinder = null
|
||||
callbackProxy = null
|
||||
registeredAt = Long.MIN_VALUE
|
||||
doorSignal.reset()
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
closed = true
|
||||
handler.removeCallbacksAndMessages(null)
|
||||
handler.post {
|
||||
detachListener()
|
||||
vsm = null
|
||||
vsmBinder = null
|
||||
thread.quitSafely()
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val INT = Int::class.javaPrimitiveType!!
|
||||
const val DESCRIPTOR = "com.saicmotor.carapi.carstate.ICarStateCallback"
|
||||
|
||||
/** How long after registering a door callback still counts as the state replay. */
|
||||
const val REGISTRATION_REPLAY_MS = 2_000L
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
import java.util.function.LongSupplier;
|
||||
|
||||
/** One close per opening event; events during an existing close are consumed, not queued. */
|
||||
final class WindowClosePolicy {
|
||||
/**
|
||||
* WinClose's TRIGGER_COOLDOWN_MS. Getting back in to fetch something reopens the door,
|
||||
* and without this the glass would run a second full pulse each time.
|
||||
*/
|
||||
static final long COOLDOWN_MS = 60_000L;
|
||||
|
||||
private final LongSupplier clock;
|
||||
private boolean closing;
|
||||
private Long lastFinishedAt;
|
||||
|
||||
WindowClosePolicy() {
|
||||
// Elapsed time, not wall clock: a head-unit time correction must not open the gate.
|
||||
this(() -> System.nanoTime() / 1_000_000L);
|
||||
}
|
||||
|
||||
WindowClosePolicy(LongSupplier clock) {
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
synchronized boolean begin(Boolean parked, Float speedKmh) {
|
||||
if (closing || !Boolean.TRUE.equals(parked)
|
||||
|| speedKmh == null || !Float.isFinite(speedKmh) || speedKmh != 0f) {
|
||||
return false;
|
||||
}
|
||||
if (lastFinishedAt != null && clock.getAsLong() - lastFinishedAt < COOLDOWN_MS) {
|
||||
return false;
|
||||
}
|
||||
closing = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
synchronized void finish() {
|
||||
closing = false;
|
||||
lastFinishedAt = clock.getAsLong();
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
import java.util.function.BooleanSupplier;
|
||||
import java.util.function.LongSupplier;
|
||||
|
||||
/** WinClose's Comfort sequence: areas 0..3, UP=1 every 120 ms, then STOP=0. */
|
||||
final class WindowClosePulse {
|
||||
static final long DURATION_MS = 5_000L;
|
||||
static final long INTERVAL_MS = 120L;
|
||||
|
||||
interface Sender { boolean send(int area, int command); }
|
||||
interface Sleeper { void sleep(long millis) throws InterruptedException; }
|
||||
|
||||
static boolean run(BooleanSupplier allowed, Sender sender) {
|
||||
return run(allowed, sender, () -> System.nanoTime() / 1_000_000L, Thread::sleep);
|
||||
}
|
||||
|
||||
static boolean run(BooleanSupplier allowed, Sender sender, LongSupplier clock, Sleeper sleeper) {
|
||||
if (!allowed.getAsBoolean() || Thread.currentThread().isInterrupted()) return false;
|
||||
long deadline = clock.getAsLong() + DURATION_MS;
|
||||
boolean ok = true;
|
||||
try {
|
||||
while (clock.getAsLong() < deadline) {
|
||||
if (Thread.currentThread().isInterrupted() || !allowed.getAsBoolean()) return false;
|
||||
for (int area = 0; area < 4; area++) {
|
||||
if (!sender.send(area, 1)) ok = false;
|
||||
}
|
||||
if (!ok) break;
|
||||
long remaining = deadline - clock.getAsLong();
|
||||
if (remaining > 0) sleeper.sleep(Math.min(INTERVAL_MS, remaining));
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
ok = false;
|
||||
} finally {
|
||||
// Release every motor even after cancellation, a lost P/speed reading, or failure.
|
||||
for (int area = 0; area < 4; area++) {
|
||||
try {
|
||||
if (!sender.send(area, 0)) ok = false;
|
||||
} catch (RuntimeException e) {
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
}
|
||||
@@ -1,227 +0,0 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.ServiceInfo;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import android.os.PowerManager;
|
||||
import android.os.Process;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.core.app.NotificationCompat;
|
||||
|
||||
import com.evsuite.hardware.EVHardware;
|
||||
import com.evsuite.hardware.FirmwareInfo;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import kotlin.Unit;
|
||||
|
||||
/** Independent of ABRP credentials, GPS and upload cadence. No alarms or wake locks. */
|
||||
public final class WindowCloseService extends Service {
|
||||
static final String KEY_ENABLED = "window_close_enabled";
|
||||
private static final String ACTION_STOP = "com.evsuite.abrp.WINDOW_CLOSE_STOP";
|
||||
private static final String CHANNEL = "window_close";
|
||||
private static final int NOTIFICATION = 2;
|
||||
private static final ArrayDeque<String> events = new ArrayDeque<>();
|
||||
private static volatile boolean running;
|
||||
private static volatile int status = R.string.window_close_stopped;
|
||||
|
||||
private final WindowClosePolicy policy = new WindowClosePolicy();
|
||||
private volatile boolean active;
|
||||
private SharedPreferences prefs;
|
||||
private ExecutorService worker;
|
||||
private WinCloseHardware monitor;
|
||||
|
||||
static void startIfEnabled(Context context) {
|
||||
if (!context.getSharedPreferences("abrp_prefs", MODE_PRIVATE)
|
||||
.getBoolean(KEY_ENABLED, false)) return;
|
||||
try {
|
||||
context.startForegroundService(new Intent(context, WindowCloseService.class));
|
||||
} catch (RuntimeException e) {
|
||||
status = R.string.window_close_start_failed;
|
||||
log("Service start failed: " + e.getClass().getSimpleName());
|
||||
}
|
||||
}
|
||||
|
||||
static int statusResource() { return status; }
|
||||
static boolean isRunning() { return running; }
|
||||
|
||||
/**
|
||||
* True when this process can actually reach the vehicle setting service.
|
||||
*
|
||||
* Only the unstable flavor declares android.uid.system, so a stable build can toggle the
|
||||
* feature on and then fail every single command. Checking the uid rather than the flavor
|
||||
* keeps the answer true for any build that is genuinely platform-signed.
|
||||
*/
|
||||
static boolean canWriteToVehicle() { return Process.myUid() == Process.SYSTEM_UID; }
|
||||
|
||||
static synchronized String eventLog() { return String.join("\n", events); }
|
||||
|
||||
private static synchronized void log(String message) {
|
||||
Log.i("WindowClose", message);
|
||||
String time = new java.text.SimpleDateFormat("HH:mm:ss", java.util.Locale.US)
|
||||
.format(new java.util.Date());
|
||||
events.addFirst(time + " " + message);
|
||||
while (events.size() > 30) events.removeLast();
|
||||
}
|
||||
|
||||
@Override public void onCreate() {
|
||||
super.onCreate();
|
||||
prefs = getSharedPreferences("abrp_prefs", MODE_PRIVATE);
|
||||
worker = Executors.newSingleThreadExecutor(r -> new Thread(r, "window-close"));
|
||||
NotificationManager notifications = getSystemService(NotificationManager.class);
|
||||
notifications.createNotificationChannel(new NotificationChannel(
|
||||
CHANNEL, getString(R.string.window_close_title), NotificationManager.IMPORTANCE_LOW));
|
||||
Notification notification = notification();
|
||||
if (Build.VERSION.SDK_INT >= 34) {
|
||||
startForeground(NOTIFICATION, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE);
|
||||
} else {
|
||||
startForeground(NOTIFICATION, notification);
|
||||
}
|
||||
running = true;
|
||||
}
|
||||
|
||||
@Override public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
if (intent != null && ACTION_STOP.equals(intent.getAction())) {
|
||||
prefs.edit().putBoolean(KEY_ENABLED, false).apply();
|
||||
}
|
||||
if (!prefs.getBoolean(KEY_ENABLED, false)) {
|
||||
active = false;
|
||||
stopSelf();
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
// UNKNOWN is left running: at boot the generation is often not resolved yet, and the
|
||||
// monitor re-checks before it connects. A different known generation never will be
|
||||
// SWI69, so idling a foreground notification and a 5 s poll on it buys nothing.
|
||||
FirmwareInfo.Gen generation = FirmwareInfo.INSTANCE.getGeneration();
|
||||
if (generation != FirmwareInfo.Gen.SWI69 && generation != FirmwareInfo.Gen.UNKNOWN) {
|
||||
status = R.string.window_close_unsupported;
|
||||
log("Stopping: window closing is SWI69 only, this is " + generation);
|
||||
active = false;
|
||||
stopSelf();
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
if (!canWriteToVehicle()) {
|
||||
status = R.string.window_close_no_system_uid;
|
||||
log("Stopping: process uid " + Process.myUid() + " cannot write vehicle settings");
|
||||
active = false;
|
||||
stopSelf();
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
if (monitor == null) {
|
||||
active = true;
|
||||
status = R.string.window_close_waiting;
|
||||
log("Enabled: SWI69, P + 0 km/h, all four windows, 5 seconds");
|
||||
monitor = new WinCloseHardware(getApplicationContext(), () -> {
|
||||
onDoorOpening();
|
||||
return Unit.INSTANCE;
|
||||
}, message -> {
|
||||
log(message);
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
monitor.start();
|
||||
}
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
/** Runs on the monitor's callback thread, so it must not block on the car. */
|
||||
private void onDoorOpening() {
|
||||
if (!active || !prefs.getBoolean(KEY_ENABLED, false)) return;
|
||||
if (FirmwareInfo.INSTANCE.getGeneration() != FirmwareInfo.Gen.SWI69) return;
|
||||
if (!monitor.isReady()) {
|
||||
status = R.string.window_close_unavailable;
|
||||
log("Close skipped: window interface unavailable");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
worker.execute(this::closeNow);
|
||||
} catch (RejectedExecutionException e) {
|
||||
log("Close skipped: worker is shutting down");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The gear and speed reads are binder round trips, so they happen here rather than on the
|
||||
* callback thread — a slow CPM read there would delay the next door event behind it.
|
||||
*/
|
||||
private void closeNow() {
|
||||
Boolean parked = monitor.isParked();
|
||||
Float speed = EVHardware.INSTANCE.getVehicleSpeedKmh();
|
||||
if (!policy.begin(parked, speed)) {
|
||||
log("WinClose door: skipped (P=" + parked + ", speed=" + speed
|
||||
+ "; or a close ran in the last minute)");
|
||||
return;
|
||||
}
|
||||
// Held across the pulse because this fires as the driver leaves, which is exactly
|
||||
// when the head unit suspends. Timed out well past the pulse so a stuck close can
|
||||
// never pin the unit awake, and released in the finally regardless.
|
||||
PowerManager power = getSystemService(PowerManager.class);
|
||||
PowerManager.WakeLock wakeLock =
|
||||
power.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "EVABRP:windowClose");
|
||||
wakeLock.acquire(WindowClosePulse.DURATION_MS + 10_000L);
|
||||
status = R.string.window_close_closing;
|
||||
log("WinClose: closing all four windows");
|
||||
try {
|
||||
boolean ok = monitor.closeAllWindowsPulsed(() ->
|
||||
active && prefs.getBoolean(KEY_ENABLED, false));
|
||||
status = ok ? R.string.window_close_sent : R.string.window_close_interrupted;
|
||||
log(ok ? "Close pulse finished; STOP sent. Position is unknown."
|
||||
: "Close interrupted/refused or command failed; check P and speed.");
|
||||
} catch (RuntimeException e) {
|
||||
status = R.string.window_close_interrupted;
|
||||
log("Close failed: " + e.getClass().getSimpleName());
|
||||
} finally {
|
||||
if (wakeLock.isHeld()) wakeLock.release();
|
||||
policy.finish();
|
||||
}
|
||||
}
|
||||
|
||||
private Notification notification() {
|
||||
PendingIntent open = PendingIntent.getActivity(this, 20,
|
||||
new Intent(this, MainActivity.class), PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
||||
PendingIntent stop = PendingIntent.getService(this, 21,
|
||||
new Intent(this, WindowCloseService.class).setAction(ACTION_STOP),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
||||
return new NotificationCompat.Builder(this, CHANNEL)
|
||||
.setSmallIcon(R.drawable.ic_launcher_monochrome)
|
||||
.setContentTitle(getString(R.string.window_close_title))
|
||||
.setContentText(getString(R.string.window_close_notification))
|
||||
.setContentIntent(open)
|
||||
.addAction(0, getString(R.string.window_close_disable), stop)
|
||||
.setOngoing(true)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override public void onDestroy() {
|
||||
active = false;
|
||||
if (monitor != null) monitor.close();
|
||||
// The pulse runner releases all windows in finally, including an interrupted sleep.
|
||||
// Waited on so those STOP commands actually go out before the process moves on.
|
||||
if (worker != null) {
|
||||
worker.shutdownNow();
|
||||
try {
|
||||
if (!worker.awaitTermination(2, TimeUnit.SECONDS)) log("Worker did not stop in 2 s");
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
running = false;
|
||||
status = R.string.window_close_stopped;
|
||||
log("Service stopped");
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override public IBinder onBind(Intent intent) { return null; }
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android"><item android:state_focused="true" android:color="@color/ev_accent"/><item android:color="@color/ev_outline"/></selector>
|
||||
@@ -1,8 +1 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Nav tab background. The state comes from View.isSelected, set by
|
||||
MainActivity.markCurrentPage on the one tab whose page is on screen: no color is
|
||||
applied by hand in code. -->
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_selected="true" android:color="@color/ev_accent" />
|
||||
<item android:color="@color/ev_surface_raised" />
|
||||
</selector>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android"><item android:state_selected="true" android:color="@color/ev_gold_surface"/><item android:color="@color/ev_transparent"/></selector>
|
||||
|
||||
@@ -1,7 +1 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Nav tab text + icon. The selected tab sits on @color/ev_accent, so its foreground
|
||||
is ev_on_accent; unselected tabs use ev_text_primary. Both pairs clear 7:1. -->
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_selected="true" android:color="@color/ev_on_accent" />
|
||||
<item android:color="@color/ev_text_primary" />
|
||||
</selector>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android"><item android:state_selected="true" android:color="@color/ev_accent"/><item android:color="@color/ev_text_secondary"/></selector>
|
||||
|
||||
@@ -1,7 +1 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Nav tab outline. The selected tab keeps a stroke the same color as its fill so its
|
||||
size doesn't jump a dp when the page changes. -->
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_selected="true" android:color="@color/ev_accent" />
|
||||
<item android:color="@color/ev_outline" />
|
||||
</selector>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android"><item android:state_selected="true" android:color="#806F6045"/><item android:color="@color/ev_transparent"/></selector>
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_enabled="false" android:color="@color/ev_outline" />
|
||||
<item android:state_checked="true" android:color="@color/ev_cyan" />
|
||||
<item android:color="@color/ev_text_secondary" />
|
||||
</selector>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_enabled="false" android:color="@color/ev_surface_raised" />
|
||||
<item android:state_checked="true" android:color="#345E60" />
|
||||
<item android:color="@color/ev_outline" />
|
||||
</selector>
|
||||
@@ -0,0 +1 @@
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"><gradient android:angle="315" android:startColor="#E5183032" android:centerColor="#F012191A" android:endColor="#FA121212"/><corners android:radius="24dp"/><stroke android:width="1dp" android:color="#40528C8C"/></shape>
|
||||
@@ -0,0 +1 @@
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"><gradient android:angle="315" android:startColor="#E91B1E1F" android:centerColor="#F0121212" android:endColor="#FA121212"/><corners android:radius="24dp"/><stroke android:width="1dp" android:color="#2AFFFFFF"/></shape>
|
||||
@@ -0,0 +1 @@
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"><solid android:color="#101213"/><corners android:radius="16dp"/><stroke android:width="1dp" android:color="#263233"/></shape>
|
||||
@@ -0,0 +1 @@
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"><gradient android:angle="0" android:startColor="#101112" android:endColor="#08090A"/><corners android:radius="26dp"/><stroke android:width="1dp" android:color="#24FFFFFF"/></shape>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||
<path android:pathData="M6,9 L12,15 L18,9" android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="@color/ev_accent" android:strokeWidth="1.8" android:strokeLineCap="round" android:strokeLineJoin="round" />
|
||||
</vector>
|
||||
@@ -0,0 +1 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24"><path android:pathData="M12,3 A9,9 0,1 0,12 21 A9,9 0,1 0,12 3 M12,10 L12,17 M12,7 L12,7.2" android:fillColor="#00000000" android:strokeColor="#D8C39A" android:strokeWidth="1.6" android:strokeLineCap="round"/></vector>
|
||||
@@ -1,13 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- A car door with its glass, for the Windows page. Filled to match the other tab icons,
|
||||
which read at a glance on a vehicle screen rather than rewarding a close look. -->
|
||||
<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="M20,10.5l-1.6,-4.6C18.1,5.1 17.4,4.5 16.5,4.5H8.2C7.5,4.5 6.9,4.9 6.6,5.5L3.2,12.2C3.1,12.4 3,12.7 3,13v5c0,0.8 0.7,1.5 1.5,1.5h14c0.8,0 1.5,-0.7 1.5,-1.5v-7.5zM8.4,6.5h8.1l1.4,4H6.3l2.1,-4zM6,17c-0.8,0 -1.5,-0.7 -1.5,-1.5S5.2,14 6,14s1.5,0.7 1.5,1.5S6.8,17 6,17zM18,16.5h-8v-2h8v2z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="92dp" android:height="180dp" android:viewportWidth="92" android:viewportHeight="180">
|
||||
<path android:pathData="M27,12 Q46,5 65,12 Q74,18 74,38 L78,130 Q79,157 64,164 L28,164 Q13,157 14,130 L18,38 Q18,18 27,12Z" android:fillColor="#151D1E" android:strokeColor="#85BDBF" android:strokeWidth="1.4"/>
|
||||
<path android:pathData="M24,45 Q46,37 68,45 L63,65 Q46,61 29,65Z M28,129 Q46,135 64,129 L66,148 Q46,155 26,148Z M28,72 L28,122 M64,72 L64,122 M23,28 L35,25 M57,25 L69,28 M23,157 L35,159 M57,159 L69,157" android:fillColor="#102D30" android:strokeColor="#527B7C" android:strokeWidth="1.3"/>
|
||||
<path android:pathData="M10,43 L10,67 M82,43 L82,67 M10,120 L10,144 M82,120 L82,144" android:fillColor="#00000000" android:strokeColor="#D8C39A" android:strokeWidth="5" android:strokeLineCap="round"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/app_root" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" android:background="@color/ev_background">
|
||||
<com.evsuite.abrp.PremiumBackdropView android:layout_width="match_parent" android:layout_height="match_parent" />
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal">
|
||||
<ScrollView android:layout_width="192dp" android:layout_height="match_parent" android:layout_margin="16dp" android:fillViewport="true" android:background="@drawable/bg_sidebar" android:scrollbars="none">
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="12dp">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/design_brand" android:textAppearance="@style/Text.EV.Hero" android:textColor="@color/ev_accent" android:paddingStart="14dp" android:paddingTop="8dp" />
|
||||
<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/tabAbrp" style="@style/Widget.EV.NavTab" android:layout_width="match_parent" android:layout_height="64dp" android:layout_marginBottom="6dp" android:text="@string/tab_abrp" app:icon="@drawable/ic_tab_abrp" />
|
||||
<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/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/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>
|
||||
<LinearLayout android:layout_width="0dp" android:layout_weight="1" android:layout_height="match_parent" android:orientation="vertical">
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:gravity="center_vertical" android:paddingStart="24dp" android:paddingEnd="24dp" android:paddingTop="16dp" android:paddingBottom="8dp">
|
||||
<LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:orientation="vertical">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/design_overview_title" android:textAppearance="@style/Text.EV.Title" android:id="@+id/header_title" android:textSize="32sp" />
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/design_overview_subtitle" android:textAppearance="@style/Text.EV.Secondary" android:id="@+id/header_subtitle" android:layout_marginTop="6dp" />
|
||||
</LinearLayout>
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/update_button" style="@style/Widget.EV.Button.Outlined" android:layout_width="72dp" android:layout_height="72dp" android:minWidth="0dp" android:padding="0dp" android:contentDescription="@string/update_check" app:icon="@drawable/ic_refresh" app:iconGravity="textStart" app:iconPadding="0dp" app:iconTint="@color/ev_accent" />
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/about_button" style="@style/Widget.EV.Button.Outlined" android:layout_width="72dp" android:layout_height="72dp" android:layout_marginStart="8dp" android:minWidth="0dp" android:padding="0dp" android:contentDescription="@string/about_action" app:icon="@drawable/ic_info" app:iconGravity="textStart" app:iconPadding="0dp" />
|
||||
</LinearLayout>
|
||||
<androidx.viewpager2.widget.ViewPager2 android:id="@+id/content" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView 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.6" 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_account" android:textAppearance="@style/Text.EV.Eyebrow" />
|
||||
<com.google.android.material.textfield.TextInputLayout android:id="@+id/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_api_key" app:endIconMode="password_toggle" app:endIconMinSize="72dp" app:helperText="@string/helper_api_key">
|
||||
<com.google.android.material.textfield.TextInputEditText android:id="@+id/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/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_token" app:endIconMode="password_toggle" app:endIconMinSize="72dp" app:helperText="@string/helper_token">
|
||||
<com.google.android.material.textfield.TextInputEditText android:id="@+id/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/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/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" />
|
||||
<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>
|
||||
<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_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" />
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/probe_button" style="@style/Widget.EV.Button.Outlined" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="@string/design_probe" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView 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/status_stopped" android:textAppearance="@style/Text.EV.Body" />
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/service_switch" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:text="@string/label_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" />
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/autostart_switch" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:text="@string/label_autostart" 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_autostart" 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/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>
|
||||
</ScrollView>
|
||||
@@ -0,0 +1,81 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" 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="horizontal" 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="20dp" android:background="@drawable/bg_glass">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/design_energy" android:textAppearance="@style/Text.EV.Eyebrow" android:layout_marginBottom="12dp" />
|
||||
<TableLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:stretchColumns="0" android:shrinkColumns="0">
|
||||
<TableRow style="@style/DataRow" android:minHeight="40dp" android:paddingTop="8dp" android:paddingBottom="8dp">
|
||||
<TextView style="@style/DataLabel" android:text="@string/vehicle_row_odometer" />
|
||||
<TextView android:id="@+id/vehicle_odometer" style="@style/DataValue" />
|
||||
</TableRow>
|
||||
<TableRow style="@style/DataRow" android:minHeight="40dp" android:paddingTop="8dp" android:paddingBottom="8dp">
|
||||
<TextView style="@style/DataLabel" android:text="@string/vehicle_row_capacity" />
|
||||
<TextView android:id="@+id/vehicle_capacity" style="@style/DataValue" />
|
||||
</TableRow>
|
||||
<TableRow style="@style/DataRow" android:minHeight="40dp" android:paddingTop="8dp" android:paddingBottom="8dp">
|
||||
<TextView style="@style/DataLabel" android:text="@string/vehicle_row_batt_temp" />
|
||||
<TextView android:id="@+id/vehicle_batt_temp" style="@style/DataValue" />
|
||||
</TableRow>
|
||||
<TableRow style="@style/DataRow" android:minHeight="40dp" android:paddingTop="8dp" android:paddingBottom="8dp">
|
||||
<TextView style="@style/DataLabel" android:text="@string/vehicle_row_ext_temp" />
|
||||
<TextView android:id="@+id/vehicle_ext_temp" style="@style/DataValue" />
|
||||
</TableRow>
|
||||
<TableRow style="@style/DataRow" android:minHeight="40dp" android:paddingTop="8dp" android:paddingBottom="8dp">
|
||||
<TextView style="@style/DataLabel" android:text="@string/vehicle_row_cabin_temp" />
|
||||
<TextView android:id="@+id/vehicle_cabin_temp" style="@style/DataValue" />
|
||||
</TableRow>
|
||||
<TableRow style="@style/DataRow" android:minHeight="40dp" android:paddingTop="8dp" android:paddingBottom="8dp">
|
||||
<TextView style="@style/DataLabel" android:text="@string/vehicle_row_hvac" />
|
||||
<TextView android:id="@+id/vehicle_hvac" style="@style/DataValue" />
|
||||
</TableRow>
|
||||
</TableLayout>
|
||||
</LinearLayout>
|
||||
<LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:orientation="vertical" android:padding="20dp" android:background="@drawable/bg_glass">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/design_tires" android:textAppearance="@style/Text.EV.Eyebrow" />
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="224dp" android:orientation="horizontal" android:gravity="center_vertical">
|
||||
<LinearLayout android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:orientation="vertical" android:gravity="center">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/vehicle_tire_fl" android:textAppearance="@style/Text.EV.Secondary" android:gravity="center" />
|
||||
<TextView android:id="@+id/vehicle_tire_fl" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="—" android:textAppearance="@style/Text.EV.Body" android:gravity="center" android:layout_marginTop="8dp" android:textColor="@color/ev_cyan" />
|
||||
<Space android:layout_width="1dp" android:layout_height="44dp" />
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/vehicle_tire_rl" android:textAppearance="@style/Text.EV.Secondary" android:gravity="center" />
|
||||
<TextView android:id="@+id/vehicle_tire_rl" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="—" android:textAppearance="@style/Text.EV.Body" android:gravity="center" android:layout_marginTop="8dp" android:textColor="@color/ev_cyan" />
|
||||
</LinearLayout>
|
||||
<ImageView android:layout_width="92dp" android:layout_height="180dp" android:src="@drawable/ic_vehicle_outline" android:contentDescription="@string/design_car_desc" />
|
||||
<LinearLayout android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:orientation="vertical" android:gravity="center">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/vehicle_tire_fr" android:textAppearance="@style/Text.EV.Secondary" android:gravity="center" />
|
||||
<TextView android:id="@+id/vehicle_tire_fr" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="—" android:textAppearance="@style/Text.EV.Body" android:gravity="center" android:layout_marginTop="8dp" android:textColor="@color/ev_cyan" />
|
||||
<Space android:layout_width="1dp" android:layout_height="44dp" />
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/vehicle_tire_rr" android:textAppearance="@style/Text.EV.Secondary" android:gravity="center" />
|
||||
<TextView android:id="@+id/vehicle_tire_rr" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="—" android:textAppearance="@style/Text.EV.Body" android:gravity="center" android:layout_marginTop="8dp" android:textColor="@color/ev_cyan" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/design_no_data" android:textAppearance="@style/Text.EV.Secondary" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
<Space android:layout_width="1dp" android:layout_height="16dp" />
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:baselineAligned="false">
|
||||
<LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:orientation="vertical" android:minHeight="112dp" android:padding="16dp" android:background="@drawable/bg_battery_card" android:layout_weight="1" android:layout_marginEnd="10dp">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/design_battery_short" android:textAppearance="@style/Text.EV.Secondary" />
|
||||
<TextView android:id="@+id/vehicle_soc" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="—" android:layout_marginTop="6dp" android:textAppearance="@style/Text.EV.Metric.Compact" android:maxLines="1" android:autoSizeTextType="uniform" android:autoSizeMinTextSize="18sp" android:autoSizeMaxTextSize="28sp" android:autoSizeStepGranularity="1sp" />
|
||||
<TextView android:id="@+id/vehicle_charging" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="—" android:textAppearance="@style/Text.EV.Secondary" android:textColor="@color/ev_cyan" android:textSize="16sp" android:maxLines="1" android:ellipsize="end" />
|
||||
</LinearLayout>
|
||||
<LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:orientation="vertical" android:minHeight="112dp" android:padding="16dp" android:background="@drawable/bg_glass" android:layout_weight="1" android:layout_marginEnd="10dp">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/vehicle_row_range" android:textAppearance="@style/Text.EV.Secondary" />
|
||||
<TextView android:id="@+id/vehicle_range" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="—" android:layout_marginTop="6dp" android:textAppearance="@style/Text.EV.Metric.Compact" android:maxLines="1" android:autoSizeTextType="uniform" android:autoSizeMinTextSize="18sp" android:autoSizeMaxTextSize="28sp" android:autoSizeStepGranularity="1sp" />
|
||||
</LinearLayout>
|
||||
<LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:orientation="vertical" android:minHeight="112dp" android:padding="16dp" android:background="@drawable/bg_glass" android:layout_weight="1" android:layout_marginEnd="10dp">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/vehicle_row_power" android:textAppearance="@style/Text.EV.Secondary" />
|
||||
<TextView android:id="@+id/vehicle_power" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="—" android:layout_marginTop="6dp" android:textAppearance="@style/Text.EV.Metric.Compact" android:maxLines="1" android:autoSizeTextType="uniform" android:autoSizeMinTextSize="18sp" android:autoSizeMaxTextSize="28sp" android:autoSizeStepGranularity="1sp" />
|
||||
</LinearLayout>
|
||||
<LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:orientation="vertical" android:minHeight="112dp" android:padding="16dp" android:background="@drawable/bg_glass" android:layout_weight="1" android:layout_marginEnd="10dp">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/vehicle_row_speed" android:textAppearance="@style/Text.EV.Secondary" />
|
||||
<TextView android:id="@+id/vehicle_speed" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="—" android:layout_marginTop="6dp" android:textAppearance="@style/Text.EV.Metric.Compact" android:maxLines="1" android:autoSizeTextType="uniform" android:autoSizeMinTextSize="18sp" android:autoSizeMaxTextSize="28sp" android:autoSizeStepGranularity="1sp" />
|
||||
</LinearLayout>
|
||||
<LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:orientation="vertical" android:minHeight="112dp" android:padding="16dp" android:background="@drawable/bg_glass" android:layout_weight="1">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/vehicle_row_soe" android:textAppearance="@style/Text.EV.Secondary" />
|
||||
<TextView android:id="@+id/vehicle_soe" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="—" android:layout_marginTop="6dp" android:textAppearance="@style/Text.EV.Metric.Compact" android:maxLines="1" android:autoSizeTextType="uniform" android:autoSizeMinTextSize="18sp" android:autoSizeMaxTextSize="28sp" android:autoSizeStepGranularity="1sp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView 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>
|
||||
</ScrollView>
|
||||
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@drawable/bg_glass" android:clipToOutline="true">
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="28dp">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/about_title" android:textAppearance="@style/Text.EV.Eyebrow" android:layout_marginBottom="24dp"/>
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:baselineAligned="false">
|
||||
<LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:layout_marginEnd="32dp" android:orientation="vertical">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/design_brand" android:textAppearance="@style/Text.EV.Hero" android:textColor="@color/ev_accent"/>
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/design_brand_caption" android:textAppearance="@style/Text.EV.Title" android:layout_marginTop="12dp"/>
|
||||
<TextView android:id="@+id/about_version" android:layout_width="match_parent" android:layout_height="wrap_content" android:textAppearance="@style/Text.EV.Secondary" android:layout_marginTop="8dp"/>
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/about_attribution" android:textAppearance="@style/Text.EV.Secondary" android:lineSpacingMultiplier="1.3" android:layout_marginTop="24dp"/>
|
||||
</LinearLayout>
|
||||
<LinearLayout android:layout_width="240dp" android:layout_height="wrap_content" android:orientation="vertical">
|
||||
<ImageView android:id="@+id/about_qr_code" android:layout_width="160dp" android:layout_height="160dp" android:layout_gravity="center_horizontal" android:background="#FFFFFF" android:padding="8dp" android:scaleType="fitCenter" android:contentDescription="@string/about_qr_description"/>
|
||||
<TextView android:id="@+id/about_repository" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:gravity="center" android:clickable="true" android:focusable="true" android:background="?attr/selectableItemBackground" android:text="@string/about_repository" android:textAppearance="@style/Text.EV.Secondary" android:textColor="@color/ev_cyan" android:paddingTop="12dp" android:paddingBottom="12dp"/>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
<View android:layout_width="match_parent" android:layout_height="1dp" android:background="@color/ev_outline" android:layout_marginTop="24dp" android:layout_marginBottom="12dp"/>
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/about_made_with_love" android:textAppearance="@style/Text.EV.Secondary" android:layout_marginBottom="12dp"/>
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/about_close" style="@style/Widget.EV.Button.Outlined" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/about_close"/>
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
@@ -1,135 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
A fixed top bar above a single ViewPager2, same shape as EVProfile and EVTasker.
|
||||
|
||||
The three screens — ABRP, Service, Log — are the pages of that one pager, in the
|
||||
order the bar lists them. Each button goes to its page and a horizontal swipe walks
|
||||
through them all: the gesture and the button name the same thing. The pages
|
||||
themselves live in pane_abrp.xml / pane_service.xml / pane_log.xml, because a
|
||||
ViewPager2 takes its children from an adapter rather than from this file.
|
||||
|
||||
Button navigation rather than Material tabs: on a vehicle screen, always-visible
|
||||
64 dp targets beat a thin tab bar. The tabs share the width left by the title and
|
||||
the About button equally (weight 1), so the row can be aimed at from memory whatever
|
||||
the translated labels are.
|
||||
|
||||
No MaterialButtonToggleGroup: the group owns a checked state of its own, which would
|
||||
fight the pager for who decides the current screen. Here the pager decides alone and
|
||||
MainActivity.markCurrentPage posts isSelected — the fill, text, icon and stroke come
|
||||
from the res/color/nav_tab_*.xml selectors.
|
||||
-->
|
||||
<LinearLayout 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:orientation="vertical"
|
||||
android:background="@color/ev_background">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="80dp"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:baselineAligned="false"
|
||||
android:paddingStart="@dimen/spacing_md"
|
||||
android:paddingEnd="@dimen/spacing_md"
|
||||
android:background="@drawable/bg_topbar">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/app_name"
|
||||
android:textAppearance="@style/Text.EV.Title"
|
||||
android:layout_marginEnd="@dimen/spacing_lg" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/tabAbrp"
|
||||
style="@style/Widget.EV.NavTab"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="64dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginEnd="@dimen/spacing_sm"
|
||||
android:text="@string/tab_abrp"
|
||||
app:icon="@drawable/ic_tab_abrp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/tabService"
|
||||
style="@style/Widget.EV.NavTab"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="64dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginEnd="@dimen/spacing_sm"
|
||||
android:text="@string/tab_service"
|
||||
app:icon="@drawable/ic_tab_service" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/tabVehicle"
|
||||
style="@style/Widget.EV.NavTab"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="64dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginEnd="@dimen/spacing_sm"
|
||||
android:text="@string/tab_vehicle"
|
||||
app:icon="@drawable/ic_tab_vehicle" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/tabWindows"
|
||||
style="@style/Widget.EV.NavTab"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="64dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginEnd="@dimen/spacing_sm"
|
||||
android:text="@string/tab_windows"
|
||||
app:icon="@drawable/ic_tab_windows" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/tabWifi"
|
||||
style="@style/Widget.EV.NavTab"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="64dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginEnd="@dimen/spacing_sm"
|
||||
android:text="@string/tab_wifi"
|
||||
app:icon="@drawable/ic_tab_wifi" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/tabLog"
|
||||
style="@style/Widget.EV.NavTab"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="64dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginEnd="@dimen/spacing_lg"
|
||||
android:text="@string/tab_log"
|
||||
app:icon="@drawable/ic_tab_log" />
|
||||
|
||||
<!-- Neither of these is a destination: they stay plain buttons, outside the tab
|
||||
row, so they cannot be mistaken for pages of the pager. -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/update_button"
|
||||
style="@style/Widget.EV.Button.Outlined"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="@dimen/spacing_sm"
|
||||
android:contentDescription="@string/update_check"
|
||||
app:icon="@drawable/ic_refresh"
|
||||
app:iconPadding="0dp"
|
||||
android:minWidth="0dp"
|
||||
android:paddingStart="@dimen/spacing_md"
|
||||
android:paddingEnd="@dimen/spacing_md" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/about_button"
|
||||
style="@style/Widget.EV.Button.Outlined"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/about_action" />
|
||||
|
||||
<?xml version="1.0"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/app_root" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" android:background="@color/ev_background">
|
||||
<com.evsuite.abrp.PremiumBackdropView android:layout_width="match_parent" android:layout_height="match_parent"/>
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical">
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:gravity="center_vertical" android:paddingStart="24dp" android:paddingEnd="24dp" android:paddingTop="16dp" android:paddingBottom="8dp">
|
||||
<LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:orientation="vertical">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/design_overview_title" android:textAppearance="@style/Text.EV.Title" android:id="@+id/header_title" android:textSize="24sp"/>
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/design_overview_subtitle" android:textAppearance="@style/Text.EV.Secondary" android:id="@+id/header_subtitle" android:layout_marginTop="6dp"/>
|
||||
</LinearLayout>
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/update_button" style="@style/Widget.EV.Button.Outlined" android:layout_width="72dp" android:layout_height="72dp" android:minWidth="0dp" android:padding="0dp" android:contentDescription="@string/update_check" app:icon="@drawable/ic_refresh" app:iconGravity="textStart" app:iconPadding="0dp" app:iconTint="@color/ev_accent"/>
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/about_button" style="@style/Widget.EV.Button.Outlined" android:layout_width="72dp" android:layout_height="72dp" android:layout_marginStart="8dp" android:minWidth="0dp" android:padding="0dp" android:contentDescription="@string/about_action" app:icon="@drawable/ic_info" app:iconGravity="textStart" app:iconPadding="0dp"/>
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
</LinearLayout>
|
||||
<androidx.viewpager2.widget.ViewPager2 android:id="@+id/content" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1"/>
|
||||
<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/tabAbrp" style="@style/Widget.EV.NavTab.Compact" android:layout_width="0dp" android:layout_height="76dp" android:layout_weight="1" android:text="@string/tab_abrp" app:icon="@drawable/ic_tab_abrp"/>
|
||||
<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/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/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>
|
||||
</FrameLayout>
|
||||
|
||||
@@ -1,142 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="760dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:cardBackgroundColor="@color/ev_surface"
|
||||
app:cardCornerRadius="@dimen/ev_corner"
|
||||
app:cardElevation="0dp"
|
||||
app:strokeColor="@color/ev_outline"
|
||||
app:strokeWidth="1dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="@dimen/spacing_lg"
|
||||
android:paddingTop="@dimen/spacing_md"
|
||||
android:paddingEnd="@dimen/spacing_lg"
|
||||
android:paddingBottom="@dimen/spacing_md"
|
||||
android:text="@string/about_title"
|
||||
android:textAppearance="@style/Text.EV.SectionHeader" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="@color/ev_outline" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:baselineAligned="false"
|
||||
android:orientation="horizontal"
|
||||
android:padding="@dimen/spacing_lg">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginEnd="@dimen/spacing_lg"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="128dp"
|
||||
android:layout_height="128dp"
|
||||
android:background="@drawable/about_logo_tile"
|
||||
android:contentDescription="@string/about_logo_description"
|
||||
android:padding="@dimen/spacing_sm"
|
||||
android:src="@drawable/ic_launcher_foreground" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/spacing_md"
|
||||
android:text="@string/app_name"
|
||||
android:textAppearance="@style/Text.EV.Title" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/about_version"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/spacing_xs"
|
||||
android:textAppearance="@style/Text.EV.Secondary" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/spacing_md"
|
||||
android:lineSpacingMultiplier="1.25"
|
||||
android:text="@string/about_attribution"
|
||||
android:textAppearance="@style/Text.EV.Body" />
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="1dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginEnd="@dimen/spacing_lg"
|
||||
android:background="@color/ev_outline" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="264dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/about_qr_code"
|
||||
android:layout_width="208dp"
|
||||
android:layout_height="208dp"
|
||||
android:background="#FFFFFFFF"
|
||||
android:contentDescription="@string/about_qr_description"
|
||||
android:padding="@dimen/spacing_sm"
|
||||
android:scaleType="fitCenter" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/about_repository"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/touch_target"
|
||||
android:layout_marginTop="@dimen/spacing_sm"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:gravity="center"
|
||||
android:text="@string/about_repository"
|
||||
android:textAppearance="@style/Text.EV.Secondary"
|
||||
android:textColor="@color/ev_accent" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="@color/ev_outline" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:padding="@dimen/spacing_md">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/about_made_with_love"
|
||||
android:textAppearance="@style/Text.EV.Body" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/about_close"
|
||||
style="@style/Widget.EV.Button.Outlined"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="@dimen/touch_target"
|
||||
android:text="@string/about_close" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@drawable/bg_glass" android:clipToOutline="true">
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="28dp">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/about_title" android:textAppearance="@style/Text.EV.Eyebrow" android:layout_marginBottom="24dp"/>
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/design_brand" android:textAppearance="@style/Text.EV.Hero" android:textColor="@color/ev_accent"/>
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/design_brand_caption" android:textAppearance="@style/Text.EV.Title" android:layout_marginTop="12dp"/>
|
||||
<TextView android:id="@+id/about_version" android:layout_width="match_parent" android:layout_height="wrap_content" android:textAppearance="@style/Text.EV.Secondary" android:layout_marginTop="8dp"/>
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/about_attribution" android:textAppearance="@style/Text.EV.Secondary" android:lineSpacingMultiplier="1.3" android:layout_marginTop="24dp"/>
|
||||
<Space android:layout_width="1dp" android:layout_height="24dp"/>
|
||||
<ImageView android:id="@+id/about_qr_code" android:layout_width="160dp" android:layout_height="160dp" android:layout_gravity="center_horizontal" android:background="#FFFFFF" android:padding="8dp" android:scaleType="fitCenter" android:contentDescription="@string/about_qr_description"/>
|
||||
<TextView android:id="@+id/about_repository" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:gravity="center" android:clickable="true" android:focusable="true" android:background="?attr/selectableItemBackground" android:text="@string/about_repository" android:textAppearance="@style/Text.EV.Secondary" android:textColor="@color/ev_cyan" android:paddingTop="12dp" android:paddingBottom="12dp"/>
|
||||
<View android:layout_width="match_parent" android:layout_height="1dp" android:background="@color/ev_outline" android:layout_marginTop="24dp" android:layout_marginBottom="12dp"/>
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/about_made_with_love" android:textAppearance="@style/Text.EV.Secondary" android:layout_marginBottom="12dp"/>
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/about_close" style="@style/Widget.EV.Button.Outlined" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/about_close"/>
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
@@ -1,153 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- ABRP page of the main pager. Inflated once by MainActivity and handed to the pager
|
||||
adapter as a fixed, non-recycled page, so the widget lookups below it stay valid for
|
||||
the whole life of the activity. -->
|
||||
<ScrollView 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">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/spacing_lg">
|
||||
|
||||
<!-- Configuration card -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:cardBackgroundColor="@color/ev_surface"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:strokeColor="@color/ev_outline"
|
||||
app:strokeWidth="1dp"
|
||||
app:cardElevation="3dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/spacing_lg">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="20dp"
|
||||
android:text="@string/card_config"
|
||||
android:textAppearance="@style/Text.EV.Title" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/api_key_layout"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:hint="@string/hint_api_key"
|
||||
app:endIconContentDescription="@string/desc_toggle_api_key"
|
||||
app:endIconMode="password_toggle"
|
||||
app:endIconMinSize="@dimen/touch_target"
|
||||
app:helperText="@string/helper_api_key"
|
||||
app:helperTextTextAppearance="@style/Text.EV.Secondary">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/api_key_input"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textPassword"
|
||||
android:singleLine="true"
|
||||
android:textSize="@dimen/text_body" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/token_layout"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/hint_token"
|
||||
app:endIconContentDescription="@string/desc_toggle_token"
|
||||
app:endIconMode="password_toggle"
|
||||
app:endIconMinSize="@dimen/touch_target"
|
||||
app:helperText="@string/helper_token"
|
||||
app:helperTextTextAppearance="@style/Text.EV.Secondary">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/token_input"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textPassword"
|
||||
android:singleLine="true"
|
||||
android:textSize="@dimen/text_body" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:gravity="end"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
android:id="@+id/import_button"
|
||||
style="@style/Widget.EV.Button.Outlined"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:text="@string/btn_import" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/test_button"
|
||||
style="@style/Widget.EV.Button.Outlined"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:text="@string/btn_test" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/save_button"
|
||||
style="@style/Widget.EV.Button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/btn_save" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/probe_button"
|
||||
style="@style/Widget.EV.Button.Outlined"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:text="VHAL Probe" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/connection_status_row"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:visibility="gone">
|
||||
|
||||
<View
|
||||
android:id="@+id/connection_indicator"
|
||||
android:layout_width="14dp"
|
||||
android:layout_height="14dp"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:background="@drawable/circle_indicator" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/connection_status_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@style/Text.EV.Body" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ScrollView 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_account" android:textAppearance="@style/Text.EV.Eyebrow" />
|
||||
<com.google.android.material.textfield.TextInputLayout android:id="@+id/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_api_key" app:endIconMode="password_toggle" app:endIconMinSize="72dp" app:helperText="@string/helper_api_key">
|
||||
<com.google.android.material.textfield.TextInputEditText android:id="@+id/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/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_token" app:endIconMode="password_toggle" app:endIconMinSize="72dp" app:helperText="@string/helper_token">
|
||||
<com.google.android.material.textfield.TextInputEditText android:id="@+id/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/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/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" />
|
||||
<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>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
</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" />
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/probe_button" style="@style/Widget.EV.Button.Outlined" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="@string/design_probe" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
@@ -1,51 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Log page of the main pager: status, time and reason for the last 20 upload calls.
|
||||
See pane_abrp.xml for how the pages are held. -->
|
||||
<ScrollView 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">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/spacing_lg">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:cardBackgroundColor="@color/ev_surface"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:strokeColor="@color/ev_outline"
|
||||
app:strokeWidth="1dp"
|
||||
app:cardElevation="3dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/spacing_md">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/label_call_log"
|
||||
android:textAppearance="@style/Text.EV.Title" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/call_log_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/spacing_sm"
|
||||
android:fontFamily="monospace"
|
||||
android:textSize="@dimen/text_secondary"
|
||||
android:textColor="@color/ev_text_secondary"
|
||||
android:text="@string/call_log_empty" />
|
||||
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" 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" android:background="@drawable/bg_glass">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/label_call_log" android:textAppearance="@style/Text.EV.Eyebrow" />
|
||||
<TextView android:id="@+id/call_log_text" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="20dp" android:minHeight="180dp" android:text="@string/call_log_empty" android:textAppearance="@style/Text.EV.Body" android:fontFamily="monospace" android:textSize="17sp" android:lineSpacingExtra="12dp" android:textIsSelectable="true" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
@@ -1,161 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Service page of the main pager. See pane_abrp.xml for how the pages are held. -->
|
||||
<ScrollView 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">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/spacing_lg">
|
||||
|
||||
<!-- Service control card -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:cardBackgroundColor="@color/ev_surface"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:strokeColor="@color/ev_outline"
|
||||
app:strokeWidth="1dp"
|
||||
app:cardElevation="3dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/spacing_lg">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="20dp"
|
||||
android:text="@string/card_service"
|
||||
android:textAppearance="@style/Text.EV.Title" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:minHeight="@dimen/touch_target">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/label_upload_enabled"
|
||||
android:textAppearance="@style/Text.EV.Body" />
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/service_switch"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Redémarrage automatique. Personne n'ouvre cette application avant
|
||||
de partir : si l'envoi ne repart pas tout seul quand la voiture
|
||||
s'allume, il ne repart pas du tout. -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:minHeight="@dimen/touch_target">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/label_autostart"
|
||||
android:textAppearance="@style/Text.EV.Body" />
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/autostart_switch"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/hint_autostart"
|
||||
android:textAppearance="@style/Text.EV.Secondary" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/status_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:text="@string/status_stopped"
|
||||
android:textAppearance="@style/Text.EV.Body" />
|
||||
|
||||
<!-- Cadence. Defaults are conservative: the head unit is awake whenever
|
||||
the car is on, and both the upload tick and the GPS subscription
|
||||
cost power for as long as it is. -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="20dp"
|
||||
android:text="@string/label_frequency"
|
||||
android:textAppearance="@style/Text.EV.Body" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/interval_spinner"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="@dimen/touch_target" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/hint_frequency"
|
||||
android:textAppearance="@style/Text.EV.Secondary" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:minHeight="@dimen/touch_target">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/label_boost_low_soc"
|
||||
android:textAppearance="@style/Text.EV.Body" />
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/boost_switch"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/low_soc_layout"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
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:inputType="number"
|
||||
android:maxLength="2" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<ScrollView 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_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/status_stopped" android:textAppearance="@style/Text.EV.Body" />
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/service_switch" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:text="@string/label_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" />
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/autostart_switch" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:text="@string/label_autostart" 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_autostart" 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/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>
|
||||
</ScrollView>
|
||||
|
||||
@@ -1,173 +1,79 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
The Vehicle page: every signal this build can read from the car, in the order a
|
||||
driver glances at them.
|
||||
|
||||
The rows are static views rather than a RecyclerView on purpose — the set is fixed
|
||||
and known at build time, and MainActivity holds the value TextViews the same way it
|
||||
holds every other widget on the other pages. A row whose signal the car does not
|
||||
publish shows a dash, never a zero: on this vehicle a zero from an unimplemented
|
||||
VHAL property is indistinguishable from a reading, and "0 °C" or "0 kWh" is worse
|
||||
than an honest blank.
|
||||
-->
|
||||
<ScrollView 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">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/spacing_lg">
|
||||
|
||||
<!-- Live vehicle data -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="@dimen/spacing_md"
|
||||
app:cardBackgroundColor="@color/ev_surface"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:strokeColor="@color/ev_outline"
|
||||
app:strokeWidth="1dp"
|
||||
app:cardElevation="3dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/spacing_lg">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="@dimen/spacing_md"
|
||||
android:text="@string/vehicle_live_data"
|
||||
android:textAppearance="@style/Text.EV.Title" />
|
||||
|
||||
<TableLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:stretchColumns="0">
|
||||
|
||||
<TableRow style="@style/DataRow">
|
||||
<TextView style="@style/DataLabel" android:text="@string/vehicle_row_soc" />
|
||||
<TextView android:id="@+id/vehicle_soc" style="@style/DataValue" />
|
||||
</TableRow>
|
||||
|
||||
<TableRow style="@style/DataRow">
|
||||
<TextView style="@style/DataLabel" android:text="@string/vehicle_row_range" />
|
||||
<TextView android:id="@+id/vehicle_range" style="@style/DataValue" />
|
||||
</TableRow>
|
||||
|
||||
<TableRow style="@style/DataRow">
|
||||
<TextView style="@style/DataLabel" android:text="@string/vehicle_row_power" />
|
||||
<TextView android:id="@+id/vehicle_power" style="@style/DataValue" />
|
||||
</TableRow>
|
||||
|
||||
<TableRow style="@style/DataRow">
|
||||
<TextView style="@style/DataLabel" android:text="@string/vehicle_row_speed" />
|
||||
<TextView android:id="@+id/vehicle_speed" style="@style/DataValue" />
|
||||
</TableRow>
|
||||
|
||||
<TableRow style="@style/DataRow">
|
||||
<TextView style="@style/DataLabel" android:text="@string/vehicle_row_odometer" />
|
||||
<TextView android:id="@+id/vehicle_odometer" style="@style/DataValue" />
|
||||
</TableRow>
|
||||
|
||||
<TableRow style="@style/DataRow">
|
||||
<TextView style="@style/DataLabel" android:text="@string/vehicle_row_charging" />
|
||||
<TextView android:id="@+id/vehicle_charging" style="@style/DataValue" />
|
||||
</TableRow>
|
||||
|
||||
<TableRow style="@style/DataRow">
|
||||
<TextView style="@style/DataLabel" android:text="@string/vehicle_row_batt_temp" />
|
||||
<TextView android:id="@+id/vehicle_batt_temp" style="@style/DataValue" />
|
||||
</TableRow>
|
||||
|
||||
<TableRow style="@style/DataRow">
|
||||
<TextView style="@style/DataLabel" android:text="@string/vehicle_row_ext_temp" />
|
||||
<TextView android:id="@+id/vehicle_ext_temp" style="@style/DataValue" />
|
||||
</TableRow>
|
||||
|
||||
<TableRow style="@style/DataRow">
|
||||
<TextView style="@style/DataLabel" android:text="@string/vehicle_row_cabin_temp" />
|
||||
<TextView android:id="@+id/vehicle_cabin_temp" style="@style/DataValue" />
|
||||
</TableRow>
|
||||
|
||||
<TableRow style="@style/DataRow">
|
||||
<TextView style="@style/DataLabel" android:text="@string/vehicle_row_hvac" />
|
||||
<TextView android:id="@+id/vehicle_hvac" style="@style/DataValue" />
|
||||
</TableRow>
|
||||
|
||||
<TableRow style="@style/DataRow">
|
||||
<TextView style="@style/DataLabel" android:text="@string/vehicle_row_capacity" />
|
||||
<TextView android:id="@+id/vehicle_capacity" style="@style/DataValue" />
|
||||
</TableRow>
|
||||
|
||||
<TableRow style="@style/DataRow">
|
||||
<TextView style="@style/DataLabel" android:text="@string/vehicle_row_soe" />
|
||||
<TextView android:id="@+id/vehicle_soe" style="@style/DataValue" />
|
||||
</TableRow>
|
||||
|
||||
</TableLayout>
|
||||
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" 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="20dp" android:background="@drawable/bg_glass">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/design_energy" android:textAppearance="@style/Text.EV.Eyebrow" android:layout_marginBottom="12dp" />
|
||||
<TableLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:stretchColumns="0" android:shrinkColumns="0">
|
||||
<TableRow style="@style/DataRow" android:minHeight="40dp" android:paddingTop="8dp" android:paddingBottom="8dp">
|
||||
<TextView style="@style/DataLabel" android:text="@string/vehicle_row_odometer" />
|
||||
<TextView android:id="@+id/vehicle_odometer" style="@style/DataValue" />
|
||||
</TableRow>
|
||||
<TableRow style="@style/DataRow" android:minHeight="40dp" android:paddingTop="8dp" android:paddingBottom="8dp">
|
||||
<TextView style="@style/DataLabel" android:text="@string/vehicle_row_capacity" />
|
||||
<TextView android:id="@+id/vehicle_capacity" style="@style/DataValue" />
|
||||
</TableRow>
|
||||
<TableRow style="@style/DataRow" android:minHeight="40dp" android:paddingTop="8dp" android:paddingBottom="8dp">
|
||||
<TextView style="@style/DataLabel" android:text="@string/vehicle_row_batt_temp" />
|
||||
<TextView android:id="@+id/vehicle_batt_temp" style="@style/DataValue" />
|
||||
</TableRow>
|
||||
<TableRow style="@style/DataRow" android:minHeight="40dp" android:paddingTop="8dp" android:paddingBottom="8dp">
|
||||
<TextView style="@style/DataLabel" android:text="@string/vehicle_row_ext_temp" />
|
||||
<TextView android:id="@+id/vehicle_ext_temp" style="@style/DataValue" />
|
||||
</TableRow>
|
||||
<TableRow style="@style/DataRow" android:minHeight="40dp" android:paddingTop="8dp" android:paddingBottom="8dp">
|
||||
<TextView style="@style/DataLabel" android:text="@string/vehicle_row_cabin_temp" />
|
||||
<TextView android:id="@+id/vehicle_cabin_temp" style="@style/DataValue" />
|
||||
</TableRow>
|
||||
<TableRow style="@style/DataRow" android:minHeight="40dp" android:paddingTop="8dp" android:paddingBottom="8dp">
|
||||
<TextView style="@style/DataLabel" android:text="@string/vehicle_row_hvac" />
|
||||
<TextView android:id="@+id/vehicle_hvac" style="@style/DataValue" />
|
||||
</TableRow>
|
||||
</TableLayout>
|
||||
</LinearLayout>
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="20dp" android:background="@drawable/bg_glass" android:layout_marginTop="16dp">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/design_tires" android:textAppearance="@style/Text.EV.Eyebrow" />
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="224dp" android:orientation="horizontal" android:gravity="center_vertical">
|
||||
<LinearLayout android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:orientation="vertical" android:gravity="center">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/vehicle_tire_fl" android:textAppearance="@style/Text.EV.Secondary" android:gravity="center" />
|
||||
<TextView android:id="@+id/vehicle_tire_fl" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="—" android:textAppearance="@style/Text.EV.Body" android:gravity="center" android:layout_marginTop="8dp" android:textColor="@color/ev_cyan" />
|
||||
<Space android:layout_width="1dp" android:layout_height="44dp" />
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/vehicle_tire_rl" android:textAppearance="@style/Text.EV.Secondary" android:gravity="center" />
|
||||
<TextView android:id="@+id/vehicle_tire_rl" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="—" android:textAppearance="@style/Text.EV.Body" android:gravity="center" android:layout_marginTop="8dp" android:textColor="@color/ev_cyan" />
|
||||
</LinearLayout>
|
||||
<ImageView android:layout_width="92dp" android:layout_height="180dp" android:src="@drawable/ic_vehicle_outline" android:contentDescription="@string/design_car_desc" />
|
||||
<LinearLayout android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:orientation="vertical" android:gravity="center">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/vehicle_tire_fr" android:textAppearance="@style/Text.EV.Secondary" android:gravity="center" />
|
||||
<TextView android:id="@+id/vehicle_tire_fr" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="—" android:textAppearance="@style/Text.EV.Body" android:gravity="center" android:layout_marginTop="8dp" android:textColor="@color/ev_cyan" />
|
||||
<Space android:layout_width="1dp" android:layout_height="44dp" />
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/vehicle_tire_rr" android:textAppearance="@style/Text.EV.Secondary" android:gravity="center" />
|
||||
<TextView android:id="@+id/vehicle_tire_rr" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="—" android:textAppearance="@style/Text.EV.Body" android:gravity="center" android:layout_marginTop="8dp" android:textColor="@color/ev_cyan" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<!-- Tyre pressures -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:cardBackgroundColor="@color/ev_surface"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:strokeColor="@color/ev_outline"
|
||||
app:strokeWidth="1dp"
|
||||
app:cardElevation="3dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/spacing_lg">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="@dimen/spacing_md"
|
||||
android:text="@string/vehicle_tires"
|
||||
android:textAppearance="@style/Text.EV.Title" />
|
||||
|
||||
<TableLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:stretchColumns="0">
|
||||
|
||||
<TableRow style="@style/DataRow">
|
||||
<TextView style="@style/DataLabel" android:text="@string/vehicle_tire_fl" />
|
||||
<TextView android:id="@+id/vehicle_tire_fl" style="@style/DataValue" />
|
||||
</TableRow>
|
||||
|
||||
<TableRow style="@style/DataRow">
|
||||
<TextView style="@style/DataLabel" android:text="@string/vehicle_tire_fr" />
|
||||
<TextView android:id="@+id/vehicle_tire_fr" style="@style/DataValue" />
|
||||
</TableRow>
|
||||
|
||||
<TableRow style="@style/DataRow">
|
||||
<TextView style="@style/DataLabel" android:text="@string/vehicle_tire_rl" />
|
||||
<TextView android:id="@+id/vehicle_tire_rl" style="@style/DataValue" />
|
||||
</TableRow>
|
||||
|
||||
<TableRow style="@style/DataRow">
|
||||
<TextView style="@style/DataLabel" android:text="@string/vehicle_tire_rr" />
|
||||
<TextView android:id="@+id/vehicle_tire_rr" style="@style/DataValue" />
|
||||
</TableRow>
|
||||
|
||||
</TableLayout>
|
||||
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/design_no_data" android:textAppearance="@style/Text.EV.Secondary" />
|
||||
</LinearLayout>
|
||||
<Space android:layout_width="1dp" android:layout_height="16dp" />
|
||||
<GridLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:columnCount="2">
|
||||
<LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:orientation="vertical" android:minHeight="112dp" android:padding="16dp" android:background="@drawable/bg_battery_card" android:layout_columnWeight="1" android:layout_margin="4dp">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/design_battery_short" android:textAppearance="@style/Text.EV.Secondary" />
|
||||
<TextView android:id="@+id/vehicle_soc" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="—" android:layout_marginTop="6dp" android:textAppearance="@style/Text.EV.Metric.Compact" android:maxLines="1" android:autoSizeTextType="uniform" android:autoSizeMinTextSize="18sp" android:autoSizeMaxTextSize="28sp" android:autoSizeStepGranularity="1sp" />
|
||||
<TextView android:id="@+id/vehicle_charging" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="—" android:textAppearance="@style/Text.EV.Secondary" android:textColor="@color/ev_cyan" android:textSize="16sp" android:maxLines="1" android:ellipsize="end" />
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:orientation="vertical" android:minHeight="112dp" android:padding="16dp" android:background="@drawable/bg_glass" android:layout_columnWeight="1" android:layout_margin="4dp">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/vehicle_row_range" android:textAppearance="@style/Text.EV.Secondary" />
|
||||
<TextView android:id="@+id/vehicle_range" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="—" android:layout_marginTop="6dp" android:textAppearance="@style/Text.EV.Metric.Compact" android:maxLines="1" android:autoSizeTextType="uniform" android:autoSizeMinTextSize="18sp" android:autoSizeMaxTextSize="28sp" android:autoSizeStepGranularity="1sp" />
|
||||
</LinearLayout>
|
||||
<LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:orientation="vertical" android:minHeight="112dp" android:padding="16dp" android:background="@drawable/bg_glass" android:layout_columnWeight="1" android:layout_margin="4dp">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/vehicle_row_power" android:textAppearance="@style/Text.EV.Secondary" />
|
||||
<TextView android:id="@+id/vehicle_power" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="—" android:layout_marginTop="6dp" android:textAppearance="@style/Text.EV.Metric.Compact" android:maxLines="1" android:autoSizeTextType="uniform" android:autoSizeMinTextSize="18sp" android:autoSizeMaxTextSize="28sp" android:autoSizeStepGranularity="1sp" />
|
||||
</LinearLayout>
|
||||
<LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:orientation="vertical" android:minHeight="112dp" android:padding="16dp" android:background="@drawable/bg_glass" android:layout_columnWeight="1" android:layout_margin="4dp">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/vehicle_row_speed" android:textAppearance="@style/Text.EV.Secondary" />
|
||||
<TextView android:id="@+id/vehicle_speed" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="—" android:layout_marginTop="6dp" android:textAppearance="@style/Text.EV.Metric.Compact" android:maxLines="1" android:autoSizeTextType="uniform" android:autoSizeMinTextSize="18sp" android:autoSizeMaxTextSize="28sp" android:autoSizeStepGranularity="1sp" />
|
||||
</LinearLayout>
|
||||
<LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:orientation="vertical" android:minHeight="112dp" android:padding="16dp" android:background="@drawable/bg_glass" android:layout_columnWeight="1" android:layout_margin="4dp" android:layout_columnSpan="2">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/vehicle_row_soe" android:textAppearance="@style/Text.EV.Secondary" />
|
||||
<TextView android:id="@+id/vehicle_soe" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="—" android:layout_marginTop="6dp" android:textAppearance="@style/Text.EV.Metric.Compact" android:maxLines="1" android:autoSizeTextType="uniform" android:autoSizeMinTextSize="18sp" android:autoSizeMaxTextSize="28sp" android:autoSizeStepGranularity="1sp" />
|
||||
</LinearLayout>
|
||||
</GridLayout>
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
@@ -1,122 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
The Wi-Fi page: what the unit is on, a button to join without a trip into Settings,
|
||||
and the list of networks it already has saved.
|
||||
|
||||
The button exists alongside the automatic attempt because the automatic one is
|
||||
rate-limited and only runs while the upload service is up. A driver who has just
|
||||
switched their hotspot on wants it now, not on the next tick.
|
||||
-->
|
||||
<ScrollView 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">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/spacing_lg">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="@dimen/spacing_md"
|
||||
app:cardBackgroundColor="@color/ev_surface"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:strokeColor="@color/ev_outline"
|
||||
app:strokeWidth="1dp"
|
||||
app:cardElevation="3dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/spacing_lg">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="@dimen/spacing_md"
|
||||
android:text="@string/wifi_card_title"
|
||||
android:textAppearance="@style/Text.EV.Title" />
|
||||
|
||||
<TableLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:stretchColumns="0">
|
||||
|
||||
<TableRow style="@style/DataRow">
|
||||
<TextView style="@style/DataLabel" android:text="@string/wifi_current" />
|
||||
<TextView android:id="@+id/wifi_ssid" style="@style/DataValue" />
|
||||
</TableRow>
|
||||
|
||||
<TableRow style="@style/DataRow">
|
||||
<TextView style="@style/DataLabel" android:text="@string/wifi_state" />
|
||||
<TextView android:id="@+id/wifi_state" style="@style/DataValue" />
|
||||
</TableRow>
|
||||
|
||||
</TableLayout>
|
||||
|
||||
<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:layout_marginTop="@dimen/spacing_md"
|
||||
android:text="@string/wifi_connect_now" />
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/wifi_auto_switch"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/spacing_md"
|
||||
android:minHeight="@dimen/touch_target"
|
||||
android:text="@string/wifi_auto_label"
|
||||
android:textAppearance="@style/Text.EV.Body" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/spacing_xs"
|
||||
android:text="@string/wifi_auto_hint"
|
||||
android:textAppearance="@style/Text.EV.Secondary" />
|
||||
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:cardBackgroundColor="@color/ev_surface"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:strokeColor="@color/ev_outline"
|
||||
app:strokeWidth="1dp"
|
||||
app:cardElevation="3dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/spacing_lg">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="@dimen/spacing_sm"
|
||||
android:text="@string/wifi_saved_title"
|
||||
android:textAppearance="@style/Text.EV.Title" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/wifi_saved_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@style/Text.EV.Body"
|
||||
android:lineSpacingExtra="4dp" />
|
||||
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<ScrollView 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>
|
||||
</ScrollView>
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Windows page of the main pager. See pane_abrp.xml for how the pages are held.
|
||||
|
||||
Its own page rather than a card on the Service one: this feature writes to the car and
|
||||
shares nothing with ABRP uploading — no token, no GPS, no interval. Sitting under
|
||||
"Upload Service" implied a dependency that does not exist. -->
|
||||
<ScrollView 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">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/spacing_lg">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="@dimen/spacing_md"
|
||||
app:cardBackgroundColor="@color/ev_surface"
|
||||
app:cardCornerRadius="@dimen/ev_corner"
|
||||
app:strokeColor="@color/ev_outline"
|
||||
app:strokeWidth="1dp"
|
||||
app:cardElevation="0dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/spacing_lg">
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/window_close_switch"
|
||||
style="@style/Widget.EV.Switch"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="@dimen/touch_target"
|
||||
android:text="@string/window_close_title" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/window_close_hint"
|
||||
android:textAppearance="@style/Text.EV.Secondary" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/window_close_status"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/spacing_sm"
|
||||
android:text="@string/window_close_stopped"
|
||||
android:textAppearance="@style/Text.EV.Body" />
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<!-- Event log. Its own card here, where there is room for more than the four lines
|
||||
it had to fit in beside the upload settings. -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:cardBackgroundColor="@color/ev_surface"
|
||||
app:cardCornerRadius="@dimen/ev_corner"
|
||||
app:strokeColor="@color/ev_outline"
|
||||
app:strokeWidth="1dp"
|
||||
app:cardElevation="0dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/spacing_lg">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/window_close_log_title"
|
||||
android:textAppearance="@style/Text.EV.Body" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/window_close_log"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/spacing_sm"
|
||||
android:fontFamily="monospace"
|
||||
android:textAppearance="@style/Text.EV.Secondary" />
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent" android:layout_height="wrap_content"
|
||||
android:minHeight="72dp" android:gravity="center_vertical"
|
||||
android:paddingStart="20dp" android:paddingEnd="20dp"
|
||||
android:paddingTop="12dp" android:paddingBottom="12dp"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:textAppearance="@style/Text.EV.Body" />
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent" android:layout_height="wrap_content"
|
||||
android:minHeight="72dp" android:gravity="center_vertical"
|
||||
android:paddingStart="20dp" android:paddingEnd="20dp"
|
||||
android:paddingTop="12dp" android:paddingBottom="12dp"
|
||||
android:drawableEnd="@drawable/ic_chevron_down" android:drawablePadding="16dp"
|
||||
android:textAppearance="@style/Text.EV.Body" />
|
||||
@@ -1,35 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- EVSuite tokens - DARK theme. See DESIGN.md section Colors.
|
||||
Generated by tools/sync-tokens.mjs and identical in all six apps.
|
||||
|
||||
Dark remains the mode that matters in a vehicle: a light background filling the
|
||||
windscreen at night is glare. The suite moved to DayNight by explicit decision,
|
||||
which does not cancel that constraint - it is the light palette that had to rise
|
||||
to the same 7:1 floor, not the other way round.
|
||||
|
||||
Values not redefined here fall back to values/, which is why ev_icon_tile is
|
||||
declared once, in the light file. -->
|
||||
<resources>
|
||||
<color name="ev_background">#FF0B0D0F</color>
|
||||
<color name="ev_surface">#FF15181C</color>
|
||||
<color name="ev_surface_raised">#FF272C33</color>
|
||||
<!-- 4.70:1 on ev_surface. The previous #4A525B sat at 2.25:1: a border below
|
||||
the 3:1 non-text floor is a border that is not there. -->
|
||||
<color name="ev_outline">#FF7A8492</color>
|
||||
<color name="ev_text_primary">#FFF4F7FA</color>
|
||||
<!-- 8.91:1 worst case (on ev_surface_raised) - the previous #9EA6AE fell to
|
||||
6:1, unreadable in sunlight. -->
|
||||
<color name="ev_text_secondary">#FFC6CFD8</color>
|
||||
<color name="ev_accent">#FF7FD4FA</color>
|
||||
<color name="ev_on_accent">#FF04121A</color>
|
||||
|
||||
<!-- Status. Never the sole carrier of meaning: a text label always accompanies
|
||||
the colour. -->
|
||||
<color name="ev_ok">#FF8FE6A6</color>
|
||||
<color name="ev_warn">#FFFFC978</color>
|
||||
<color name="ev_error">#FFFFACAC</color>
|
||||
|
||||
<!-- Pressed: an alpha veil over the surface, not a different hue. -->
|
||||
<color name="ev_pressed">#1FFFFFFF</color>
|
||||
<color name="ev_pressed_strong">#33FFFFFF</color>
|
||||
</resources>
|
||||
@@ -1,41 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- EVSuite tokens - LIGHT theme. See DESIGN.md section Colors.
|
||||
Generated by tools/sync-tokens.mjs and identical in all six apps: edit
|
||||
tools/tokens/colors.light.xml, not this copy, and re-check the ratios.
|
||||
|
||||
Contrast: this screen is read in daylight, usually through a reflection. Every
|
||||
text colour clears 7:1 (AAA) on every surface it can land on, not the 4.5:1 AA
|
||||
minimum, which collapses as soon as outside brightness rises. The AAA floor
|
||||
applies to the light theme too - a badly contrasted light theme is no more
|
||||
readable than a badly contrasted dark one. -->
|
||||
<!-- User-requested app-specific OLED palette. -->
|
||||
<resources>
|
||||
<color name="ev_background">#FFF2F4F7</color>
|
||||
<color name="ev_surface">#FFFFFFFF</color>
|
||||
<color name="ev_surface_raised">#FFE4E8ED</color>
|
||||
<!-- Card border. Without it, surface and surface_raised merge in sunlight.
|
||||
4.07:1 on ev_surface - above the 3:1 floor for non-text UI (WCAG 1.4.11),
|
||||
which the previous #4A525B (2.25:1) did not clear. -->
|
||||
<color name="ev_outline">#FF737F8F</color>
|
||||
<color name="ev_text_primary">#FF101418</color>
|
||||
<!-- 7.22:1 worst case (on ev_surface_raised). -->
|
||||
<color name="ev_text_secondary">#FF414B55</color>
|
||||
<!-- A deep cyan, not the dark theme's pale one: a pale cyan on white falls to
|
||||
1.5:1 and disappears outright in daylight. 7.69:1 worst case. -->
|
||||
<color name="ev_accent">#FF044C63</color>
|
||||
<color name="ev_on_accent">#FFFFFFFF</color>
|
||||
|
||||
<!-- Status. Never the sole carrier of meaning: a text label always accompanies
|
||||
the colour. -->
|
||||
<color name="ev_ok">#FF09501F</color>
|
||||
<color name="ev_warn">#FF6B3A00</color>
|
||||
<color name="ev_error">#FF8F0F18</color>
|
||||
|
||||
<!-- Pressed: an alpha veil over the surface, not a different hue, so a pressed
|
||||
card still reads as the same card. -->
|
||||
<color name="ev_pressed">#14000000</color>
|
||||
<color name="ev_pressed_strong">#29000000</color>
|
||||
|
||||
<!-- Launcher icon tile. The same in light and dark: it is the suite's mark, not
|
||||
a surface. See DESIGN.md section Iconography. -->
|
||||
<color name="ev_icon_tile">#FF2A2A2A</color>
|
||||
<color name="ev_background">#000000</color>
|
||||
<color name="ev_surface">#121212</color>
|
||||
<color name="ev_surface_raised">#1B1D1E</color>
|
||||
<color name="ev_outline">#363B3D</color>
|
||||
<color name="ev_text_primary">#F4F3EF</color>
|
||||
<color name="ev_text_secondary">#BDC4C6</color>
|
||||
<color name="ev_accent">#D8C39A</color>
|
||||
<color name="ev_on_accent">#17140E</color>
|
||||
<color name="ev_cyan">#A0E2E0</color>
|
||||
<color name="ev_cyan_deep">#103436</color>
|
||||
<color name="ev_gold_surface">#28241B</color>
|
||||
<color name="ev_ok">#A4DCC1</color>
|
||||
<color name="ev_warn">#E9CA96</color>
|
||||
<color name="ev_error">#FFB4AB</color>
|
||||
<color name="ev_pressed">#16FFFFFF</color>
|
||||
<color name="ev_pressed_strong">#28FFFFFF</color>
|
||||
<color name="ev_icon_tile">#121212</color>
|
||||
<color name="ev_transparent">#00000000</color>
|
||||
</resources>
|
||||
|
||||
@@ -1,27 +1,12 @@
|
||||
<resources>
|
||||
<string name="window_close_title">Kapı açılınca camları kapat</string>
|
||||
<string name="window_close_hint">P konumunda ve araç dururken WinClose kapı sinyaliyle dört cama 5 saniye kapatma komutu gönderilir. ABRP yüklemesinden bağımsız çalışır.</string>
|
||||
<string name="window_close_stopped">Cam kapatma kapalı</string>
|
||||
<string name="window_close_waiting">WinClose kapı sinyali bekleniyor</string>
|
||||
<string name="window_close_closing">Dört cama kapatma komutu gönderiliyor…</string>
|
||||
<string name="window_close_sent">5 saniyelik komut tamamlandı. Cam konumu sensörsüz olduğu için doğrulanamıyor.</string>
|
||||
<string name="window_close_interrupted">Kapatma durdu veya komut gönderilemedi</string>
|
||||
<string name="window_close_unavailable">Cam bağlantısı henüz hazır değil</string>
|
||||
<string name="window_close_start_failed">Cam servisi başlatılamadı</string>
|
||||
<string name="window_close_unsupported">Bu cam entegrasyonu yalnızca SWI69 için etkin</string>
|
||||
<string name="window_close_no_system_uid">Bu sürüm araca yazamıyor (sistem imzası gerekli)</string>
|
||||
<string name="window_close_notification">P ve 0 km/sa: kapı açılınca dört camı kapat</string>
|
||||
<string name="window_close_disable">Kapat</string>
|
||||
<string name="window_close_log_title">Olay kaydı</string>
|
||||
<string name="app_name">Fatih\'sMG4</string>
|
||||
|
||||
<!-- Main screen -->
|
||||
<string name="tab_abrp">ABRP</string>
|
||||
<string name="tab_service">Upload Service</string>
|
||||
<string name="tab_vehicle">Araç</string>
|
||||
<string name="tab_windows">Pencereler</string>
|
||||
<string name="tab_service">Aktarım</string>
|
||||
<string name="tab_vehicle">Genel</string>
|
||||
<string name="tab_wifi">Wi-Fi</string>
|
||||
<string name="tab_log">Log</string>
|
||||
<string name="tab_log">Kayıtlar</string>
|
||||
|
||||
<!-- Wi-Fi page -->
|
||||
<string name="wifi_card_title">Wi-Fi Bağlantısı</string>
|
||||
@@ -62,9 +47,9 @@
|
||||
<string name="helper_token">Find your token in the ABRP app: Settings → Connections → Generic (Manual Entry)</string>
|
||||
<string name="desc_toggle_api_key">Show or hide API key</string>
|
||||
<string name="desc_toggle_token">Show or hide user token</string>
|
||||
<string name="btn_save">Save</string>
|
||||
<string name="btn_test">Test Connection</string>
|
||||
<string name="btn_import">Import file</string>
|
||||
<string name="btn_save">Kaydet</string>
|
||||
<string name="btn_test">Bağlantıyı test et</string>
|
||||
<string name="btn_import">Dosyadan al</string>
|
||||
|
||||
<!-- Config file import -->
|
||||
<string name="import_ok">Config imported — press Test to verify</string>
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0"?>
|
||||
<resources>
|
||||
<string name="design_battery_short">Batarya</string>
|
||||
<string name="design_probe">VHAL Probe</string>
|
||||
<string name="design_brand">MG⁴</string>
|
||||
<string name="design_brand_caption">Fatih’s MG4</string>
|
||||
<string name="design_brand_footer">ELEKTRİKLE YOLDA</string>
|
||||
<string name="design_overview_title">Sürüşe bir bakış.</string>
|
||||
<string name="design_overview_subtitle">Aracından gelen veriler, tek bir yerde.</string>
|
||||
<string name="design_abrp_title">Rotana bağlan.</string>
|
||||
<string name="design_abrp_subtitle">ABRP hesabınla aracını buluştur.</string>
|
||||
<string name="design_service_title">Sen yola odaklan.</string>
|
||||
<string name="design_service_subtitle">Verilerin arka planda güncel kalsın.</string>
|
||||
<string name="design_wifi_title">Bağlantıda kal.</string>
|
||||
<string name="design_wifi_subtitle">Yolculuğunun internet bağlantısı.</string>
|
||||
<string name="design_log_title">Her aktarım, burada.</string>
|
||||
<string name="design_log_subtitle">Son gönderimlerin durumu ve ayrıntıları.</string>
|
||||
<string name="design_battery">BATARYA</string>
|
||||
<string name="design_energy">ENERJİ VE SICAKLIK</string>
|
||||
<string name="design_tires">LASTİK BASINÇLARI</string>
|
||||
<string name="design_account">HESAP BAĞLANTISI</string>
|
||||
<string name="design_private_title">Sana özel bağlantı.</string>
|
||||
<string name="design_private_body">Token, aracını ABRP hesabına bağlar. Bilgilerin bu cihazda şifreli saklanır.</string>
|
||||
<string name="design_no_data">Okunamayan veriler — olarak gösterilir.</string>
|
||||
<string name="design_service_controls">AKTARIM KONTROLÜ</string>
|
||||
<string name="design_service_timing">GÜNCELLEME SIKLIĞI</string>
|
||||
<string name="design_car_desc">Lastik basınçlarının araç üzerindeki konumları</string>
|
||||
</resources>
|
||||
@@ -1,69 +1,88 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Per-app styles (not synced by tools/sync-tokens.mjs — see styles_ev.xml for the
|
||||
shared set). -->
|
||||
<resources>
|
||||
|
||||
<!-- Icon-over-label nav tab, identical to EVProfile's and EVTasker's. The tabs
|
||||
share the bar's width equally, so the label has to shrink rather than push its
|
||||
neighbours around: hence autoSize and a single line. Selected state is
|
||||
View.isSelected, set by MainActivity.markCurrentPage, read by the
|
||||
res/color/nav_tab_*.xml selectors — no color is ever set from code. -->
|
||||
<style name="Widget.EV.NavTab" parent="Widget.Material3.Button.OutlinedButton">
|
||||
<item name="android:minWidth">0dp</item>
|
||||
<item name="android:minHeight">0dp</item>
|
||||
<item name="android:insetTop">0dp</item>
|
||||
<item name="android:insetBottom">0dp</item>
|
||||
<item name="android:paddingStart">@dimen/spacing_xs</item>
|
||||
<item name="android:paddingEnd">@dimen/spacing_xs</item>
|
||||
<item name="android:paddingTop">@dimen/spacing_xs</item>
|
||||
<item name="android:paddingBottom">@dimen/spacing_xs</item>
|
||||
<item name="android:maxLines">1</item>
|
||||
<item name="android:textStyle">bold</item>
|
||||
<item name="android:letterSpacing">0.02</item>
|
||||
<item name="android:textAllCaps">false</item>
|
||||
<item name="android:textColor">@color/nav_tab_foreground</item>
|
||||
<item name="autoSizeTextType">uniform</item>
|
||||
<item name="autoSizeMinTextSize">12sp</item>
|
||||
<item name="autoSizeMaxTextSize">@dimen/text_label</item>
|
||||
<item name="iconGravity">top</item>
|
||||
<item name="iconSize">28dp</item>
|
||||
<item name="iconPadding">@dimen/spacing_xs</item>
|
||||
<item name="iconTint">@color/nav_tab_foreground</item>
|
||||
<item name="backgroundTint">@color/nav_tab_background</item>
|
||||
<item name="strokeColor">@color/nav_tab_stroke</item>
|
||||
<item name="strokeWidth">1dp</item>
|
||||
<item name="cornerRadius">@dimen/ev_corner</item>
|
||||
<item name="elevation">0dp</item>
|
||||
</style>
|
||||
|
||||
<!-- Vehicle page. A label/value table: the label takes the slack (stretchColumns=0)
|
||||
and the value is right-aligned so the numbers line up down the column, which
|
||||
is what makes a reading legible at a glance from the driver's seat. -->
|
||||
<style name="DataRow" parent="">
|
||||
<item name="android:layout_width">match_parent</item>
|
||||
<item name="android:layout_height">wrap_content</item>
|
||||
<item name="android:paddingTop">@dimen/spacing_sm</item>
|
||||
<item name="android:paddingBottom">@dimen/spacing_sm</item>
|
||||
<item name="android:gravity">center_vertical</item>
|
||||
</style>
|
||||
|
||||
<style name="DataLabel" parent="Text.EV.Body">
|
||||
<item name="android:layout_width">wrap_content</item>
|
||||
<item name="android:layout_height">wrap_content</item>
|
||||
<item name="android:textColor">@color/ev_text_secondary</item>
|
||||
</style>
|
||||
|
||||
<style name="DataValue" parent="Text.EV.Body">
|
||||
<item name="android:layout_width">wrap_content</item>
|
||||
<item name="android:layout_height">wrap_content</item>
|
||||
<item name="android:paddingStart">@dimen/spacing_md</item>
|
||||
<item name="android:gravity">end</item>
|
||||
<item name="android:textStyle">bold</item>
|
||||
<item name="android:textColor">@color/ev_text_primary</item>
|
||||
<!-- Placeholder, replaced on the first refresh tick. A dash, never a zero:
|
||||
see the note at the top of pane_vehicle.xml. -->
|
||||
<item name="android:text">—</item>
|
||||
</style>
|
||||
|
||||
|
||||
<style name="Widget.EV.NavTab" parent="Widget.Material3.Button.OutlinedButton">
|
||||
<item name="android:minWidth">0dp</item>
|
||||
<item name="android:minHeight">64dp</item>
|
||||
<item name="android:insetTop">0dp</item>
|
||||
<item name="android:insetBottom">0dp</item>
|
||||
<item name="android:paddingStart">16dp</item>
|
||||
<item name="android:paddingEnd">12dp</item>
|
||||
<item name="android:gravity">start|center_vertical</item>
|
||||
<item name="android:maxLines">1</item>
|
||||
<item name="android:fontFamily">sans-serif-medium</item>
|
||||
<item name="android:textSize">16sp</item>
|
||||
<item name="android:textAllCaps">false</item>
|
||||
<item name="android:textColor">@color/nav_tab_foreground</item>
|
||||
<item name="iconGravity">start</item>
|
||||
<item name="iconSize">24dp</item>
|
||||
<item name="iconPadding">12dp</item>
|
||||
<item name="iconTint">@color/nav_tab_foreground</item>
|
||||
<item name="backgroundTint">@color/nav_tab_background</item>
|
||||
<item name="strokeColor">@color/nav_tab_stroke</item>
|
||||
<item name="strokeWidth">1dp</item>
|
||||
<item name="cornerRadius">18dp</item>
|
||||
<item name="elevation">0dp</item>
|
||||
</style>
|
||||
<style name="Widget.EV.NavTab.Compact">
|
||||
<item name="android:paddingStart">2dp</item>
|
||||
<item name="android:paddingEnd">2dp</item>
|
||||
<item name="android:gravity">center</item>
|
||||
<item name="iconGravity">top</item>
|
||||
<item name="iconPadding">5dp</item>
|
||||
</style>
|
||||
<style name="Widget.EV.Input" parent="Widget.Material3.TextInputLayout.OutlinedBox">
|
||||
<item name="boxBackgroundColor">@color/ev_background</item>
|
||||
<item name="boxStrokeColor">@color/input_stroke</item>
|
||||
<item name="boxStrokeWidth">1dp</item>
|
||||
<item name="boxStrokeWidthFocused">2dp</item>
|
||||
<item name="boxCornerRadiusTopStart">16dp</item>
|
||||
<item name="boxCornerRadiusTopEnd">16dp</item>
|
||||
<item name="boxCornerRadiusBottomStart">16dp</item>
|
||||
<item name="boxCornerRadiusBottomEnd">16dp</item>
|
||||
<item name="hintTextColor">@color/ev_accent</item>
|
||||
<item name="endIconTint">@color/ev_text_secondary</item>
|
||||
<item name="helperTextTextAppearance">@style/Text.EV.Secondary</item>
|
||||
</style>
|
||||
<style name="Text.EV.Eyebrow" parent="Text.EV.SectionHeader">
|
||||
<item name="android:textSize">16sp</item>
|
||||
<item name="android:textColor">@color/ev_accent</item>
|
||||
<item name="android:fontFamily">sans-serif-medium</item>
|
||||
<item name="android:letterSpacing">0.12</item>
|
||||
</style>
|
||||
<style name="Text.EV.Hero" parent="Text.EV.Display">
|
||||
<item name="android:textSize">48sp</item>
|
||||
<item name="android:fontFamily">sans-serif-light</item>
|
||||
<item name="android:letterSpacing">-0.025</item>
|
||||
<item name="android:fontFeatureSettings">tnum</item>
|
||||
</style>
|
||||
<style name="Text.EV.Metric" parent="Text.EV.Display">
|
||||
<item name="android:textSize">34sp</item>
|
||||
<item name="android:fontFamily">sans-serif-light</item>
|
||||
<item name="android:fontFeatureSettings">tnum</item>
|
||||
</style>
|
||||
<style name="Text.EV.Metric.Compact">
|
||||
<item name="android:textSize">28sp</item>
|
||||
</style>
|
||||
<style name="DataRow" parent="">
|
||||
<item name="android:layout_width">match_parent</item>
|
||||
<item name="android:layout_height">wrap_content</item>
|
||||
<item name="android:minHeight">48dp</item>
|
||||
<item name="android:paddingTop">10dp</item>
|
||||
<item name="android:paddingBottom">10dp</item>
|
||||
<item name="android:gravity">center_vertical</item>
|
||||
</style>
|
||||
<style name="DataLabel" parent="Text.EV.Secondary">
|
||||
<item name="android:layout_width">wrap_content</item>
|
||||
<item name="android:layout_height">wrap_content</item>
|
||||
</style>
|
||||
<style name="DataValue" parent="Text.EV.Body">
|
||||
<item name="android:layout_width">wrap_content</item>
|
||||
<item name="android:layout_height">wrap_content</item>
|
||||
<item name="android:paddingStart">16dp</item>
|
||||
<item name="android:gravity">end</item>
|
||||
<item name="android:fontFamily">sans-serif-medium</item>
|
||||
<item name="android:fontFeatureSettings">tnum</item>
|
||||
<item name="android:textColor">@color/ev_text_primary</item>
|
||||
<item name="android:text">—</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- EVSuite - component styles. See DESIGN.md section Components.
|
||||
Generated by tools/sync-tokens.mjs: identical in all six apps, edit only
|
||||
tools/tokens/styles.xml. The application theme itself stays in values/themes.xml,
|
||||
because its name changes from one app to the next.
|
||||
|
||||
These styles carry the same names everywhere, so a screen can move between
|
||||
repositories without being renamed. -->
|
||||
<!-- EVABRPUploader components. The user-requested OLED theme intentionally
|
||||
overrides the shared EVSuite tokens; see AGENTS.md for the local design direction. -->
|
||||
<resources>
|
||||
|
||||
<!-- Dialogs inherit the same contrast as everything else: without this they fall
|
||||
@@ -24,6 +19,12 @@
|
||||
away. This is the minimum used everywhere in the suite, including where
|
||||
Material offers something much smaller. -->
|
||||
<style name="Widget.EV.Button" parent="Widget.Material3.Button">
|
||||
<item name="android:textAllCaps">false</item>
|
||||
<item name="android:fontFamily">sans-serif-medium</item>
|
||||
<item name="android:letterSpacing">0.01</item>
|
||||
<item name="backgroundTint">@color/ev_accent</item>
|
||||
<item name="android:textColor">@color/ev_on_accent</item>
|
||||
<item name="elevation">0dp</item>
|
||||
<item name="shapeAppearanceOverlay">@style/ShapeAppearanceOverlay.EV.Button</item>
|
||||
<item name="android:minHeight">@dimen/touch_target</item>
|
||||
<item name="android:textSize">@dimen/text_body</item>
|
||||
@@ -32,6 +33,11 @@
|
||||
</style>
|
||||
|
||||
<style name="Widget.EV.Button.Outlined" parent="Widget.Material3.Button.OutlinedButton">
|
||||
<item name="android:textAllCaps">false</item>
|
||||
<item name="android:fontFamily">sans-serif-medium</item>
|
||||
<item name="backgroundTint">@color/ev_surface_raised</item>
|
||||
<item name="strokeColor">@color/ev_outline</item>
|
||||
<item name="android:textColor">@color/ev_text_primary</item>
|
||||
<item name="shapeAppearanceOverlay">@style/ShapeAppearanceOverlay.EV.Button</item>
|
||||
<item name="android:minHeight">@dimen/touch_target</item>
|
||||
<item name="android:textSize">@dimen/text_body</item>
|
||||
@@ -64,14 +70,14 @@
|
||||
<item name="android:textStyle">bold</item>
|
||||
</style>
|
||||
|
||||
<!-- One Display element per screen: two 32sp numbers competing for attention are
|
||||
two glances, and the glance is the budget. -->
|
||||
<!-- Shared typography base; the overview uses larger hero and metric variants. -->
|
||||
<style name="Text.EV.Display" parent="TextAppearance.Material3.HeadlineSmall">
|
||||
<item name="android:textSize">@dimen/text_display</item>
|
||||
<item name="android:textColor">@color/ev_text_primary</item>
|
||||
</style>
|
||||
|
||||
<style name="Text.EV.Title" parent="TextAppearance.Material3.TitleLarge">
|
||||
<item name="android:fontFamily">sans-serif-medium</item>
|
||||
<item name="android:textSize">@dimen/text_title</item>
|
||||
<item name="android:textColor">@color/ev_text_primary</item>
|
||||
</style>
|
||||
@@ -86,11 +92,8 @@
|
||||
<item name="android:textColor">@color/ev_text_secondary</item>
|
||||
</style>
|
||||
|
||||
<!-- The suite radius applied to buttons. Material3 buttons are fully rounded by
|
||||
default, which reads as a different design language from every card and dialog
|
||||
around them. This lived in EVSwipe only, in a generated file, so the first sync
|
||||
deleted it — one radius across the suite has to be a token, not a local edit. -->
|
||||
<!-- Rounded corners echo the glass panels without making every button a pill. -->
|
||||
<style name="ShapeAppearanceOverlay.EV.Button" parent="">
|
||||
<item name="cornerSize">@dimen/ev_corner</item>
|
||||
<item name="cornerSize">18dp</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<!-- DayNight, not a forced dark theme: the suite follows the vehicle's day/night
|
||||
setting. Dark is still the mode that matters - a light background filling the
|
||||
windscreen at night is glare - which is why the light palette was raised to
|
||||
the same 7:1 floor as the dark one rather than the other way round.
|
||||
Shared component styles live in styles_ev.xml. -->
|
||||
<!-- User-requested OLED theme. MainActivity fixes night mode for system widgets too. -->
|
||||
<style name="Theme.EVABRPUploader" parent="Theme.Material3.DayNight.NoActionBar">
|
||||
<item name="colorPrimary">@color/ev_accent</item>
|
||||
<item name="colorSecondary">@color/ev_cyan</item>
|
||||
<item name="colorControlActivated">@color/ev_cyan</item>
|
||||
<item name="android:fontFamily">sans-serif</item>
|
||||
<item name="android:windowLightStatusBar">false</item>
|
||||
<item name="android:windowLightNavigationBar">false</item>
|
||||
<item name="android:navigationBarColor">@color/ev_background</item>
|
||||
<item name="android:forceDarkAllowed" tools:targetApi="q">false</item>
|
||||
<item name="colorOnPrimary">@color/ev_on_accent</item>
|
||||
<item name="colorSurface">@color/ev_surface</item>
|
||||
<item name="colorSurfaceContainer">@color/ev_surface_raised</item>
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
import org.junit.Test;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class WinCloseDoorSignalTest {
|
||||
@Test public void firstOpenEventWorksWithoutDrivingOrArming() {
|
||||
assertTrue(new WinCloseDoorSignal().opening(0));
|
||||
}
|
||||
|
||||
@Test public void repeatedOpenIsIgnoredButAReopeningTriggers() {
|
||||
WinCloseDoorSignal signal = new WinCloseDoorSignal();
|
||||
assertFalse(signal.opening(2));
|
||||
assertTrue(signal.opening(0));
|
||||
assertFalse(signal.opening(0));
|
||||
assertFalse(signal.opening(1));
|
||||
assertFalse(signal.opening(2));
|
||||
assertTrue(signal.opening(0));
|
||||
}
|
||||
|
||||
@Test public void aSeededOpenDoorIsNotTreatedAsSomeoneOpeningIt() {
|
||||
WinCloseDoorSignal signal = new WinCloseDoorSignal();
|
||||
// What a fresh listener registration replays: the door is already open.
|
||||
signal.seed(0);
|
||||
assertFalse(signal.opening(0));
|
||||
// Closing it and opening it again is a real edge.
|
||||
assertFalse(signal.opening(2));
|
||||
assertTrue(signal.opening(0));
|
||||
}
|
||||
|
||||
@Test public void seedingAClosedDoorStillArmsTheNextOpening() {
|
||||
WinCloseDoorSignal signal = new WinCloseDoorSignal();
|
||||
signal.seed(2);
|
||||
assertTrue(signal.opening(0));
|
||||
}
|
||||
|
||||
@Test public void seedIgnoresAnInvalidReadingRatherThanRecordingIt() {
|
||||
WinCloseDoorSignal signal = new WinCloseDoorSignal();
|
||||
signal.seed(0);
|
||||
signal.seed(-1);
|
||||
assertFalse(signal.opening(0));
|
||||
}
|
||||
|
||||
@Test public void invalidReadingDoesNotInventAnotherOpening() {
|
||||
WinCloseDoorSignal signal = new WinCloseDoorSignal();
|
||||
assertTrue(signal.opening(0));
|
||||
assertFalse(signal.opening(-1));
|
||||
assertFalse(signal.opening(0));
|
||||
signal.reset();
|
||||
assertTrue(signal.opening(0));
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
import org.junit.Test;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class WindowClosePolicyTest {
|
||||
@Test public void onlyKnownParkAndStandstillCanStart() {
|
||||
WindowClosePolicy policy = new WindowClosePolicy();
|
||||
assertFalse(policy.begin(null, 0f));
|
||||
assertFalse(policy.begin(false, 0f));
|
||||
assertFalse(policy.begin(true, null));
|
||||
assertFalse(policy.begin(true, 0.1f));
|
||||
assertFalse(policy.begin(true, -1f));
|
||||
assertFalse(policy.begin(true, Float.NaN));
|
||||
assertFalse(policy.begin(true, Float.POSITIVE_INFINITY));
|
||||
assertTrue(policy.begin(true, 0f));
|
||||
}
|
||||
|
||||
@Test public void openingsDuringACloseCannotExtendOrQueueIt() {
|
||||
long[] now = {0L};
|
||||
WindowClosePolicy policy = new WindowClosePolicy(() -> now[0]);
|
||||
assertTrue(policy.begin(true, 0f));
|
||||
assertFalse(policy.begin(true, 0f));
|
||||
assertFalse(policy.begin(true, 0f));
|
||||
policy.finish();
|
||||
// A later, distinct opening needs no previous drive or arming timer.
|
||||
now[0] += WindowClosePolicy.COOLDOWN_MS;
|
||||
assertTrue(policy.begin(true, 0f));
|
||||
}
|
||||
|
||||
@Test public void reopeningTheDoorWithinTheCooldownDoesNotCloseAgain() {
|
||||
long[] now = {0L};
|
||||
WindowClosePolicy policy = new WindowClosePolicy(() -> now[0]);
|
||||
assertTrue(policy.begin(true, 0f));
|
||||
now[0] += 5_000L;
|
||||
policy.finish();
|
||||
// Reaching back into the car reopens the door; the glass must not run a second pulse.
|
||||
now[0] += WindowClosePolicy.COOLDOWN_MS - 1;
|
||||
assertFalse(policy.begin(true, 0f));
|
||||
now[0] += 1;
|
||||
assertTrue(policy.begin(true, 0f));
|
||||
}
|
||||
|
||||
@Test public void theCooldownIsMeasuredFromTheEndOfTheCloseNotItsStart() {
|
||||
long[] now = {0L};
|
||||
WindowClosePolicy policy = new WindowClosePolicy(() -> now[0]);
|
||||
assertTrue(policy.begin(true, 0f));
|
||||
now[0] += WindowClosePulse.DURATION_MS;
|
||||
policy.finish();
|
||||
now[0] += WindowClosePolicy.COOLDOWN_MS - WindowClosePulse.DURATION_MS;
|
||||
assertFalse(policy.begin(true, 0f));
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
import org.junit.Test;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class WindowClosePulseTest {
|
||||
private final List<String> writes = new ArrayList<>();
|
||||
private long now;
|
||||
|
||||
private boolean send(int area, int command) {
|
||||
writes.add(area + ":" + command);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void assertStoppedAll() {
|
||||
assertEquals(Arrays.asList("0:0", "1:0", "2:0", "3:0"),
|
||||
writes.subList(writes.size() - 4, writes.size()));
|
||||
}
|
||||
|
||||
@Test public void comfortPulseTargetsAllFourAndEndsAtFiveSeconds() {
|
||||
assertTrue(WindowClosePulse.run(() -> true, this::send, () -> now, ms -> now += ms));
|
||||
assertEquals(5_000L, now);
|
||||
assertEquals(Arrays.asList("0:1", "1:1", "2:1", "3:1"), writes.subList(0, 4));
|
||||
// Forty-two UP batches at 120 ms intervals, followed by a single STOP batch.
|
||||
assertEquals(42 * 4 + 4, writes.size());
|
||||
assertStoppedAll();
|
||||
}
|
||||
|
||||
@Test public void refusedStartWritesNothing() {
|
||||
assertFalse(WindowClosePulse.run(() -> false, this::send, () -> now, ms -> now += ms));
|
||||
assertTrue(writes.isEmpty());
|
||||
}
|
||||
|
||||
@Test public void leavingParkMovingOrDisablingStopsAtNextPulse() {
|
||||
assertFalse(WindowClosePulse.run(() -> now < 120L, this::send, () -> now, ms -> now += ms));
|
||||
assertEquals(8, writes.size());
|
||||
assertStoppedAll();
|
||||
}
|
||||
|
||||
@Test public void interruptionReleasesEveryWindowAndPreservesInterrupt() {
|
||||
try {
|
||||
assertFalse(WindowClosePulse.run(() -> true, this::send, () -> now,
|
||||
ms -> { throw new InterruptedException(); }));
|
||||
assertTrue(Thread.currentThread().isInterrupted());
|
||||
assertStoppedAll();
|
||||
} finally {
|
||||
Thread.interrupted();
|
||||
}
|
||||
}
|
||||
|
||||
@Test public void oneFailedCommandEndsPulseAndReleasesEveryWindow() {
|
||||
assertFalse(WindowClosePulse.run(() -> true, (area, command) -> {
|
||||
send(area, command);
|
||||
return area != 2 || command == 0;
|
||||
}, () -> now, ms -> now += ms));
|
||||
assertEquals(0L, now);
|
||||
assertStoppedAll();
|
||||
}
|
||||
|
||||
@Test public void unexpectedFailureStillReleasesEveryWindow() {
|
||||
try {
|
||||
WindowClosePulse.run(() -> true, (area, command) -> {
|
||||
send(area, command);
|
||||
if (area == 1 && command == 1) throw new IllegalStateException();
|
||||
return true;
|
||||
}, () -> now, ms -> now += ms);
|
||||
fail("Expected sender failure");
|
||||
} catch (IllegalStateException expected) {
|
||||
assertStoppedAll();
|
||||
}
|
||||
}
|
||||
|
||||
@Test public void aFailedStopDoesNotPreventOtherStops() {
|
||||
assertFalse(WindowClosePulse.run(() -> true, (area, command) -> {
|
||||
send(area, command);
|
||||
if (area == 0 && command == 0) throw new IllegalStateException();
|
||||
return true;
|
||||
}, () -> now, ms -> now += ms));
|
||||
assertStoppedAll();
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,16 @@ import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* OTA update policy — unstable channel only. The updater installs code on a vehicle, so the
|
||||
@@ -111,6 +121,109 @@ public class OtaUpdaterTest {
|
||||
assertArrayEquals(new int[]{1, 0, 5}, OtaUpdater.segments("1.x.5"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void newestAssetWinsEvenWhenItIsLaterInTheSameRelease() throws Exception {
|
||||
JSONArray releases = new JSONArray().put(release(true,
|
||||
asset("2.2.3"), asset("2.2.6"), asset("2.2.4")))
|
||||
.put(release(true, asset("2.2.5")));
|
||||
OtaUpdater.Update update = OtaUpdater.selectUpdate(releases, "2.2.2.0-unstable");
|
||||
assertEquals("2.2.6", update.versionName);
|
||||
assertTrue(update.apkUrl.endsWith("FatihsMG4-unstable-2.2.6.apk"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stableDraftForeignAndUnversionedAssetsCannotWin() throws Exception {
|
||||
JSONObject foreign = asset("9.0.0").put("browser_download_url", "https://example.com/app.apk");
|
||||
JSONObject unversioned = asset("9.0.0").put("name", "FatihsMG4-unstable.apk");
|
||||
JSONObject stableName = asset("9.0.0").put("name", "FatihsMG4-stable-9.0.0.apk");
|
||||
JSONArray releases = new JSONArray()
|
||||
.put(release(false, asset("9.0.0")))
|
||||
.put(release(true, asset("9.0.0")).put("draft", true))
|
||||
.put(release(true, foreign, unversioned, stableName, asset("2.2.5")));
|
||||
assertEquals("2.2.5", OtaUpdater.selectUpdate(releases, "2.2.4.0-unstable").versionName);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void successfulCheckFindsUpdateAndDisconnects() throws Exception {
|
||||
Response response = new Response(200,
|
||||
new JSONArray().put(release(true, asset("2.2.5"))).toString());
|
||||
assertEquals("2.2.5", OtaUpdater.check("2.2.4.0-unstable", response).versionName);
|
||||
assertTrue(response.disconnected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void successfulCheckWithOnlyOlderOrEqualVersionsIsCurrent() throws Exception {
|
||||
Response response = new Response(200,
|
||||
new JSONArray().put(release(true, asset("2.2.3"), asset("2.2.5"))).toString());
|
||||
assertNull(OtaUpdater.check("2.2.5.0-unstable", response));
|
||||
assertTrue(response.disconnected);
|
||||
}
|
||||
|
||||
@Test(expected = IOException.class)
|
||||
public void rateLimitMustNotBeReportedAsCurrent() throws Exception {
|
||||
Response response = new Response(403, "{}");
|
||||
try {
|
||||
OtaUpdater.check("2.2.4.0-unstable", response);
|
||||
} finally {
|
||||
assertTrue(response.disconnected);
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = IOException.class)
|
||||
public void malformedResponseMustNotBeReportedAsCurrent() throws Exception {
|
||||
Response response = new Response(200, "<html>unavailable</html>");
|
||||
try {
|
||||
OtaUpdater.check("2.2.4.0-unstable", response);
|
||||
} finally {
|
||||
assertTrue(response.disconnected);
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = SocketTimeoutException.class)
|
||||
public void timeoutMustNotBeReportedAsCurrent() throws Exception {
|
||||
Response response = new Response(200, "[]") {
|
||||
@Override public InputStream getInputStream() throws IOException {
|
||||
throw new SocketTimeoutException("test timeout");
|
||||
}
|
||||
};
|
||||
try {
|
||||
OtaUpdater.check("2.2.4.0-unstable", response);
|
||||
} finally {
|
||||
assertTrue(response.disconnected);
|
||||
}
|
||||
}
|
||||
|
||||
private static JSONObject asset(String version) throws Exception {
|
||||
String name = "FatihsMG4-unstable-" + version + ".apk";
|
||||
return new JSONObject().put("name", name).put("browser_download_url",
|
||||
"https://github.com/fatihdonmezdev/MG4ABRP/releases/download/unstable/" + name);
|
||||
}
|
||||
|
||||
private static JSONObject release(boolean prerelease, JSONObject... assets) throws Exception {
|
||||
JSONArray list = new JSONArray();
|
||||
for (JSONObject asset : assets) list.put(asset);
|
||||
return new JSONObject().put("prerelease", prerelease).put("assets", list);
|
||||
}
|
||||
|
||||
private static class Response extends HttpURLConnection {
|
||||
private final int status;
|
||||
private final String body;
|
||||
boolean disconnected;
|
||||
|
||||
Response(int status, String body) throws Exception {
|
||||
super(new URL("https://api.github.com/repos/fatihdonmezdev/MG4ABRP/releases"));
|
||||
this.status = status;
|
||||
this.body = body;
|
||||
}
|
||||
@Override public int getResponseCode() { return status; }
|
||||
@Override public InputStream getInputStream() throws IOException {
|
||||
return new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
@Override public void disconnect() { disconnected = true; }
|
||||
@Override public boolean usingProxy() { return false; }
|
||||
@Override public void connect() { }
|
||||
}
|
||||
|
||||
// The `pm install -r` exit-code test that stood here is gone with the shell-out it
|
||||
// covered. Installation is now a PackageInstaller session, whose every step needs a real
|
||||
// PackageManager — there is no pure part of it left to exercise on the JVM. What used to
|
||||
|
||||
@@ -7,12 +7,14 @@ import android.content.pm.PackageInstaller;
|
||||
import android.util.Log;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
@@ -78,7 +80,7 @@ final class OtaUpdater {
|
||||
}
|
||||
}
|
||||
|
||||
/** Result of a check: null when there is nothing newer or the check failed. */
|
||||
/** Result of a successful check: null means there is no newer eligible release. */
|
||||
static final class Update {
|
||||
final String versionName;
|
||||
final String apkUrl;
|
||||
@@ -165,19 +167,21 @@ final class OtaUpdater {
|
||||
* Asks GitHub for the newest pre-release and returns it if it beats [currentVersion].
|
||||
* Runs on the caller's thread — never call from the main thread.
|
||||
*/
|
||||
static Update check(String currentVersion) {
|
||||
HttpURLConnection conn = null;
|
||||
static Update check(String currentVersion) throws IOException {
|
||||
return check(currentVersion, (HttpURLConnection) new URL(RELEASES_API).openConnection());
|
||||
}
|
||||
|
||||
/** Connection seam for exercising real response handling without a device or network. */
|
||||
static Update check(String currentVersion, HttpURLConnection conn) throws IOException {
|
||||
try {
|
||||
conn = (HttpURLConnection) new URL(RELEASES_API).openConnection();
|
||||
conn.setInstanceFollowRedirects(false);
|
||||
conn.setRequestProperty("Accept", "application/vnd.github.v3+json");
|
||||
conn.setRequestProperty("User-Agent", "EVABRPUploader-Android");
|
||||
conn.setConnectTimeout(TIMEOUT_MS);
|
||||
conn.setReadTimeout(TIMEOUT_MS);
|
||||
if (conn.getResponseCode() != 200) {
|
||||
Log.w(TAG, "Release API returned " + conn.getResponseCode());
|
||||
return null;
|
||||
}
|
||||
int status = conn.getResponseCode();
|
||||
if (status != HttpURLConnection.HTTP_OK)
|
||||
throw new IOException("Release API returned " + status);
|
||||
|
||||
StringBuilder body = new StringBuilder();
|
||||
try (BufferedReader br = new BufferedReader(new InputStreamReader(
|
||||
@@ -186,54 +190,36 @@ final class OtaUpdater {
|
||||
while ((line = br.readLine()) != null) body.append(line);
|
||||
}
|
||||
|
||||
JSONArray releases = new JSONArray(body.toString());
|
||||
|
||||
// Scan every entry and keep the HIGHEST version, not simply the first one
|
||||
// that beats the installed build: the API's ordering is by creation date, and
|
||||
// a re-published or back-dated release would otherwise win over a newer one.
|
||||
//
|
||||
// Stable releases are skipped on purpose. This channel tracks pre-releases
|
||||
// only — and a stable APK could not update a unstable install anyway, since
|
||||
// the unstable applicationId carries a .unstable suffix.
|
||||
//
|
||||
// The version comes from the asset name, not the tag: the unstable channel is
|
||||
// a single rolling pre-release tagged "unstable", overwritten on every build.
|
||||
Update best = null;
|
||||
for (int i = 0; i < releases.length(); i++) {
|
||||
JSONObject release = releases.getJSONObject(i);
|
||||
if (!release.optBoolean("prerelease", false)) continue;
|
||||
|
||||
JSONArray assets = release.optJSONArray("assets");
|
||||
if (assets == null) continue;
|
||||
for (int a = 0; a < assets.length(); a++) {
|
||||
JSONObject asset = assets.getJSONObject(a);
|
||||
String name = asset.optString("name", "");
|
||||
if (!name.toLowerCase(java.util.Locale.US).endsWith(".apk")) continue;
|
||||
if (!name.contains("unstable")) continue;
|
||||
|
||||
String version = versionFromAssetName(name);
|
||||
if (version == null) continue;
|
||||
if (!isNewer(version, currentVersion)) continue;
|
||||
if (best != null && !isNewer(version, best.versionName)) continue;
|
||||
|
||||
String url = asset.optString("browser_download_url", "");
|
||||
if (!isAllowedUrl(url)) {
|
||||
Log.w(TAG, "Rejected update URL from an unexpected host: " + url);
|
||||
continue;
|
||||
}
|
||||
best = new Update(version, url);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "Update check failed: " + e.getMessage());
|
||||
return null;
|
||||
return selectUpdate(new JSONArray(body.toString()), currentVersion);
|
||||
} catch (JSONException e) {
|
||||
throw new IOException("Invalid release response", e);
|
||||
} finally {
|
||||
if (conn != null) conn.disconnect();
|
||||
conn.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
/** Scan every asset of every pre-release; asset ordering does not imply version order. */
|
||||
static Update selectUpdate(JSONArray releases, String currentVersion) throws JSONException {
|
||||
Update best = null;
|
||||
for (int i = 0; i < releases.length(); i++) {
|
||||
JSONObject release = releases.getJSONObject(i);
|
||||
if (!release.optBoolean("prerelease", false) || release.optBoolean("draft", false)) continue;
|
||||
JSONArray assets = release.optJSONArray("assets");
|
||||
if (assets == null) continue;
|
||||
for (int a = 0; a < assets.length(); a++) {
|
||||
JSONObject asset = assets.getJSONObject(a);
|
||||
String name = asset.optString("name", "");
|
||||
if (!name.toLowerCase(Locale.US).contains("unstable")) continue;
|
||||
String version = versionFromAssetName(name);
|
||||
if (version == null || !isNewer(version, currentVersion)) continue;
|
||||
if (best != null && !isNewer(version, best.versionName)) continue;
|
||||
String url = asset.optString("browser_download_url", "");
|
||||
if (isAllowedUrl(url)) best = new Update(version, url);
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
/**
|
||||
* Safe diagnostic name for the downloaded APK. The version comes from a remote
|
||||
* asset name, so it is reduced to a safe character set before it reaches a path. Callers that
|
||||
@@ -378,6 +364,7 @@ final class OtaUpdater {
|
||||
}
|
||||
int sessionId = installer.createSession(params);
|
||||
PackageInstaller.Session session = null;
|
||||
boolean committed = false;
|
||||
try {
|
||||
session = installer.openSession(sessionId);
|
||||
try (java.io.InputStream in = new java.io.FileInputStream(apk);
|
||||
@@ -395,12 +382,14 @@ final class OtaUpdater {
|
||||
PendingIntent pending = PendingIntent.getBroadcast(context, sessionId, callback,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
|
||||
session.commit(pending.getIntentSender());
|
||||
committed = true;
|
||||
Log.i(TAG, "OTA session " + sessionId + " committed (" + apk.length() + " bytes)");
|
||||
session = null;
|
||||
} finally {
|
||||
// Only reached when commit did not happen; an abandoned session frees its staging.
|
||||
// Abandon failed staging, but always close our local handle after a commit too.
|
||||
if (!committed) {
|
||||
try { installer.abandonSession(sessionId); } catch (Throwable ignored) { }
|
||||
}
|
||||
if (session != null) {
|
||||
try { session.abandon(); } catch (Throwable ignored) { }
|
||||
try { session.close(); } catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* Drives {@link OtaUpdater} — UNSTABLE BUILDS ONLY.
|
||||
@@ -25,6 +27,7 @@ import java.io.File;
|
||||
final class UpdateHook {
|
||||
|
||||
private static final String TAG = "EVABRP.Update";
|
||||
private static final AtomicBoolean checking = new AtomicBoolean();
|
||||
|
||||
private UpdateHook() { }
|
||||
|
||||
@@ -44,12 +47,16 @@ final class UpdateHook {
|
||||
* Checks, downloads and installs in one background pass.
|
||||
*
|
||||
* The whole sequence runs off the main thread because every step blocks: the release
|
||||
* API call, the download, and {@code pm install}. [listener] is called back on the main
|
||||
* API call, the download, and installer staging. [listener] is called back on the main
|
||||
* thread, so a UI caller can write straight to a view.
|
||||
*/
|
||||
static void checkInBackground(Context context, Listener listener) {
|
||||
Context app = context.getApplicationContext();
|
||||
android.os.Handler main = new android.os.Handler(android.os.Looper.getMainLooper());
|
||||
if (!checking.compareAndSet(false, true)) {
|
||||
if (listener != null) main.post(() -> listener.onResult("Güncelleme kontrolü zaten sürüyor"));
|
||||
return;
|
||||
}
|
||||
new Thread(() -> {
|
||||
String message;
|
||||
try {
|
||||
@@ -57,6 +64,8 @@ final class UpdateHook {
|
||||
} catch (Throwable t) {
|
||||
Log.w(TAG, "Update check threw", t);
|
||||
message = "Güncelleme kontrolü başarısız";
|
||||
} finally {
|
||||
checking.set(false);
|
||||
}
|
||||
if (listener != null) {
|
||||
final String result = message;
|
||||
@@ -66,7 +75,7 @@ final class UpdateHook {
|
||||
}
|
||||
|
||||
/** The blocking part. Returns a line describing what happened, for the UI. */
|
||||
private static String runCheck(Context app) {
|
||||
private static String runCheck(Context app) throws IOException {
|
||||
String current;
|
||||
try {
|
||||
current = app.getPackageManager()
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 149 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 154 KiB |
Reference in New Issue
Block a user