Skip to content

Dashboard: Show battery and charging details in one place - #24

Merged
d4rken merged 3 commits into
mainfrom
worktree-battery-charging-consolidation
Jul 25, 2026
Merged

Dashboard: Show battery and charging details in one place#24
d4rken merged 3 commits into
mainfrom
worktree-battery-charging-consolidation

Conversation

@d4rken

@d4rken d4rken commented Jul 25, 2026

Copy link
Copy Markdown
Member

What changed

Battery and charging information now lives in one place instead of being spread across the dashboard and two separate screens.

  • The card at the top states your charge policy and what the battery is actually doing about it — "Connected · not charging · 80%" while a limit is holding. It no longer opens battery details when tapped, because tapping "80% limit active" and landing on voltage and cycle counts was never what it promised.
  • One battery card below it is now the single place readings appear. It always shows level, status, temperature, and charging speed while charging — whether or not charge recording is switched on — and it stays put instead of jumping to a different position when you plug in.
  • Tapping it opens a new "Battery & charging" screen holding the recording switch, the current or last charge, and the full readout. Past charges moved to a "Charge history" action in its toolbar, so the card always leads to the same place.

Two accuracy problems surfaced while doing this and are fixed here:

  • Charging speed is no longer shown or recorded while the battery isn't actually taking charge. A phone drawing more power than its charger supplies could previously record that drain as though it were charging speed, which then fed the charge curve and the session's average and peak.
  • If the battery can't be read for several seconds running, the app now says so instead of continuing to present the last value as the current one.

Technical Context

Why the reading line is derived from the battery broadcast alone. The status card's title is frequently only a last-requested claim — Android blocks third-party reads of the hidden Pixel values — so deriving the line beneath it from the configured policy would restate an unverified claim as though it were an observation. Wording is deliberately conservative: NOT_CHARGING also covers thermal throttling, a weak supply, or a fault, so it is never attributed to a protection policy; and an unreported plug state claims neither "connected" nor "on battery".

The charge teaser is derived from the card's presentation, not live ?: lastSession. A recorder row survives an unplug until the recorder seals it, and a row left open by a failed service start is frozen at its last values. Routing both surfaces through one decision keeps those rules from having to be fixed twice.

Power direction. StatsPowerCalculator.milliwatts returns an unsigned magnitude and its contract puts direction on the caller. Direction now comes from batteryStatus rather than the sign of currentNow, which is OEM-defined. One gate serves both the live reading and the recorder. Curve points are additionally filtered on read, so rows written before the gate stop plotting drain as charge power; their stored averages are deliberately left alone, since they cannot be recomputed once raw samples age out of retention and every new session is clean.

Room laziness. ChargeStatsRepository resolves the database eagerly, so binding the session list at ViewModel construction would create stats.db for someone who never enabled recording and merely opened the new screen. The history flow takes a provider invoked inside the flow instead, with a test asserting it is untouched when only the capture switch is collected.

Package placement. Both new surfaces live under main/ui, where feature composition already happens (the alarm and quick-access cards are there too), so battery/ and stats/ stay one-way rather than importing each other.

Verification

CI aside, this was exercised on hardware:

  • Pixel 8 / API 37 — limit hold, charging, full, and unplugged states; no power figure at a hold or while discharging; the card holding a fixed slot across plug/unplug; and the whole navigation chain including back-origins from both entry points into a session.
  • Pixel 2 / API 28, R8-minified — startup and navigation under minification (CI does not run R8). Also covers the unsupported-device layout and the recording-off state.

The one item not verified on-device is that a fresh install creates no stats.db; that needs an uninstall and is covered by a unit test instead.

d4rken added 3 commits July 25, 2026 19:50
StatsPowerCalculator.milliwatts returns an unsigned magnitude, and its
contract puts direction on the caller. The recorder ignored that and
stored the figure on every sample, so a device drawing more than its
charger supplies persisted its discharge draw as if it were a charge
rate — into the curve, the peak, and the time-weighted average.

chargeMilliwatts() is now the single gate: a value only when plugged and
BATTERY_STATUS_CHARGING. Direction comes from batteryStatus because the
sign of currentNow is OEM-defined and cannot be trusted for it. Nothing
is lost — voltage, current and status stay on the sample, so only the
derived field is withheld where it would mean something it doesn't.

The repository applies the same rule when mapping to curve points, so
rows written before the gate stop plotting draw as charge power too.
Their stored averages are deliberately left alone: they cannot be
recomputed once raw samples age out of retention, and every new session
is clean.

The engine needed no change — it credits an interval before replacing
the last reading, so a null-power sample leaves its interval uncredited
rather than skewing the mean.
A throwing read re-emitted the last known readout forever, so a reader
that stayed broken froze the UI on whatever it last saw — still claiming
"Charging · 82%" long after the cable came out. That was tolerable while
the reading was incidental; it is not now that a surface labels it "Now".

The repeat is capped at two consecutive failures (~6s at the default
interval), after which the flow emits BatteryReadout.UNKNOWN and every
field honestly reads "Not reported" until a read succeeds. A transient
blip stays invisible, a sustained failure stops being asserted, and a
recovery emits fresh data rather than a stale copy.

The loop moved to an internal batteryReadouts(interval, read) so the
failure/recovery behaviour is JVM-testable against a scripted reader
instead of a real device. CancellationException still propagates rather
than being absorbed as a failed read.
Level and temperature were rendered on three surfaces through two
formatting paths, and the cards disagreed about what they were for. The
hero showed the policy but navigated to voltage and cycle counts. The
stats card opened two different destinations depending on its state and
carried a History button inside a body that was itself tappable. Its slot
moved with the charger, so the list reshuffled under the user. The
capture switch sat at the bottom of the session list.

Two cards with non-overlapping jobs, and one destination:

- The hero states the policy and nothing else. It is no longer clickable.
  Its one reading line comes from BatteryEffect, derived from the battery
  broadcast alone — never from the policy, because that title is often
  only a last-requested claim and restating it would look like
  corroboration. Wording stays neutral: NOT_CHARGING can be thermal or a
  weak supply, and an unreported plug state claims nothing at all.
- The charging card is the single telemetry surface. It renders a live
  reading in every state including capture-off, holds a fixed slot, and
  always opens the hub. Its title follows the readout, saying "Charging"
  only when the platform reports it.
- Battery details and the statistics screen merge into a "Battery &
  charging" hub: capture switch, current-or-last charge, full readout,
  and history in the top bar.

The hub's teaser is derived from the card's presentation rather than
`live ?: lastSession`, so a row left open by an unplug or a failed
service start cannot be shown as a charge in progress.

Both surfaces move to main/ui, where feature composition already lives,
keeping battery/ and stats/ one-way rather than mutually dependent.

The history flow now takes a provider invoked inside the flow.
ChargeStatsRepository resolves the database eagerly, so building it at
construction would have created stats.db for a user who never enabled
capture and only opened the hub.
@d4rken d4rken added the enhancement New feature or request label Jul 25, 2026
@d4rken
d4rken merged commit 5424533 into main Jul 25, 2026
12 checks passed
@d4rken
d4rken deleted the worktree-battery-charging-consolidation branch July 25, 2026 17:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant