ui(battery): Surface health before data exists, reorder the Battery card

- Show a "Still determining — check back after a few more listening sessions"
  placeholder under Battery Health in the device info sheet while the estimate
  is still accumulating data, so the feature is discoverable from day one; no
  placeholder for profiles without a paired device, where it could never resolve
- Reorder the Battery card: time remaining & health settings first, then a
  divider, then the charging-side settings (charge limit, charged notification)
- Mention the battery-health component in the estimate toggle and reset texts
- Ground the charge-band boundaries in Apple's documented fast-to-80%/trickle
  charging behavior and standard lithium CC/CV charging references
This commit is contained in:
darken
2026-07-02 17:32:29 +02:00
committed by Matthias Urhahn
parent afba33dd83
commit aac0590017
8 changed files with 137 additions and 32 deletions
@@ -575,6 +575,39 @@ class DeviceSettingsViewModelTest : BaseTest() {
vm.state.first().batteryHealth shouldBe BatteryHealth.PerPod(left = 50)
}
@Test
fun `battery health pending flag is set for rated models without data`() = runVmTest {
val device = mockk<PodDevice>(relaxed = true).also {
every { it.profileId } returns testAddress
every { it.model } returns PodModel.AIRPODS_PRO2
every { it.hasSelectedPairedDevice } returns true
}
devicesFlow.value = listOf(device)
val vm = createViewModel()
vm.initialize(testAddress)
val state = vm.state.first()
state.batteryHealth shouldBe null
state.batteryHealthPending shouldBe true
}
@Test
fun `battery health pending flag stays off without a paired device`() = runVmTest {
// No paired address -> the listening gate can never open -> "check back later" would lie.
val device = mockk<PodDevice>(relaxed = true).also {
every { it.profileId } returns testAddress
every { it.model } returns PodModel.AIRPODS_PRO2
every { it.hasSelectedPairedDevice } returns false
}
devicesFlow.value = listOf(device)
val vm = createViewModel()
vm.initialize(testAddress)
vm.state.first().batteryHealthPending shouldBe false
}
@Test
fun `battery health hides when the estimate is disabled for the device`() = runVmTest {
val device = mockk<PodDevice>(relaxed = true).also {
@@ -607,7 +640,9 @@ class DeviceSettingsViewModelTest : BaseTest() {
val vm = createViewModel()
vm.initialize(testAddress)
vm.state.first().batteryHealth shouldBe null
val state = vm.state.first()
state.batteryHealth shouldBe null
state.batteryHealthPending shouldBe false
}
@Test
@@ -107,6 +107,33 @@ class DeviceInfoDetailItemsTest : BaseTest() {
)
}
@Test
fun `pending placeholder shows while no health value exists`() {
// The feature stays discoverable before enough listening data has accumulated.
val result = buildDeviceInfoDetailItems(
null,
labels,
batteryHealth = BatteryHealthTexts(pending = "Still determining"),
formatDate = formatter,
)
result shouldContainExactly listOf(
DeviceDetailItem.Single("Battery Health", "Still determining"),
)
}
@Test
fun `pending placeholder is ignored once a health value exists`() {
val result = buildDeviceInfoDetailItems(
null,
labels,
batteryHealth = BatteryHealthTexts(left = "~85%", pending = "Still determining"),
formatDate = formatter,
)
result shouldContainExactly listOf(
DeviceDetailItem.Single("Left Battery Health", "~85%"),
)
}
@Test
fun `headset battery health yields a Single row with the generic label`() {
val result = buildDeviceInfoDetailItems(