From 235672641729da3385ed83447377b0fb48563a45 Mon Sep 17 00:00:00 2001 From: darken Date: Tue, 25 Aug 2026 21:11:22 +0200 Subject: [PATCH] fix(overview): Call an empty case not enough on older models AirPods Gen 1 and Gen 2 publish their case capacity as a lower bound, and the adequacy check let that guard run before any zero-reading check. At a 0% case those two models rendered the definite "no charges left" text in the neutral uncertainty colour while the screen reader hedged with "may not be enough", contradicting the same node three ways. The empty reading is now handled before the lower-bound guard. It sits after the "enough" branch, which a zero reading can never satisfy, so it cannot mask a positive claim. The lower-bound rule exists to avoid underselling a case whose published capacity is only a floor, and an empty case has nothing to undersell. Fixes review finding F4 --- .../capod/monitor/core/battery/CaseCharges.kt | 4 +++- .../main/ui/overview/cards/CaseChargesLineTest.kt | 10 ++++++++++ .../capod/monitor/core/battery/CaseChargesTest.kt | 14 ++++++++++---- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/eu/darken/capod/monitor/core/battery/CaseCharges.kt b/app/src/main/java/eu/darken/capod/monitor/core/battery/CaseCharges.kt index b272dcfe..c14e2098 100644 --- a/app/src/main/java/eu/darken/capod/monitor/core/battery/CaseCharges.kt +++ b/app/src/main/java/eu/darken/capod/monitor/core/battery/CaseCharges.kt @@ -33,7 +33,8 @@ data class CaseCharges( * * The interval excludes its upper end, so an upper end of exactly one charge is still short of one. * - * A spec that is itself a lower bound has no upper end, so it can never claim "not enough". + * A spec that is itself a lower bound has no upper end, so it can never claim "not enough" — except + * at an empty reading, where a floor on the published capacity has nothing left to undersell. * * The wording follows the whole interval, not its pessimistic end: an uncertain one rounds to a * single charge, or names no number at all ([CaseCharges.Display.UNCERTAIN]) when the spec is a @@ -52,6 +53,7 @@ fun caseCharges(spec: PodModel.CaseSpec?, reading: BatteryReading?): CaseCharges val adequacy = when { lowest >= 1f -> CaseCharges.Adequacy.ENOUGH + reading.percent == 0f -> CaseCharges.Adequacy.NOT_ENOUGH spec.isLowerBound -> CaseCharges.Adequacy.UNCERTAIN highest <= 1f -> CaseCharges.Adequacy.NOT_ENOUGH else -> CaseCharges.Adequacy.UNCERTAIN diff --git a/app/src/test/java/eu/darken/capod/main/ui/overview/cards/CaseChargesLineTest.kt b/app/src/test/java/eu/darken/capod/main/ui/overview/cards/CaseChargesLineTest.kt index ce8dd8c7..941ec1f1 100644 --- a/app/src/test/java/eu/darken/capod/main/ui/overview/cards/CaseChargesLineTest.kt +++ b/app/src/test/java/eu/darken/capod/main/ui/overview/cards/CaseChargesLineTest.kt @@ -128,6 +128,16 @@ class CaseChargesLineTest : BaseComposeRobolectricTest() { .assertCountEquals(0) } + @Test + fun `an empty case on an open ended spec still reads as not enough`() { + setCard(device(case = 0f, model = PodModel.AIRPODS_GEN1)) + + assertAdequacy( + context.getString(R.string.battery_case_charges_empty), + R.string.battery_case_charges_state_not_enough_cd, + ) + } + @Test fun `the line survives a right-to-left layout`() { setCard(device(case = 0.60f)) { card -> diff --git a/app/src/test/java/eu/darken/capod/monitor/core/battery/CaseChargesTest.kt b/app/src/test/java/eu/darken/capod/monitor/core/battery/CaseChargesTest.kt index 4b305f26..1422429d 100644 --- a/app/src/test/java/eu/darken/capod/monitor/core/battery/CaseChargesTest.kt +++ b/app/src/test/java/eu/darken/capod/monitor/core/battery/CaseChargesTest.kt @@ -82,7 +82,7 @@ class CaseChargesTest : BaseTest() { } @Test - fun `a lower bound spec never claims not enough`() { + fun `a lower bound spec above empty never claims not enough`() { // 3.8 x 0.25 = 0.95 — a confident "not enough" for a case that may well hold more charges(lowerBound, 0.25f, percent)!!.adequacy shouldBe CaseCharges.Adequacy.UNCERTAIN charges(lowerBound, 0.01f, percent)!!.adequacy shouldBe CaseCharges.Adequacy.UNCERTAIN @@ -138,8 +138,14 @@ class CaseChargesTest : BaseTest() { } @Test - fun `an empty case renders as empty`() { - charges(exact, 0f, percent)!!.display shouldBe CaseCharges.Display.EMPTY - charges(lowerBound, 0f, decile)!!.display shouldBe CaseCharges.Display.EMPTY + fun `an empty case renders as empty and claims not enough`() { + val exactly = charges(exact, 0f, percent)!! + exactly.display shouldBe CaseCharges.Display.EMPTY + exactly.adequacy shouldBe CaseCharges.Adequacy.NOT_ENOUGH + + // an empty case holds nothing for the open ended spec to undersell + val floored = charges(lowerBound, 0f, decile)!! + floored.display shouldBe CaseCharges.Display.EMPTY + floored.adequacy shouldBe CaseCharges.Adequacy.NOT_ENOUGH } }