Skip to content

Commit f361409

Browse files
malysclaude
andcommitted
feat(dashboard): give the screen back to the numbers, and say which version it is
The top bar spent 80dp of a 720dp panel introducing the app to a driver who had just tapped its name to get here, and announcing "vehicle data available" all day — a line that said what the numbers underneath were already saying. Both are gone. Sub-screens keep their bars, where a title and a way back are the whole point of one. What the space bought: the state of charge, the one Display element on this screen, reads at 56sp instead of 32sp, and the battery card grew with it; every value in the instrument moved up a step of the scale. The suite token text_display is untouched — the 56sp lives in dimens_app.xml with the reason next to it, because this anchor no longer shares its screen with a bar and a status line. When the car stops answering, the line under the state of charge says so in red, in the place where the missing number is, instead of a banner saying it in orange somewhere else. Text and colour together: roughly one driver in twelve cannot separate the hues, and the word is faster to read anyway. Nothing appears, moves or asks for a tap while driving. The rail did not fit and had not for two tickets. Once the system bars take their share of 720dp, four stacked full-width buttons put the charge stop off the bottom of the screen, where it was unreachable — the emulator at 1920x720 shows it plainly. The five actions are now one primary and a 2x2 grid, which fits with room to spare. The paired readout labels wrap in French and not in English, so their values sat at four different heights; two lines everywhere puts them back on one baseline. About lives on a version badge at the top of the rail, where the app's name used to be in the bar: version, author, and the repository as a QR encoded on the device. The address is deliberately not a link — this head unit has no browser to hand it to, and a tap that does nothing is worse than a code the phone in a pocket can read off the glass. Modelled on EVTasker's, which is where the zxing dependency comes from too. Verified on the 1920x720 API 28 emulator in both palettes: nothing clipped, nothing off-screen, #8F0F18 on white at 8.7:1 and #FFACAC on the night surface at 10:1, both past the 7:1 daylight floor. README screenshots regenerated from that run. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent b2b5984 commit f361409

14 files changed

Lines changed: 358 additions & 66 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,24 @@ All notable changes follow [Keep a Changelog](https://keepachangelog.com/en/1.1.
106106

107107
### Changed
108108

109+
- **The dashboard stopped introducing itself and started reading larger.** The top bar spent
110+
80dp of a 720dp screen on the app's own name — which the driver had just tapped to get here —
111+
and on a permanent "vehicle data available", a line that said what the numbers underneath
112+
were already saying. Both are gone. The state of charge, the one Display element on this
113+
screen, is now 56sp instead of 32sp, and every value on it moved up a step of the type scale;
114+
the battery card grew with them. When the car stops answering, the line under the state of
115+
charge says so in red instead of a banner saying it in orange all day: text and colour
116+
together, in the place where the missing number is, and nothing appears, moves or asks for a
117+
tap while driving. Sub-screens keep their title bars, where a title and a way back are the
118+
point of the bar.
119+
120+
- **The app says which version it is, and how to find its source from a car with no keyboard.**
121+
A version badge sits at the top of the trip rail, where the app's name used to be in the bar,
122+
and opens an About with the version, the author and the repository as a QR code — encoded on
123+
the device, no network involved, modelled on EVTasker's. The address is deliberately not a
124+
link: this head unit has no browser to hand it to, and a tap that does nothing is worse than
125+
a code a phone can read off the glass.
126+
109127
- **"Works offline" stopped being true in one direction only, and the documents say so.**
110128
Network and location were approved for route planning, which retires the first line of this
111129
app's safety argument — an app that cannot open a socket cannot exfiltrate a trip history, and

‎app/build.gradle.kts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,5 +94,7 @@ dependencies {
9494
implementation(libs.material)
9595
implementation(libs.gson)
9696
implementation(libs.androidx.security.crypto)
97+
// Encodes the About QR locally; the same version EVTasker ships.
98+
implementation(libs.zxing.core)
9799
testImplementation(libs.junit)
98100
}

‎app/src/main/java/com/evsuite/chargepilot/MainActivity.kt‎

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@ import android.content.Context
55
import android.content.Intent
66
import android.content.ServiceConnection
77
import android.content.pm.PackageManager
8+
import android.graphics.Color
9+
import android.graphics.drawable.ColorDrawable
810
import android.os.Bundle
911
import android.os.IBinder
12+
import android.widget.ImageView
1013
import android.widget.TextView
1114
import android.widget.Toast
1215
import androidx.activity.result.contract.ActivityResultContracts
@@ -29,6 +32,8 @@ import com.evsuite.hardware.telemetry.EnergyTripSummary
2932
import com.evsuite.hardware.telemetry.Provenanced
3033
import com.evsuite.hardware.telemetry.TripDetector
3134
import com.evsuite.hardware.telemetry.UnavailableReason
35+
import com.google.android.material.button.MaterialButton
36+
import com.google.android.material.dialog.MaterialAlertDialogBuilder
3237
import java.io.File
3338
import java.util.Locale
3439
import java.util.concurrent.Executors
@@ -106,6 +111,8 @@ class MainActivity : AppCompatActivity() {
106111
binding.chargeStopAction.setOnClickListener {
107112
startActivity(Intent(this, ChargeStopActivity::class.java))
108113
}
114+
binding.aboutAction.text = getString(R.string.about_version_badge, appVersion())
115+
binding.aboutAction.setOnClickListener { showAbout() }
109116
renderUnavailable()
110117
requestVehiclePermissions()
111118
if (TripRecordingService.isAutomaticDetectionEnabled(this)) {
@@ -188,10 +195,7 @@ class MainActivity : AppCompatActivity() {
188195
renderReadings(readings, value.firmware)
189196

190197
val hasVehicleData = value.hasVehicleData
191-
binding.dataStatus.text = getString(
192-
if (hasVehicleData) R.string.status_vehicle_connected else R.string.status_waiting_for_vehicle
193-
)
194-
binding.dataStatus.setTextColor(getColor(if (hasVehicleData) R.color.ev_ok else R.color.ev_warn))
198+
renderDataStatus(hasVehicleData)
195199

196200
// Recording controls are for a parked driver. Live values remain passive and readable
197201
// while moving; the app never presents an overlay or asks for attention.
@@ -417,11 +421,51 @@ class MainActivity : AppCompatActivity() {
417421
view.contentDescription = provenance.describe(getString(label), value, rendered)
418422
}
419423

424+
/**
425+
* The screen carries no status banner: when the car answers, this line says where the
426+
* number above it comes from, and when it does not, the same line says so in red. Text
427+
* and colour together, because a driver reads the word faster than the hue and roughly
428+
* one in twelve cannot separate the hues at all.
429+
*/
430+
private fun renderDataStatus(hasVehicleData: Boolean) {
431+
binding.dataStatus.setText(
432+
if (hasVehicleData) R.string.soc_vehicle_reported else R.string.status_waiting_for_vehicle
433+
)
434+
binding.dataStatus.setTextColor(
435+
getColor(if (hasVehicleData) R.color.ev_text_secondary else R.color.ev_error)
436+
)
437+
}
438+
439+
/** What the driver can read back down a phone line, build suffix and all. */
440+
private fun appVersion(): String = runCatching {
441+
packageManager.getPackageInfo(packageName, 0).versionName
442+
}.getOrNull() ?: getString(R.string.about_version_unknown)
443+
444+
/**
445+
* Version, authorship and the repository as a QR. The address is not a link: this head
446+
* unit has no browser to hand it to, and a tap that does nothing is worse than a code
447+
* the phone in the driver's pocket can read off the glass.
448+
*/
449+
private fun showAbout() {
450+
val content = layoutInflater.inflate(R.layout.dialog_about, null)
451+
content.findViewById<TextView>(R.id.aboutVersion).text =
452+
getString(R.string.about_version, appVersion())
453+
QrCode.generate(REPOSITORY_URL, QR_SIZE_PX)?.let {
454+
content.findViewById<ImageView>(R.id.aboutQrCode).setImageBitmap(it)
455+
}
456+
val dialog = MaterialAlertDialogBuilder(this).setView(content).create()
457+
content.findViewById<MaterialButton>(R.id.aboutClose).setOnClickListener { dialog.dismiss() }
458+
dialog.show()
459+
// The card draws its own surface, outline and radius; the dialog window must not
460+
// draw a second one behind it.
461+
dialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
462+
}
463+
420464
private fun renderUnavailable() {
421465
latestReadings = DashboardReadings.empty()
422466
consumption.reset()
423467
renderReadings(latestReadings)
424-
binding.dataStatus.text = getString(R.string.status_waiting_for_vehicle)
468+
renderDataStatus(false)
425469
binding.tripAction.isEnabled = false
426470
binding.automaticDetection.isEnabled = false
427471
val automatic = TripRecordingService.isAutomaticDetectionEnabled(this)
@@ -766,6 +810,9 @@ class MainActivity : AppCompatActivity() {
766810
const val MAX_LOG_SECTION_BYTES = 32 * 1_024
767811
const val MAX_INVENTORY_FILES = 64
768812
const val DASH = "—"
813+
const val REPOSITORY_URL = "https://github.com/malys/EVChargePilot"
814+
/** Encoded once at this size, then scaled down into a 208dp view. */
815+
const val QR_SIZE_PX = 416
769816
// The unit-bearing fields keep their unit while unavailable so the layout does not
770817
// shift the moment the vehicle starts answering.
771818
const val SOC_UNAVAILABLE = "— %"
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package com.evsuite.chargepilot
2+
3+
import android.graphics.Bitmap
4+
import android.graphics.Color
5+
import com.google.zxing.BarcodeFormat
6+
import com.google.zxing.EncodeHintType
7+
import com.google.zxing.MultiFormatWriter
8+
9+
/**
10+
* The repository address as a QR, encoded on the device: this app holds no network
11+
* capability of its own beyond routing, and an About screen is not the place to gain one.
12+
* Null when the encoder refuses; the caller leaves the image blank rather than guessing.
13+
*/
14+
object QrCode {
15+
16+
fun generate(content: String, sizePx: Int): Bitmap? = runCatching {
17+
val hints = mapOf<EncodeHintType, Any>(
18+
EncodeHintType.MARGIN to 1,
19+
EncodeHintType.CHARACTER_SET to "UTF-8",
20+
)
21+
val matrix = MultiFormatWriter()
22+
.encode(content, BarcodeFormat.QR_CODE, sizePx, sizePx, hints)
23+
val pixels = IntArray(sizePx * sizePx) { index ->
24+
if (matrix.get(index % sizePx, index / sizePx)) Color.BLACK else Color.WHITE
25+
}
26+
Bitmap.createBitmap(pixels, sizePx, sizePx, Bitmap.Config.RGB_565)
27+
}.getOrNull()
28+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
3+
<solid android:color="@color/ev_icon_tile" />
4+
<corners android:radius="@dimen/ev_corner" />
5+
</shape>

0 commit comments

Comments
 (0)