Skip to content

Commit 5424533

Browse files
committed
Dashboard: Consolidate battery and charging telemetry
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.
1 parent d9dfa1d commit 5424533

29 files changed

Lines changed: 2389 additions & 1256 deletions

app/src/debug/java/eu/darken/amply/screenshots/ScreenshotContent.kt

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,13 @@ import eu.darken.amply.charging.core.access.BackendStatus
1717
import eu.darken.amply.common.ca.toCaString
1818
import eu.darken.amply.common.compose.PreviewWrapper
1919
import eu.darken.amply.common.theming.ThemeState
20+
import eu.darken.amply.battery.core.BatteryReadout
2021
import eu.darken.amply.fullcharge.core.ChargeSessionRecord
2122
import eu.darken.amply.main.ui.dashboard.DashboardScreen
2223
import eu.darken.amply.main.ui.dashboard.DashboardUiState
24+
import eu.darken.amply.main.ui.dashboard.StatsDashboardState
25+
import eu.darken.amply.stats.core.ChargeCurvePoint
26+
import eu.darken.amply.stats.core.StatsLiveSession
2327
import eu.darken.amply.main.ui.settings.GeneralSettingsScreen
2428
import eu.darken.amply.main.ui.settings.ReconnectGestureSettingsScreen
2529

@@ -85,9 +89,7 @@ private fun DashboardShot(state: DashboardUiState) = PreviewWrapper {
8589
onAlarmEnabledChange = {},
8690
onAlarmTargetChange = {},
8791
onFixNotifications = {},
88-
onOpenBatteryDetail = {},
89-
onOpenStats = {},
90-
onOpenLiveSession = {},
92+
onOpenBatteryHub = {},
9193
onRetryCapture = {},
9294
onPinWidget = {},
9395
onAddTile = {},
@@ -129,11 +131,56 @@ private fun grantedAccess() = AccessSnapshot(
129131
),
130132
)
131133

134+
// The charging card renders a live reading in every state, so a state without a batteryReadout shows
135+
// "Not reported" three times over, and one without stats shows the capture promo. Both would be a
136+
// misleading thing to ship as a store screenshot — these fixtures give the showcase states real data.
137+
private fun holdingAtLimit() = BatteryReadout(
138+
levelPercent = 80,
139+
// Held at the limit: connected but not taking charge, which is the whole point of the app.
140+
status = android.os.BatteryManager.BATTERY_STATUS_NOT_CHARGING,
141+
plugged = android.os.BatteryManager.BATTERY_PLUGGED_AC,
142+
health = android.os.BatteryManager.BATTERY_HEALTH_GOOD,
143+
technology = "Li-ion",
144+
temperatureTenthsC = 298,
145+
voltageMillivolts = 4_120,
146+
currentNowMicroamps = 0,
147+
chargeCounterMicroampHours = 3_800_000,
148+
cycleCount = 142,
149+
)
150+
151+
private fun charging() = holdingAtLimit().copy(
152+
levelPercent = 72,
153+
status = android.os.BatteryManager.BATTERY_STATUS_CHARGING,
154+
temperatureTenthsC = 312,
155+
currentNowMicroamps = 2_050_000,
156+
)
157+
158+
private fun liveStats() = StatsDashboardState(
159+
enabled = true,
160+
live = StatsLiveSession(
161+
id = 1,
162+
startedAtWallMillis = 0L,
163+
startedElapsedRealtimeMillis = 0L,
164+
startPercent = 41,
165+
partial = false,
166+
curve = (0..14).map { i ->
167+
ChargeCurvePoint(
168+
elapsedFromStartMillis = i * 300_000L,
169+
percent = (41 + i * 2).coerceAtMost(80),
170+
powerMilliwatts = (19_000 - i * 900).coerceAtLeast(2_500),
171+
temperatureTenthsC = 300 + i,
172+
)
173+
},
174+
),
175+
)
176+
132177
// Pixel, set up and verified at 80%: the headline showcase.
133178
private fun readyState() = DashboardUiState(
134179
onboardingComplete = true,
135180
quickFullChargeEnabled = true,
136181
quickAccessChecked = true,
182+
batteryReadout = holdingAtLimit(),
183+
stats = liveStats(),
137184
charging = ChargingState(
138185
device = pixelDevice(),
139186
adapterName = "Pixel Charge Control".toCaString(),
@@ -149,6 +196,9 @@ private fun readyState() = DashboardUiState(
149196
// A one-time full charge in progress: session-aware hero plus the restore card.
150197
private fun sessionState() = DashboardUiState(
151198
onboardingComplete = true,
199+
// Charging past the limit for once — this shot is about the one-time full charge.
200+
batteryReadout = charging(),
201+
stats = liveStats(),
152202
session = ChargeSessionRecord(
153203
restorePolicy = ChargePolicy.FixedLimit(80),
154204
startedAtMillis = 0L,
@@ -169,6 +219,8 @@ private fun sessionState() = DashboardUiState(
169219
// Samsung One UI 8 multi-mode: four fixed limits plus pause-at-full, shown as chips.
170220
private fun samsungState() = DashboardUiState(
171221
onboardingComplete = true,
222+
batteryReadout = holdingAtLimit(),
223+
stats = liveStats(),
172224
charging = ChargingState(
173225
device = DeviceInfo("samsung", "SM-X210", 36, "preview", oneUiVersion = 80000, hasProtectBattery = true),
174226
adapterName = "Samsung battery protection".toCaString(),
@@ -201,7 +253,10 @@ private fun samsungState() = DashboardUiState(
201253

202254
// Pixel before access is granted: the dashboard leads with the setup guide.
203255
private fun setupNeededState() = DashboardUiState(
256+
// Capture is off here: this shot is about the setup guide, and the charging card showing its
257+
// "turn on charge recording" hint is the honest pre-setup state.
204258
onboardingComplete = true,
259+
batteryReadout = charging(),
205260
charging = ChargingState(
206261
device = pixelDevice(),
207262
adapterName = "Pixel Charge Control".toCaString(),

app/src/main/java/eu/darken/amply/battery/ui/BatteryDetailScreen.kt

Lines changed: 0 additions & 211 deletions
This file was deleted.

app/src/main/java/eu/darken/amply/common/compose/AmplyCard.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ fun AmplyCardHeader(
316316
* *secondary* destination or side effect (never a duplicate of [onClick]); anything richer — switches,
317317
* multiple buttons, a whole control row — belongs in [AmplyToggleCard] or a plain [AmplyCard]. A
318318
* nested action must not bubble to the surface: assert both routes separately in tests, as
319-
* `StatsDashboardCard`'s "History" and "Retry" actions do.
319+
* `ChargingCard`'s "Retry" action does.
320320
*/
321321
@Composable
322322
fun AmplyNavigationCard(

0 commit comments

Comments
 (0)