fix(overview): Stop denying a charge the reading still allows

The visible count came from the pessimistic end of the interval alone, so
an interval straddling a full charge said "Less than one more full charge"
while the colour stayed neutral and TalkBack said "may not be enough". An
uncertain interval now rounds to a single charge instead, and an
open-ended spec that has not reached one charge names no number at all.

Fixes review finding F2
This commit is contained in:
darken
2026-08-25 22:33:42 +02:00
committed by Matthias Urhahn
parent 70830cf2ca
commit 03c09917da
5 changed files with 59 additions and 5 deletions
@@ -513,6 +513,7 @@ private fun CaseChargesLine(
val text = when (charges.display) {
CaseCharges.Display.EMPTY -> stringResource(R.string.battery_case_charges_empty)
CaseCharges.Display.LESS_THAN_ONE -> stringResource(R.string.battery_case_charges_less_than_one)
CaseCharges.Display.UNCERTAIN -> stringResource(R.string.battery_case_charges_uncertain)
CaseCharges.Display.AT_LEAST -> pluralStringResource(
R.plurals.battery_case_charges_at_least,
charges.count,
@@ -14,6 +14,9 @@ data class CaseCharges(
/** Nothing left to give — distinct from [LESS_THAN_ONE], which still implies some charge. */
EMPTY,
LESS_THAN_ONE,
/** No number at all: an open-ended interval that has not yet reached a full charge. */
UNCERTAIN,
AT_LEAST,
APPROXIMATE,
}
@@ -32,6 +35,10 @@ data class CaseCharges(
*
* A spec that is itself a lower bound has no upper end, so it can never claim "not enough".
*
* 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
* lower bound and there is no upper end to round from.
*
* Null when the model publishes no case figures or the reading is missing or nonsense — the caller
* shows no line at all rather than a neutral one.
*/
@@ -50,13 +57,23 @@ fun caseCharges(spec: PodModel.CaseSpec?, reading: BatteryReading?): CaseCharges
else -> CaseCharges.Adequacy.UNCERTAIN
}
val count = floor(lowest).toInt()
val floored = floor(lowest).toInt()
val display = when {
reading.percent == 0f -> CaseCharges.Display.EMPTY
count < 1 -> CaseCharges.Display.LESS_THAN_ONE
adequacy == CaseCharges.Adequacy.NOT_ENOUGH -> CaseCharges.Display.LESS_THAN_ONE
adequacy == CaseCharges.Adequacy.UNCERTAIN && spec.isLowerBound -> CaseCharges.Display.UNCERTAIN
adequacy == CaseCharges.Adequacy.UNCERTAIN -> CaseCharges.Display.APPROXIMATE
spec.isLowerBound -> CaseCharges.Display.AT_LEAST
else -> CaseCharges.Display.APPROXIMATE
}
val count = when {
reading.percent == 0f -> 0
adequacy == CaseCharges.Adequacy.NOT_ENOUGH -> 0
adequacy == CaseCharges.Adequacy.UNCERTAIN && !spec.isLowerBound -> 1
else -> floored
}
return CaseCharges(count = count, display = display, adequacy = adequacy)
}
+2
View File
@@ -287,6 +287,8 @@
</plurals>
<string name="battery_case_charges_less_than_one">Less than one more full charge for both earbuds</string>
<string name="battery_case_charges_empty">Case is empty, no charges left for your earbuds</string>
<!-- Shown when the case reading is too coarse to name any number of charges. Must not mention a count. -->
<string name="battery_case_charges_uncertain">Not sure how many full charges are left for both earbuds</string>
<string name="battery_case_charges_state_enough_cd">Enough for a full charge</string>
<string name="battery_case_charges_state_not_enough_cd">Not enough for a full charge</string>
<string name="battery_case_charges_state_uncertain_cd">May not be enough for a full charge</string>
@@ -92,11 +92,19 @@ class CaseChargesLineTest : BaseComposeRobolectricTest() {
}
@Test
fun `a reading astride a full charge claims nothing`() {
fun `a reading astride a full charge rounds to one`() {
setCard(device(case = 0.20f))
assertAdequacy(charges(1), R.string.battery_case_charges_state_uncertain_cd)
}
@Test
fun `an open ended spec below a full charge names no number`() {
// AirPods (Gen 1) publish a lower bound, so 3.8 x 0.10 has no upper end to round to
setCard(device(case = 0.10f, model = PodModel.AIRPODS_GEN1))
assertAdequacy(
context.getString(R.string.battery_case_charges_less_than_one),
context.getString(R.string.battery_case_charges_uncertain),
R.string.battery_case_charges_state_uncertain_cd,
)
}
@@ -107,10 +107,36 @@ class CaseChargesTest : BaseTest() {
@Test
fun `below a full charge renders as less than one`() {
charges(exact, 0.20f, percent)!!.display shouldBe CaseCharges.Display.LESS_THAN_ONE
charges(lowerBound, 0.20f, percent)!!.display shouldBe CaseCharges.Display.LESS_THAN_ONE
charges(exact, 0.01f, percent)!!.display shouldBe CaseCharges.Display.LESS_THAN_ONE
}
@Test
fun `an interval straddling one charge rounds to one instead of claiming less`() {
// 4.0 x 0.20 = 0.8, 4.0 x 0.30 = 1.2 — the text must not deny what the interval allows
val straddle = charges(exact, 0.20f, decile)!!
straddle.display shouldBe CaseCharges.Display.APPROXIMATE
straddle.count shouldBe 1
}
@Test
fun `an uncertain lower bound spec names no number`() {
// 3.8 x 0.10 = 0.38 with no upper end, so neither "less than one" nor "~1" is supportable
val vague = charges(lowerBound, 0.10f, decile)!!
vague.adequacy shouldBe CaseCharges.Adequacy.UNCERTAIN
vague.display shouldBe CaseCharges.Display.UNCERTAIN
charges(lowerBound, 0.20f, percent)!!.display shouldBe CaseCharges.Display.UNCERTAIN
}
@Test
fun `a lower bound spec that reaches enough still counts`() {
val enough = charges(lowerBound, 0.50f, decile)!!
enough.adequacy shouldBe CaseCharges.Adequacy.ENOUGH
enough.display shouldBe CaseCharges.Display.AT_LEAST
// 3.8 x 0.50 = 1.9
enough.count shouldBe 1
}
@Test
fun `an empty case renders as empty`() {
charges(exact, 0f, percent)!!.display shouldBe CaseCharges.Display.EMPTY