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 604d571..94ab547 100644
--- a/app/src/main/java/com/almothafar/simplebatterynotifier/service/BatteryRateTracker.java
+++ b/app/src/main/java/com/almothafar/simplebatterynotifier/service/BatteryRateTracker.java
@@ -411,6 +411,45 @@ public static String formatCurrentValue(final Context context, final int signedM
return context.getString(R.string.battery_current_value, sign + Math.abs(signedMilliAmps));
}
+ /**
+ * 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
+ * unreliable (Kirin). Deliberately simple — it does not model the charge-curve taper above ~80%,
+ * so it overshoots near full; precision is a non-goal (the OS owns the accurate figure). Callers gate on
+ * charging + a trustworthy rate + a level below the taper top before showing it. Pure so it is testable.
+ *
+ * @param level current battery level (0-100)
+ * @param ratePercentPerHour smoothed charge-rate magnitude in %/h
+ *
+ * @return estimated minutes to full, or 0 when not computable (non-positive rate, or already at/above full)
+ */
+ public static int estimateMinutesToFull(final int level, final int ratePercentPerHour) {
+ if (ratePercentPerHour <= 0 || level >= 100) {
+ return 0;
+ }
+ final int remaining = 100 - level;
+ return (int) Math.round(remaining * 60.0 / ratePercentPerHour);
+ }
+
+ /**
+ * Formats a time-to-full estimate as a compact {@code "~1h 20m"} / {@code "~45m"}, with Western digits
+ * in every locale (#96). The units stay Latin (h/m), matching {@code battery_rate_value} and
+ * {@code design_capacity_value}.
+ *
+ * @param context Application context
+ * @param totalMinutes estimated minutes to full (from {@link #estimateMinutesToFull}); must be > 0
+ *
+ * @return the formatted duration string
+ */
+ public static String formatTimeToFull(final Context context, final int totalMinutes) {
+ final int hours = totalMinutes / 60;
+ final int minutes = totalMinutes % 60;
+ if (hours > 0) {
+ return context.getString(R.string.time_to_full_value_hm, String.valueOf(hours), String.valueOf(minutes));
+ }
+ return context.getString(R.string.time_to_full_value_m, String.valueOf(minutes));
+ }
+
/**
* Serializes a window to a compact string ("t:level:currentUa" joined by ';'). Pure and testable.
*
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 dc217ea..3236f00 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
@@ -383,12 +383,40 @@ private void addRateRows(final View view) {
valuesMap.put(rateLabel, BatteryRateTracker.formatRateValue(view.getContext(), rate.percentPerHour()));
rateValueColor = rateColor(view.getContext(), rate);
}
+ addTimeToFullRow(view, rate);
if (rate.hasCurrent()) {
valuesMap.put(getResources().getString(R.string.battery_current),
BatteryRateTracker.formatCurrentValue(view.getContext(), rate.currentMilliAmps()));
}
}
+ /**
+ * Adds the estimated time-to-full row directly below the charge rate it is derived from (#124). Shown
+ * only while charging, with a trustworthy rate, and below the taper top (level < 99%) — hidden
+ * otherwise, so the user never sees a garbage "0m" or a wildly optimistic figure right as the charge
+ * tapers. The estimate is a rough linear projection (see
+ * {@link BatteryRateTracker#estimateMinutesToFull}); precision is a non-goal — the OS owns the accurate
+ * number. Reuses the already-computed {@code rate}; only the level is read here.
+ *
+ * @param view The fragment view
+ * @param rate The already-computed rate for this refresh
+ */
+ private void addTimeToFullRow(final View view, final BatteryRateTracker.BatteryRate rate) {
+ if (!rate.charging() || !rate.hasRate()) {
+ return;
+ }
+ final int level = Math.round(batteryDO.getBatteryPercentage());
+ if (level >= 99) {
+ return;
+ }
+ final int minutes = BatteryRateTracker.estimateMinutesToFull(level, rate.percentPerHour());
+ if (minutes <= 0) {
+ return;
+ }
+ valuesMap.put(getResources().getString(R.string.time_to_full),
+ BatteryRateTracker.formatTimeToFull(view.getContext(), minutes));
+ }
+
/**
* 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 c94e343..72404bf 100644
--- a/app/src/main/res/values-ar/strings.xml
+++ b/app/src/main/res/values-ar/strings.xml
@@ -30,6 +30,8 @@
معدل الاستهلاكمعدل الشحنالتيار
+
+ الوقت حتى الاكتمالعلى البطاريةمقبس طاقة
@@ -154,6 +156,9 @@
%1$s%%/h%1$s mA
+
+ ~%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 a80ae81..970f02e 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -32,6 +32,8 @@
Drain rateCharge rateCurrent
+
+ Time to fullOn BatteryAC Charger
@@ -162,6 +164,10 @@
%1$s%%/h%1$s mA
+
+ ~%1$sh %2$sm
+ ~%1$smBattery DrainShow rate in status notificationThe ongoing notification shows the live drain/charge rate next to the status
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 15a34f0..670cfea 100644
--- a/app/src/test/java/com/almothafar/simplebatterynotifier/service/BatteryRateTrackerTest.java
+++ b/app/src/test/java/com/almothafar/simplebatterynotifier/service/BatteryRateTrackerTest.java
@@ -1,17 +1,23 @@
package com.almothafar.simplebatterynotifier.service;
+import android.content.Context;
import android.os.BatteryManager;
+import androidx.test.core.app.ApplicationProvider;
+
import com.almothafar.simplebatterynotifier.model.BatteryDO;
import com.almothafar.simplebatterynotifier.service.BatteryRateTracker.BatteryRate;
import com.almothafar.simplebatterynotifier.service.BatteryRateTracker.Sample;
+import org.junit.Before;
import org.junit.Test;
import org.junit.experimental.runners.Enclosed;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
import java.util.Arrays;
import java.util.Collection;
@@ -252,6 +258,78 @@ public void matchesExpected() {
}
}
+ /**
+ * {@link BatteryRateTracker#estimateMinutesToFull}: the capacity-free linear projection (#124) and its
+ * not-computable guards. Boundaries: the taper-top levels (99/100), a zero/negative rate, and rounding.
+ */
+ @RunWith(Parameterized.class)
+ public static class EstimateMinutesToFull {
+
+ @Parameter(0) public String label;
+ @Parameter(1) public int level;
+ @Parameter(2) public int ratePercentPerHour;
+ @Parameter(3) public int expectedMinutes;
+
+ @Parameters(name = "{0}")
+ public static Collection