From e5a80003c73fd9b025a8ef940280c5f5209c21a4 Mon Sep 17 00:00:00 2001 From: Al-Mothafar Al-Hasan Date: Fri, 17 Jul 2026 15:34:05 +0300 Subject: [PATCH 1/4] Current row: moment value with windowed average, floor gate on the average (#173) The Current row flickered in and out when the instantaneous reading dipped below the 10 mA display floor, and each surface showed its own instant. Keep the moment value as the headline but display the windowed average next to it ("-208 mA (avg: -245)"), and let the average carry the floor gate when available: the average moves slowly, so the row's visibility is stable, while sustained garbage (whose average also sits below the floor) stays hidden exactly as before. The average reuses source A's smoothing criteria, extracted into a shared averagedCurrentMicroAmps helper so the rate source and the displayed average cannot disagree. The notification's mA fallback stays instant-only (the line is already long) and inherits the stable gate for free. Arabic drafted for the new string, pending maintainer review. Co-Authored-By: Claude Fable 5 --- .../service/BatteryRateTracker.java | 105 ++++++++++++++---- .../ui/fragment/BatteryDetailsFragment.java | 8 +- app/src/main/res/values-ar/strings.xml | 2 + app/src/main/res/values/strings.xml | 3 + .../service/BatteryRateTrackerTest.java | 91 +++++++++++++++ 5 files changed, 184 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/com/almothafar/simplebatterynotifier/service/BatteryRateTracker.java b/app/src/main/java/com/almothafar/simplebatterynotifier/service/BatteryRateTracker.java index 5fb99a9..f886dcd 100644 --- a/app/src/main/java/com/almothafar/simplebatterynotifier/service/BatteryRateTracker.java +++ b/app/src/main/java/com/almothafar/simplebatterynotifier/service/BatteryRateTracker.java @@ -257,6 +257,11 @@ static List trimToWindow(final List window, final long now) { * a plausible ceiling (a garbage reading). The current is reported independently, signed by * direction so it reads negative while discharging and positive while charging regardless of the * device's raw sign convention — and only when it clears {@link #MIN_DISPLAY_CURRENT_MA} (#152). + *

+ * The windowed average current is reported alongside the instant (#173), and when available + * it also takes over the floor gate: the average moves slowly, so the row's visibility is stable — + * a momentary dip below the floor no longer blinks the row out, while sustained-garbage readings + * (whose average is also below the floor) stay hidden. * * @param window samples oldest-first * @param capacityMah measured full capacity in mAh, or 0 when unknown/untrusted (#69) @@ -268,14 +273,52 @@ static List trimToWindow(final List window, final long now) { */ static BatteryRate computeRate(final List window, final int capacityMah, final boolean charging, final long nowMillis, final int latestCurrentMicroAmps) { + // Floor-gate on the windowed average when available (#173): the average moves slowly, so the + // row's visibility can't flicker with a momentary dip, and sustained garbage (Kirin pre-#152-v2 + // calibration averages ~1 mA) stays hidden. The instant gates itself only while the window is + // still too fresh to average. + final int avgMicroAmps = averagedCurrentMicroAmps(window); + final boolean hasAvg = avgMicroAmps != PROPERTY_UNSUPPORTED; + final int floorGateMicroAmps = hasAvg ? avgMicroAmps : latestCurrentMicroAmps; final boolean hasCurrent = isPlausibleCurrentMicroAmps(latestCurrentMicroAmps) - && Math.round(Math.abs(latestCurrentMicroAmps) / 1000f) >= MIN_DISPLAY_CURRENT_MA; + && Math.round(Math.abs(floorGateMicroAmps) / 1000f) >= MIN_DISPLAY_CURRENT_MA; final int signedMilliAmps = hasCurrent ? signedCurrentMilliAmps(latestCurrentMicroAmps, charging) : 0; + final boolean hasAvgCurrent = hasCurrent && hasAvg; + final int signedAvgMilliAmps = hasAvgCurrent ? signedCurrentMilliAmps(avgMicroAmps, charging) : 0; final int pph = ratePercentPerHour(window, capacityMah); final boolean hasRate = pph >= 1 && pph <= MAX_PLAUSIBLE_RATE_PPH; - return new BatteryRate(hasRate, hasRate ? pph : 0, charging, hasCurrent, signedMilliAmps); + return new BatteryRate(hasRate, hasRate ? pph : 0, charging, hasCurrent, signedMilliAmps, hasAvgCurrent, signedAvgMilliAmps); + } + + /** + * The averaged instantaneous current over the window in µA, or {@link Integer#MIN_VALUE} when the + * window doesn't yet hold enough spaced plausible readings — the same smoothing criteria source A + * uses ({@link #MIN_CURRENT_SAMPLES} readings spanning {@link #MIN_SPAN_CURRENT_MS}), extracted so + * the rate source and the displayed average (#173) can't disagree. Pure so it is unit-testable. + * + * @param window samples oldest-first + * + * @return averaged current in µA, or {@link Integer#MIN_VALUE} when not enough data yet + */ + static int averagedCurrentMicroAmps(final List window) { + if (window.size() < 2) { + return PROPERTY_UNSUPPORTED; + } + final long spanMs = window.get(window.size() - 1).timeMillis() - window.get(0).timeMillis(); + double sumMicroAmps = 0; + int currentSamples = 0; + for (final Sample s : window) { + if (isPlausibleCurrentMicroAmps(s.currentMicroAmps())) { + sumMicroAmps += s.currentMicroAmps(); + currentSamples++; + } + } + if (currentSamples < MIN_CURRENT_SAMPLES || spanMs < MIN_SPAN_CURRENT_MS) { + return PROPERTY_UNSUPPORTED; + } + return (int) Math.round(sumMicroAmps / currentSamples); } /** @@ -296,18 +339,10 @@ private static int ratePercentPerHour(final List window, final int capac final Sample last = window.get(window.size() - 1); final long spanMs = last.timeMillis() - first.timeMillis(); - // Source A: averaged current / capacity. - double sumMilliAmps = 0; - int currentSamples = 0; - for (final Sample s : window) { - if (isPlausibleCurrentMicroAmps(s.currentMicroAmps())) { - sumMilliAmps += s.currentMicroAmps() / 1000.0; - currentSamples++; - } - } - if (capacityMah > 0 && currentSamples >= MIN_CURRENT_SAMPLES && spanMs >= MIN_SPAN_CURRENT_MS) { - final double avgMilliAmps = sumMilliAmps / currentSamples; - return (int) Math.round(Math.abs(avgMilliAmps) / capacityMah * 100.0); + // Source A: averaged current / capacity (average shared with the displayed avg current, #173). + final int avgMicroAmps = averagedCurrentMicroAmps(window); + if (capacityMah > 0 && avgMicroAmps != PROPERTY_UNSUPPORTED) { + return (int) Math.round(Math.abs(avgMicroAmps / 1000.0) / capacityMah * 100.0); } // Source B: level change over time (capacity-free). @@ -416,6 +451,26 @@ public static String formatCurrentValue(final Context context, final int signedM return context.getString(R.string.battery_current_value, sign + Math.abs(signedMilliAmps)); } + /** + * Formats the instantaneous current with its windowed average, e.g. {@code "−208 mA (avg: −245)"} + * (#173): the moment value stays the headline, the average gives the number a stable anchor. The + * average repeats the sign but not the unit, to keep the row compact. Western digits in every + * locale (#96). + * + * @param context Application context + * @param signedMilliAmps signed instantaneous current in mA + * @param signedAvgMilliAmps signed windowed-average current in mA + * + * @return the formatted current string + */ + public static String formatCurrentWithAverage(final Context context, + final int signedMilliAmps, + final int signedAvgMilliAmps) { + final String avgSign = signedAvgMilliAmps >= 0 ? "+" : "−"; + return context.getString(R.string.battery_current_value_with_avg, + formatCurrentValue(context, signedMilliAmps), avgSign + Math.abs(signedAvgMilliAmps)); + } + /** * Estimated minutes until full from the smoothed charge rate (#124): a capacity-free linear projection, * {@code (100 − level) / ratePercentPerHour} hours, so it works even where the charge counter is @@ -525,19 +580,23 @@ record Sample(long timeMillis, int level, int currentMicroAmps) { /** * Result of a rate computation: the smoothed %/h and the signed instantaneous current, each with a - * flag saying whether it is trustworthy enough to display. - * - * @param hasRate whether a trustworthy %/h is available - * @param percentPerHour rate magnitude in %/h (valid only when {@code hasRate}) - * @param charging direction: true charging (charge rate), false discharging (drain rate) - * @param hasCurrent whether a trustworthy instantaneous current is available - * @param currentMilliAmps signed current in mA (valid only when {@code hasCurrent}) + * flag saying whether it is trustworthy enough to display, plus the windowed average current shown + * next to the instant (#173). + * + * @param hasRate whether a trustworthy %/h is available + * @param percentPerHour rate magnitude in %/h (valid only when {@code hasRate}) + * @param charging direction: true charging (charge rate), false discharging (drain rate) + * @param hasCurrent whether a trustworthy instantaneous current is available + * @param currentMilliAmps signed current in mA (valid only when {@code hasCurrent}) + * @param hasAvgCurrent whether the windowed average current is available for display + * @param avgCurrentMilliAmps signed windowed-average current in mA (valid only when {@code hasAvgCurrent}) */ public record BatteryRate(boolean hasRate, int percentPerHour, boolean charging, - boolean hasCurrent, int currentMilliAmps) { + boolean hasCurrent, int currentMilliAmps, + boolean hasAvgCurrent, int avgCurrentMilliAmps) { static BatteryRate empty() { - return new BatteryRate(false, 0, false, false, 0); + return new BatteryRate(false, 0, false, false, 0, false, 0); } } } diff --git a/app/src/main/java/com/almothafar/simplebatterynotifier/ui/fragment/BatteryDetailsFragment.java b/app/src/main/java/com/almothafar/simplebatterynotifier/ui/fragment/BatteryDetailsFragment.java index 3236f00..193bc5f 100644 --- a/app/src/main/java/com/almothafar/simplebatterynotifier/ui/fragment/BatteryDetailsFragment.java +++ b/app/src/main/java/com/almothafar/simplebatterynotifier/ui/fragment/BatteryDetailsFragment.java @@ -385,8 +385,12 @@ private void addRateRows(final View view) { } addTimeToFullRow(view, rate); if (rate.hasCurrent()) { - valuesMap.put(getResources().getString(R.string.battery_current), - BatteryRateTracker.formatCurrentValue(view.getContext(), rate.currentMilliAmps())); + // #173: the moment value stays the headline; the windowed average rides along in parentheses + // once the window has enough data, anchoring the ticking instant to a stable figure. + final String currentValue = rate.hasAvgCurrent() + ? BatteryRateTracker.formatCurrentWithAverage(view.getContext(), rate.currentMilliAmps(), rate.avgCurrentMilliAmps()) + : BatteryRateTracker.formatCurrentValue(view.getContext(), rate.currentMilliAmps()); + valuesMap.put(getResources().getString(R.string.battery_current), currentValue); } } diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 0a39f39..e25a5a3 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -158,6 +158,8 @@ %1$s%%/h %1$s mA + + %1$s (المتوسط: %2$s) ~%1$sh %2$sm ~%1$sm diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 184e5c3..f27f45b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -168,6 +168,9 @@ %1$s%%/h %1$s mA + + %1$s (avg: %2$s) ~%1$sh %2$sm diff --git a/app/src/test/java/com/almothafar/simplebatterynotifier/service/BatteryRateTrackerTest.java b/app/src/test/java/com/almothafar/simplebatterynotifier/service/BatteryRateTrackerTest.java index d62f6e4..938d9f8 100644 --- a/app/src/test/java/com/almothafar/simplebatterynotifier/service/BatteryRateTrackerTest.java +++ b/app/src/test/java/com/almothafar/simplebatterynotifier/service/BatteryRateTrackerTest.java @@ -155,6 +155,50 @@ public void negligibleRate_roundsToZeroAndHides() { assertEquals(-10, rate.currentMilliAmps()); } + @Test + public void momentaryDipBelowFloor_staysShownViaAverageGate() { + // #173: a real ~278 mA draw whose latest instant dips to 6 mA — the windowed average carries + // the floor gate, so the row doesn't blink out; the instant stays the displayed headline. + final List window = Arrays.asList( + new Sample(0, 50, -278_000), + new Sample(30_000, 50, -278_000), + new Sample(60_000, 50, -278_000)); + final BatteryRate rate = BatteryRateTracker.computeRate(window, 0, false, 60_000, -6_000); + + assertTrue(rate.hasCurrent()); + assertEquals(-6, rate.currentMilliAmps()); + assertTrue(rate.hasAvgCurrent()); + assertEquals(-278, rate.avgCurrentMilliAmps()); + } + + @Test + public void sustainedSubFloorAverage_isHidden() { + // #173: when the *average* is also below the floor (Kirin pre-calibration garbage, deep + // sleep), the row stays hidden — the stable gate hides sustained noise, not just instants. + final List window = Arrays.asList( + new Sample(0, 50, -800), + new Sample(30_000, 50, -800), + new Sample(60_000, 50, -800)); + final BatteryRate rate = BatteryRateTracker.computeRate(window, 0, false, 60_000, -800); + + assertFalse(rate.hasCurrent()); + assertFalse(rate.hasAvgCurrent()); + } + + @Test + public void freshWindow_instantGatesItselfWithoutAverage() { + // Two samples can't average yet (#173): the instant carries the floor gate alone and the + // avg segment is absent. + final List window = Arrays.asList( + new Sample(0, 50, -208_000), + new Sample(20_000, 50, -208_000)); + final BatteryRate rate = BatteryRateTracker.computeRate(window, 0, false, 20_000, -208_000); + + assertTrue(rate.hasCurrent()); + assertEquals(-208, rate.currentMilliAmps()); + assertFalse(rate.hasAvgCurrent()); + } + @Test public void kirinMisreadCurrent_isHiddenWhileRateStillShown() { // Kirin/HiSilicon reports CURRENT_NOW in mA, not µA (#152): a real ~800 mA draw arrives as @@ -214,6 +258,53 @@ public void currentUnsupported_isHidden() { } } + /** + * {@link BatteryRateTracker#averagedCurrentMicroAmps}: the shared windowed average (#173) — needs + * enough spaced plausible readings, skips implausible ones, and signals "not yet" otherwise. + */ + public static class AveragedCurrent { + + @Test + public void enoughSamples_returnsMean() { + final List window = Arrays.asList( + new Sample(0, 50, -300_000), + new Sample(30_000, 50, -200_000), + new Sample(60_000, 50, -250_000)); + + assertEquals(-250_000, BatteryRateTracker.averagedCurrentMicroAmps(window)); + } + + @Test + public void implausibleSamplesAreSkipped() { + // The NO_CURRENT sentinel doesn't drag the mean; but with it skipped only 2 plausible + // readings remain, below MIN_CURRENT_SAMPLES -> not yet. + final List window = Arrays.asList( + new Sample(0, 50, -300_000), + new Sample(30_000, 50, NO_CURRENT), + new Sample(60_000, 50, -200_000)); + + assertEquals(NO_CURRENT, BatteryRateTracker.averagedCurrentMicroAmps(window)); + } + + @Test + public void shortSpan_isNotEnough() { + // 3 readings but only 30s of span (< MIN_SPAN_CURRENT_MS): a burst, not a smoothed average. + final List window = Arrays.asList( + new Sample(0, 50, -300_000), + new Sample(15_000, 50, -200_000), + new Sample(30_000, 50, -250_000)); + + assertEquals(NO_CURRENT, BatteryRateTracker.averagedCurrentMicroAmps(window)); + } + + @Test + public void tooFewSamples_isNotEnough() { + assertEquals(NO_CURRENT, BatteryRateTracker.averagedCurrentMicroAmps(List.of())); + assertEquals(NO_CURRENT, BatteryRateTracker.averagedCurrentMicroAmps( + List.of(new Sample(0, 50, -300_000)))); + } + } + /** * {@link BatteryRateTracker#signedCurrentMilliAmps}: magnitude from the reading, sign from the * charging direction (so an OEM's inverted sign convention can't flip the displayed sign). From 0e84a40b2c11374cebdea0131a1586a5d71e2579 Mon Sep 17 00:00:00 2001 From: Al-Mothafar Al-Hasan Date: Fri, 17 Jul 2026 15:58:44 +0300 Subject: [PATCH 2/4] Render the current average as its own smaller line (#173) The inline "-208 mA (avg: -245)" wrapped mid-parenthesis in narrow columns, larger font scales, and Arabic (maintainer on-device feedback). Move the average to a second line under the moment value, rendered at 0.8x size in the label colour so it reads as a quiet anchor rather than competing with the instant. Table cells accept a CharSequence now so the styled value flows through unchanged. Co-Authored-By: Claude Fable 5 --- .../service/BatteryRateTracker.java | 24 ++++------ .../ui/fragment/BatteryDetailsFragment.java | 46 +++++++++++++++---- app/src/main/res/values-ar/strings.xml | 4 +- app/src/main/res/values/strings.xml | 6 +-- .../service/BatteryRateTrackerTest.java | 32 +++++++++++++ 5 files changed, 84 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/com/almothafar/simplebatterynotifier/service/BatteryRateTracker.java b/app/src/main/java/com/almothafar/simplebatterynotifier/service/BatteryRateTracker.java index f886dcd..3ddf36f 100644 --- a/app/src/main/java/com/almothafar/simplebatterynotifier/service/BatteryRateTracker.java +++ b/app/src/main/java/com/almothafar/simplebatterynotifier/service/BatteryRateTracker.java @@ -452,23 +452,19 @@ public static String formatCurrentValue(final Context context, final int signedM } /** - * Formats the instantaneous current with its windowed average, e.g. {@code "−208 mA (avg: −245)"} - * (#173): the moment value stays the headline, the average gives the number a stable anchor. The - * average repeats the sign but not the unit, to keep the row compact. Western digits in every - * locale (#96). + * Formats the windowed-average line shown under the instantaneous current, e.g. {@code "avg: −245"} + * (#173): the moment value stays the headline; this line gives it a stable anchor. The average + * repeats the sign but not the unit, to stay compact. Western digits in every locale (#96). The + * caller renders it as a second, smaller line (see {@code BatteryDetailsFragment}). * - * @param context Application context - * @param signedMilliAmps signed instantaneous current in mA - * @param signedAvgMilliAmps signed windowed-average current in mA + * @param context Application context + * @param signedAvgMilliAmps signed windowed-average current in mA * - * @return the formatted current string + * @return the formatted average line */ - public static String formatCurrentWithAverage(final Context context, - final int signedMilliAmps, - final int signedAvgMilliAmps) { - final String avgSign = signedAvgMilliAmps >= 0 ? "+" : "−"; - return context.getString(R.string.battery_current_value_with_avg, - formatCurrentValue(context, signedMilliAmps), avgSign + Math.abs(signedAvgMilliAmps)); + public static String formatAverageCurrentLine(final Context context, final int signedAvgMilliAmps) { + final String sign = signedAvgMilliAmps >= 0 ? "+" : "−"; // U+2212, matching formatCurrentValue + return context.getString(R.string.battery_current_avg_line, sign + Math.abs(signedAvgMilliAmps)); } /** diff --git a/app/src/main/java/com/almothafar/simplebatterynotifier/ui/fragment/BatteryDetailsFragment.java b/app/src/main/java/com/almothafar/simplebatterynotifier/ui/fragment/BatteryDetailsFragment.java index 193bc5f..860c1ea 100644 --- a/app/src/main/java/com/almothafar/simplebatterynotifier/ui/fragment/BatteryDetailsFragment.java +++ b/app/src/main/java/com/almothafar/simplebatterynotifier/ui/fragment/BatteryDetailsFragment.java @@ -7,6 +7,10 @@ import android.content.Context; import android.os.Bundle; import android.provider.Settings; +import android.text.SpannableString; +import android.text.Spanned; +import android.text.style.ForegroundColorSpan; +import android.text.style.RelativeSizeSpan; import android.util.Log; import android.view.Gravity; import android.view.LayoutInflater; @@ -47,8 +51,11 @@ public class BatteryDetailsFragment extends Fragment { private static final int SCROLL_HINT_BOB_COUNT = 2; // how many bobs private static final int SCROLL_HINT_REVEAL_DP = 96; // max peek distance + // #173: the avg line under the Current value renders at this fraction of the cell's text size. + private static final float AVG_LINE_SIZE_RATIO = 0.8f; + private BatteryDO batteryDO; - private Map valuesMap; + private Map valuesMap; private View viewRef; // #94: when this device's charge counter can't be trusted, the Capacity row shows "Unknown" with a @@ -210,7 +217,7 @@ public void onAnimationEnd(final Animator animation) { * * @return The created table row */ - private TableRow createTableRow(final View view, final String label, final String value, + private TableRow createTableRow(final View view, final String label, final CharSequence value, final int cellPadding, final int cellPaddingTop, final boolean valueUnreliable, final int valueColor) { final TableRow row = new TableRow(view.getContext()); @@ -290,7 +297,7 @@ private TextView createSeparatorTextView(final View view) { * @return The created TextView */ private TextView createValueTextView(final View view, - final String text, + final CharSequence text, final int cellPadding, final int cellPaddingTop, final boolean unreliable, @@ -385,12 +392,7 @@ private void addRateRows(final View view) { } addTimeToFullRow(view, rate); if (rate.hasCurrent()) { - // #173: the moment value stays the headline; the windowed average rides along in parentheses - // once the window has enough data, anchoring the ticking instant to a stable figure. - final String currentValue = rate.hasAvgCurrent() - ? BatteryRateTracker.formatCurrentWithAverage(view.getContext(), rate.currentMilliAmps(), rate.avgCurrentMilliAmps()) - : BatteryRateTracker.formatCurrentValue(view.getContext(), rate.currentMilliAmps()); - valuesMap.put(getResources().getString(R.string.battery_current), currentValue); + valuesMap.put(getResources().getString(R.string.battery_current), currentValueText(view, rate)); } } @@ -421,6 +423,32 @@ private void addTimeToFullRow(final View view, final BatteryRateTracker.BatteryR BatteryRateTracker.formatTimeToFull(view.getContext(), minutes)); } + /** + * The Current row's value (#173): the moment value as the headline, and — once the window has + * enough data — the windowed average on its own second line, rendered smaller and in the label + * colour so it reads as a quiet anchor under the ticking instant. A second line (rather than an + * inline "(avg: …)") because the inline form wrapped mid-parenthesis in narrow columns, larger + * font scales, and Arabic. + * + * @param view The fragment view + * @param rate The already-computed rate for this refresh + * + * @return the styled value text for the Current row + */ + private CharSequence currentValueText(final View view, final BatteryRateTracker.BatteryRate rate) { + final String instant = BatteryRateTracker.formatCurrentValue(view.getContext(), rate.currentMilliAmps()); + if (!rate.hasAvgCurrent()) { + return instant; + } + final String avgLine = BatteryRateTracker.formatAverageCurrentLine(view.getContext(), rate.avgCurrentMilliAmps()); + final SpannableString styled = new SpannableString(instant + "\n" + avgLine); + final int avgStart = instant.length() + 1; + styled.setSpan(new RelativeSizeSpan(AVG_LINE_SIZE_RATIO), avgStart, styled.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + styled.setSpan(new ForegroundColorSpan(GeneralHelper.getColor(getResources(), R.color.battery_details_label_color)), + avgStart, styled.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + return styled; + } + /** * The colour for the drain-rate value: red at/above the user's limit, amber as it approaches (derived * just below the limit), otherwise the default. Charging is left uncoloured in v1 — its context traps diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index e25a5a3..bd27911 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -158,8 +158,8 @@ %1$s%%/h %1$s mA - - %1$s (المتوسط: %2$s) + + المتوسط: %1$s ~%1$sh %2$sm ~%1$sm diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f27f45b..794f624 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -168,9 +168,9 @@ %1$s%%/h %1$s mA - - %1$s (avg: %2$s) + + avg: %1$s ~%1$sh %2$sm diff --git a/app/src/test/java/com/almothafar/simplebatterynotifier/service/BatteryRateTrackerTest.java b/app/src/test/java/com/almothafar/simplebatterynotifier/service/BatteryRateTrackerTest.java index 938d9f8..4e84489 100644 --- a/app/src/test/java/com/almothafar/simplebatterynotifier/service/BatteryRateTrackerTest.java +++ b/app/src/test/java/com/almothafar/simplebatterynotifier/service/BatteryRateTrackerTest.java @@ -458,6 +458,38 @@ public void keepsWesternDigitsUnderArabicLocale() { } } + /** + * {@link BatteryRateTracker#formatAverageCurrentLine}: the "avg: −245" line under the Current value + * (#173) — signed like the instant, unitless for compactness, Western digits in every locale (#96). + */ + @RunWith(RobolectricTestRunner.class) + @Config(sdk = 34) + public static class FormatAverageCurrentLine { + + private Context context; + + @Before + public void setUp() { + context = ApplicationProvider.getApplicationContext(); + } + + @Test + public void dischargingSignsNegative() { + assertEquals("avg: −245", BatteryRateTracker.formatAverageCurrentLine(context, -245)); + } + + @Test + public void chargingSignsPositive() { + assertEquals("avg: +900", BatteryRateTracker.formatAverageCurrentLine(context, 900)); + } + + @Test + @Config(qualifiers = "ar") + public void arabicLabelKeepsWesternDigits() { + assertEquals("المتوسط: −245", BatteryRateTracker.formatAverageCurrentLine(context, -245)); + } + } + /** * {@link BatteryRateTracker#isChargingDirection}: charging and full map to the charging direction. */ From 954b88787f26ec1a8a9cbcab540b8b4d7e6d185e Mon Sep 17 00:00:00 2001 From: Al-Mothafar Al-Hasan Date: Fri, 17 Jul 2026 16:01:15 +0300 Subject: [PATCH 3/4] Avg line: include the mA unit (#173) The unitless "avg: -245" was a leftover of the abandoned inline layout's width squeeze; on its own line the unit costs nothing and reads clearer. Route the value through formatCurrentValue so sign, unit, and Western-digit handling can't drift from the instant above. Co-Authored-By: Claude Fable 5 --- .../service/BatteryRateTracker.java | 12 ++++++------ app/src/main/res/values/strings.xml | 2 +- .../service/BatteryRateTrackerTest.java | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/almothafar/simplebatterynotifier/service/BatteryRateTracker.java b/app/src/main/java/com/almothafar/simplebatterynotifier/service/BatteryRateTracker.java index 3ddf36f..21bcb3e 100644 --- a/app/src/main/java/com/almothafar/simplebatterynotifier/service/BatteryRateTracker.java +++ b/app/src/main/java/com/almothafar/simplebatterynotifier/service/BatteryRateTracker.java @@ -452,10 +452,11 @@ public static String formatCurrentValue(final Context context, final int signedM } /** - * Formats the windowed-average line shown under the instantaneous current, e.g. {@code "avg: −245"} - * (#173): the moment value stays the headline; this line gives it a stable anchor. The average - * repeats the sign but not the unit, to stay compact. Western digits in every locale (#96). The - * caller renders it as a second, smaller line (see {@code BatteryDetailsFragment}). + * Formats the windowed-average line shown under the instantaneous current, e.g. + * {@code "avg: −245 mA"} (#173): the moment value stays the headline; this line gives it a stable + * anchor. The value goes through {@link #formatCurrentValue} so the sign, unit, and Western-digit + * guarantee (#96) can't drift from the instant above it. The caller renders it as a second, + * smaller line (see {@code BatteryDetailsFragment}). * * @param context Application context * @param signedAvgMilliAmps signed windowed-average current in mA @@ -463,8 +464,7 @@ public static String formatCurrentValue(final Context context, final int signedM * @return the formatted average line */ public static String formatAverageCurrentLine(final Context context, final int signedAvgMilliAmps) { - final String sign = signedAvgMilliAmps >= 0 ? "+" : "−"; // U+2212, matching formatCurrentValue - return context.getString(R.string.battery_current_avg_line, sign + Math.abs(signedAvgMilliAmps)); + return context.getString(R.string.battery_current_avg_line, formatCurrentValue(context, signedAvgMilliAmps)); } /** diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 794f624..af52e9f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -169,7 +169,7 @@ %1$s mA + %1$s is the formatted battery_current_value ("−245 mA") --> avg: %1$s diff --git a/app/src/test/java/com/almothafar/simplebatterynotifier/service/BatteryRateTrackerTest.java b/app/src/test/java/com/almothafar/simplebatterynotifier/service/BatteryRateTrackerTest.java index 4e84489..991b09a 100644 --- a/app/src/test/java/com/almothafar/simplebatterynotifier/service/BatteryRateTrackerTest.java +++ b/app/src/test/java/com/almothafar/simplebatterynotifier/service/BatteryRateTrackerTest.java @@ -475,18 +475,18 @@ public void setUp() { @Test public void dischargingSignsNegative() { - assertEquals("avg: −245", BatteryRateTracker.formatAverageCurrentLine(context, -245)); + assertEquals("avg: −245 mA", BatteryRateTracker.formatAverageCurrentLine(context, -245)); } @Test public void chargingSignsPositive() { - assertEquals("avg: +900", BatteryRateTracker.formatAverageCurrentLine(context, 900)); + assertEquals("avg: +900 mA", BatteryRateTracker.formatAverageCurrentLine(context, 900)); } @Test @Config(qualifiers = "ar") public void arabicLabelKeepsWesternDigits() { - assertEquals("المتوسط: −245", BatteryRateTracker.formatAverageCurrentLine(context, -245)); + assertEquals("المتوسط: −245 mA", BatteryRateTracker.formatAverageCurrentLine(context, -245)); } } From 299fe2f6eee80554f257eb59ef4060b9266f5a5e Mon Sep 17 00:00:00 2001 From: Al-Mothafar Al-Hasan Date: Fri, 17 Jul 2026 16:04:37 +0300 Subject: [PATCH 4/4] Arabic avg line: keep the Latin "avg" label (#173) Maintainer decision: the line is Latin-heavy already (sign + mA), so a lone Arabic word would just break the text direction. Wording to be revisited later. Co-Authored-By: Claude Fable 5 --- app/src/main/res/values-ar/strings.xml | 5 +++-- .../service/BatteryRateTrackerTest.java | 6 ++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index bd27911..dc8b54b 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -158,8 +158,9 @@ %1$s%%/h %1$s mA - - المتوسط: %1$s + + avg: %1$s ~%1$sh %2$sm ~%1$sm diff --git a/app/src/test/java/com/almothafar/simplebatterynotifier/service/BatteryRateTrackerTest.java b/app/src/test/java/com/almothafar/simplebatterynotifier/service/BatteryRateTrackerTest.java index 991b09a..84b5c3c 100644 --- a/app/src/test/java/com/almothafar/simplebatterynotifier/service/BatteryRateTrackerTest.java +++ b/app/src/test/java/com/almothafar/simplebatterynotifier/service/BatteryRateTrackerTest.java @@ -485,8 +485,10 @@ public void chargingSignsPositive() { @Test @Config(qualifiers = "ar") - public void arabicLabelKeepsWesternDigits() { - assertEquals("المتوسط: −245 mA", BatteryRateTracker.formatAverageCurrentLine(context, -245)); + public void arabicKeepsLatinAvgLabel() { + // Deliberately identical to English (maintainer decision): the line is Latin-heavy already + // (sign + mA), so a lone Arabic word would just break the text direction. Revisit later. + assertEquals("avg: −245 mA", BatteryRateTracker.formatAverageCurrentLine(context, -245)); } }