Dashboard: Show battery and charging details in one place - #24
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed
Battery and charging information now lives in one place instead of being spread across the dashboard and two separate screens.
Two accuracy problems surfaced while doing this and are fixed here:
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_CHARGINGalso 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.milliwattsreturns an unsigned magnitude and its contract puts direction on the caller. Direction now comes frombatteryStatusrather than the sign ofcurrentNow, 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.
ChargeStatsRepositoryresolves the database eagerly, so binding the session list at ViewModel construction would createstats.dbfor 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), sobattery/andstats/stay one-way rather than importing each other.Verification
CI aside, this was exercised on hardware:
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.