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
This commit is contained in:
darken
2026-08-25 22:33:42 +02:00
committed by Matthias Urhahn
parent 03c09917da
commit 2356726417
3 changed files with 23 additions and 5 deletions
@@ -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
@@ -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 ->
@@ -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
}
}