Skip to content

Settings: Enable charge recording once and choose how long to keep it - #27

Merged
d4rken merged 1 commit into
mainfrom
feat/charging-history-settings
Jul 26, 2026
Merged

Settings: Enable charge recording once and choose how long to keep it#27
d4rken merged 1 commit into
mainfrom
feat/charging-history-settings

Conversation

@d4rken

@d4rken d4rken commented Jul 26, 2026

Copy link
Copy Markdown
Member

What changed

Recording charge statistics used to be a switch that sat permanently at the top of the Battery & charging screen. Since it is something you turn on once and then never touch again, it took up the most valuable space on that screen forever.

It is now a one-time opt-in card: the same title and explanation, but with a Start recording button and a line telling you where to turn it back off. Once recording is on, the card disappears entirely and your current or last charge moves to the top of the screen, where it is actually useful.

The permanent on/off control now lives in a new Settings → Charging history screen, which also adds something that wasn't controllable before: how long charge history is kept, as a slider from 3 to 14 days. Turning recording off from there brings the opt-in card back, exactly as on a fresh install.

Please note: history retention is a real behavior change, not just a new setting. Recorded charges were previously kept forever. With this change, charges older than the configured window (14 days by default) are deleted automatically, so existing recordings older than that will be removed. Turning recording off still does not delete anything — Clear data on the history screen remains the only way to wipe it.

Technical Context

Retention was not merely unbounded — its purge was unreachable. The existing purge call sat inside reconcileDanglingSessions() behind an open.maxOfOrNull { … }?.let { … }, so it only ever ran when a dangling open session existed. In the normal state — every row closed after a clean disable — nothing was purged at all. The purge now runs in startupRepair() after reconciliation, on session seal, and immediately on a retention change through an ordered Command.Purge. This was found in review, not by the tests, and is the highest-value part of the diff to check.

deleteSamplesOlderThan is deliberately kept. The first draft removed it as redundant, reasoning that a surviving session's samples are inside the window by construction. That proof is invalid: sample.wallMillis <= session.endedAtWallMillis does not put the sample inside the window, so a 20-day charge that ended yesterday would have retained its entire curve. Whole-entry purging by endedAtWallMillis and the existing per-sample bound now run together with the same cutoff. endedAtWallMillis rather than startedAtWallMillis is what keeps a long charge that ended recently from being dropped, and IS NOT NULL keeps an open session untouchable.

The recorder's "might we have data" guard changed from a timestamp to a file check. lastCaptureWallMillis was written after the first row was committed, so a crash in that gap hid existing data from the guard permanently. It is now context.getDatabasePath(StatsDatabase.NAME).exists(). That retires the preference entirely, which also removes a DataStore write every ~20 s while charging — the same class of shared-store fan-out that #26 was about.

The slider is not a plain Slider. SettingsSliderItem holds drag state locally and commits only on release, and only when the day value actually changed. Without that, dragging would write the shared DataStore and enqueue a Room purge on every frame; the same-value guard also stops a plain tap-and-release from committing. The label formats the live dragged value, so the number tracks the thumb rather than the persisted value.

Why the retention flow is collected in MainActivity's composition root. StatsViewModel is a lazy by viewModels() delegate, so collecting at the settings destination would construct it — and start its eager sharing — in the very frame the screen appears, rendering the placeholder default before the stored value landed. This mirrors how themeState is collected. Review caught this after a first attempt that only changed the sharing strategy, which did nothing.

Two consequences are accepted and documented in the code rather than engineered around: retention is wall-clock based, so a large forward clock jump expires history early; and with recording off, retention only runs at process start and on a slider change.

Known test gap. The property "many drag frames produce exactly one write" is not covered by a test. Material3's IncreaseHorizontalSemanticsBounds measures the slider 20 dp wider than its interactive region and offsets it 10 dp left, so touch coordinates derived from the reported semantics node land outside the draggable area under Robolectric. Rather than ship a green test that pins nothing, the gap is documented in SettingsSliderItemTest; the semantics path, the no-op guard, and external re-sync are covered. The commit-on-release guard is held by review.

Verification. 722 unit tests, assemble, the screenshot-test source set, and lintVital all pass on both flavors. The UI flow was smoke-tested twice on a throwaway API 36 emulator — opt-in card shape, enabling, card disappearing, the settings entry, the slider reaching 3 days and persisting, disabling restoring the card, and a navigation sweep — with no crashes. It has not been run on a physical device, and retention purging itself was not exercised on-device (it needs multi-day time travel); that path is covered by the DAO, StatsRetention, and recorder tests.

…tory screen

The battery hub's "Record statistics" card was a toggle card that stayed on
screen forever. Since the switch is flipped once and then never again, it
occupied the top of the hub permanently for no further purpose.

It becomes a one-time opt-in card: same title, subtitle and tone, but with a
"Start recording" button and a sentence pointing at the new settings screen for
turning it back off. Once recording is on the card is gone entirely and the
charge section becomes the hub's first content. The durable on/off control now
lives in Settings > Charging history, which also carries a retention slider.

Retention is new behavior, not just a new knob. Previously charge_sessions rows
were kept forever and only raw samples were purged, on a hardcoded 30-day
window. Now whole entries are purged by endedAtWallMillis past a user-set
window (3-14 days, default 14) with their samples cascading, while
deleteSamplesOlderThan is retained: a sample's wall time being <= its session's
end stamp does not put it inside the window, so a long charge that ended
recently would otherwise keep its entire curve.

The purge also had to be made reachable. It previously sat inside
reconcileDanglingSessions() behind a maxOfOrNull{}?.let{}, so with no dangling
open rows - the normal state after a clean disable - it never ran at all. It now
runs in startupRepair() after reconciliation, on session seal, and immediately
on a retention change via an ordered Command.Purge.

The recorder's "might we have data" guard moves from the lastCaptureWallMillis
timestamp to a stats.db file check. The stamp was written after the first row
was committed, so a crash in that gap hid existing data from the guard forever.
That also retires the preference outright, removing a DataStore write every ~20s
while charging.

SettingsSliderItem is new: local drag state committed only on release and only
when the value actually changed, so dragging cannot write the shared DataStore
per frame or enqueue a purge per frame. Its label formats the live dragged value
so the number tracks the thumb.

The retention flow is collected at MainActivity's composition root rather than
at the settings destination. StatsViewModel is a lazy by viewModels() delegate,
so collecting at the destination would start its eager sharing in the very frame
the screen appears and render the placeholder default before the stored value
landed.
@d4rken d4rken added the enhancement New feature or request label Jul 26, 2026
@d4rken
d4rken merged commit 457e94b into main Jul 26, 2026
12 checks passed
@d4rken
d4rken deleted the feat/charging-history-settings branch July 26, 2026 16:22
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