diff --git a/app/src/main/java/com/almothafar/simplebatterynotifier/model/LevelThresholds.java b/app/src/main/java/com/almothafar/simplebatterynotifier/model/LevelThresholds.java
new file mode 100644
index 0000000..9e25b46
--- /dev/null
+++ b/app/src/main/java/com/almothafar/simplebatterynotifier/model/LevelThresholds.java
@@ -0,0 +1,16 @@
+package com.almothafar.simplebatterynotifier.model;
+
+/**
+ * The two battery-level alert thresholds as one value, so critical and warning travel together instead
+ * of as a loose {@code (int, int)} pair callers must keep in the right order (#162). Critical is the
+ * lower, more urgent level; warning the higher.
+ *
+ * No ordering invariant is enforced here: the range slider structurally keeps critical below warning,
+ * and any persisted or corrupt pair is reconciled by
+ * {@code BatteryRangeSliderHelper.clampPair(LevelThresholds, int, int, int)} before it is shown.
+ *
+ * @param critical the critical level in percent — turns the gauge red and fires the critical alert
+ * @param warning the warning level in percent — turns the gauge amber and fires the warning alert
+ */
+public record LevelThresholds(int critical, int warning) {
+}
diff --git a/app/src/main/java/com/almothafar/simplebatterynotifier/receiver/BatteryLevelReceiver.java b/app/src/main/java/com/almothafar/simplebatterynotifier/receiver/BatteryLevelReceiver.java
index 8a71690..4d339fa 100644
--- a/app/src/main/java/com/almothafar/simplebatterynotifier/receiver/BatteryLevelReceiver.java
+++ b/app/src/main/java/com/almothafar/simplebatterynotifier/receiver/BatteryLevelReceiver.java
@@ -15,6 +15,7 @@
import com.almothafar.simplebatterynotifier.service.NotificationService;
import com.almothafar.simplebatterynotifier.service.SlowChargeDetector;
import com.almothafar.simplebatterynotifier.service.SystemService;
+import com.almothafar.simplebatterynotifier.util.AppPrefs;
import com.almothafar.simplebatterynotifier.util.TemperatureUtils;
import static java.util.Objects.isNull;
@@ -104,8 +105,8 @@ public void onReceive(final Context context, final Intent intent) {
final SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(context);
final LevelAlertConfig config = new LevelAlertConfig(
- sharedPref.getInt(context.getString(R.string._pref_key_critical_battery_level), 20),
- sharedPref.getInt(context.getString(R.string._pref_key_warn_battery_level), 40),
+ AppPrefs.criticalLevel(context),
+ AppPrefs.warningLevel(context),
sharedPref.getBoolean(context.getString(R.string._pref_key_notify_for_warning_level), true),
sharedPref.getBoolean(context.getString(R.string._pref_key_notify_for_full_level), true),
sharedPref.getBoolean(context.getString(R.string._pref_key_notify_every_tick), false));
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 a476284..6a441c8 100644
--- a/app/src/main/java/com/almothafar/simplebatterynotifier/service/BatteryRateTracker.java
+++ b/app/src/main/java/com/almothafar/simplebatterynotifier/service/BatteryRateTracker.java
@@ -65,12 +65,6 @@ public final class BatteryRateTracker {
// Above this the derived %/h is garbage (e.g. a wrong-unit current); reject rather than display it.
static final int MAX_PLAUSIBLE_RATE_PPH = 500;
- // The "red / high drain" limit shared with the fast-drain alert (#109): default and accepted range.
- // MIN/MAX must match the slider bounds (android:min/android:max) in pref_alerts.xml; they are
- // enforced when the preference is read, so an out-of-range stored value can't skew the red line.
- public static final int DEFAULT_DRAIN_LIMIT_PPH = 20;
- public static final int MIN_DRAIN_LIMIT_PPH = 5;
- public static final int MAX_DRAIN_LIMIT_PPH = 60;
// Amber sits just below the red limit; 0.75x keeps colour reserved for genuinely high drain (#108).
private static final float AMBER_RATIO = 0.75f;
@@ -386,33 +380,6 @@ static int signedCurrentMilliAmps(final int microAmps, final boolean charging) {
return charging ? magnitude : -magnitude;
}
- /**
- * The user's shared "high drain" limit in %/h — the red line in the details table (#108) and the
- * fast-drain alert trigger (#109). Defaults to {@link #DEFAULT_DRAIN_LIMIT_PPH}.
- *
- * @param context Application context
- *
- * @return the configured limit in %/h
- */
- public static int getDrainLimitPercentPerHour(final Context context) {
- final int stored = PreferenceManager.getDefaultSharedPreferences(context)
- .getInt(context.getString(R.string._pref_key_fast_drain_limit), DEFAULT_DRAIN_LIMIT_PPH);
- return clampDrainLimit(stored);
- }
-
- /**
- * Clamps a stored drain limit to the accepted range, so a corrupt or out-of-range preference value
- * can't skew the red line here or the fast-drain trigger in #109. The bounds mirror the slider's
- * {@code android:min}/{@code android:max} in {@code pref_alerts.xml}. Pure so it is unit-testable.
- *
- * @param stored the raw persisted limit in %/h
- *
- * @return the limit clamped to [{@link #MIN_DRAIN_LIMIT_PPH}, {@link #MAX_DRAIN_LIMIT_PPH}]
- */
- static int clampDrainLimit(final int stored) {
- return Math.max(MIN_DRAIN_LIMIT_PPH, Math.min(MAX_DRAIN_LIMIT_PPH, stored));
- }
-
/**
* The amber threshold derived just below a given red limit (#108). Pure so it is unit-testable.
*
diff --git a/app/src/main/java/com/almothafar/simplebatterynotifier/service/FastDrainDetector.java b/app/src/main/java/com/almothafar/simplebatterynotifier/service/FastDrainDetector.java
index b56877e..8979bd9 100644
--- a/app/src/main/java/com/almothafar/simplebatterynotifier/service/FastDrainDetector.java
+++ b/app/src/main/java/com/almothafar/simplebatterynotifier/service/FastDrainDetector.java
@@ -10,6 +10,7 @@
import com.almothafar.simplebatterynotifier.service.SustainedConditionTracker.Outcome;
import com.almothafar.simplebatterynotifier.service.SustainedConditionTracker.Streak;
import com.almothafar.simplebatterynotifier.service.SustainedConditionTracker.StreakStore;
+import com.almothafar.simplebatterynotifier.util.AppPrefs;
import static java.util.Objects.isNull;
@@ -80,7 +81,7 @@ public static void evaluate(Context context, BatteryDO batteryDO, BatteryRate ra
return;
}
- final int limit = BatteryRateTracker.getDrainLimitPercentPerHour(context);
+ final int limit = AppPrefs.drainLimitPph(context);
final long sustainedMs = minutesPref(prefs, context, R.string._pref_key_fast_drain_sustained_minutes,
DEFAULT_SUSTAINED_MINUTES, MIN_SUSTAINED_MINUTES, MAX_SUSTAINED_MINUTES);
final long reminderGapMs = minutesPref(prefs, context, R.string._pref_key_fast_drain_reminder_minutes,
@@ -143,7 +144,7 @@ private static long minutesPref(SharedPreferences prefs,
/**
* Clamps a stored minutes preference to its slider range and converts to millis. Mirrors
- * {@link BatteryRateTracker#clampDrainLimit}: the slider constrains UI input, but a corrupt or
+ * {@link AppPrefs#clampDrainLimit}: the slider constrains UI input, but a corrupt or
* out-of-range stored value (e.g. 0 sustained minutes) would otherwise defeat the sustained-window
* requirement and fire on a momentary spike. Pure so it is unit-testable.
*
diff --git a/app/src/main/java/com/almothafar/simplebatterynotifier/service/NotificationService.java b/app/src/main/java/com/almothafar/simplebatterynotifier/service/NotificationService.java
index 94a9569..6ce3485 100644
--- a/app/src/main/java/com/almothafar/simplebatterynotifier/service/NotificationService.java
+++ b/app/src/main/java/com/almothafar/simplebatterynotifier/service/NotificationService.java
@@ -31,6 +31,7 @@
import com.almothafar.simplebatterynotifier.model.ChargeSpeed;
import com.almothafar.simplebatterynotifier.model.ChargeSpeedTier;
import com.almothafar.simplebatterynotifier.ui.MainActivity;
+import com.almothafar.simplebatterynotifier.util.AppPrefs;
import com.almothafar.simplebatterynotifier.util.BatteryPercentFormatter;
import com.almothafar.simplebatterynotifier.util.GeneralHelper;
import com.almothafar.simplebatterynotifier.util.TemperatureUtils;
@@ -1407,8 +1408,8 @@ private static class NotificationConfig {
this.type = type;
// Load common preferences
- final int warningLevel = prefs.getInt(context.getString(R.string._pref_key_warn_battery_level), 40);
- final int criticalLevel = prefs.getInt(context.getString(R.string._pref_key_critical_battery_level), 20);
+ final int warningLevel = AppPrefs.warningLevel(context);
+ final int criticalLevel = AppPrefs.criticalLevel(context);
this.stickyNotification = prefs.getBoolean(context.getString(R.string._pref_key_notifications_sticky), false);
final boolean withinWindow = isWithinNotificationWindow(context, prefs);
diff --git a/app/src/main/java/com/almothafar/simplebatterynotifier/ui/MainActivity.java b/app/src/main/java/com/almothafar/simplebatterynotifier/ui/MainActivity.java
index 96064bf..0ca481e 100644
--- a/app/src/main/java/com/almothafar/simplebatterynotifier/ui/MainActivity.java
+++ b/app/src/main/java/com/almothafar/simplebatterynotifier/ui/MainActivity.java
@@ -3,7 +3,6 @@
import android.Manifest;
import android.content.ActivityNotFoundException;
import android.content.Intent;
-import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.BatteryManager;
@@ -28,13 +27,14 @@
import androidx.annotation.NonNull;
import androidx.appcompat.widget.Toolbar;
import androidx.core.content.ContextCompat;
-import androidx.preference.PreferenceManager;
import com.almothafar.simplebatterynotifier.R;
import com.almothafar.simplebatterynotifier.model.BatteryDO;
+import com.almothafar.simplebatterynotifier.model.LevelThresholds;
import com.almothafar.simplebatterynotifier.service.BatteryHealthTracker;
import com.almothafar.simplebatterynotifier.service.PowerConnectionService;
import com.almothafar.simplebatterynotifier.service.SystemService;
import com.almothafar.simplebatterynotifier.ui.widget.HorseshoeProgressBar;
+import com.almothafar.simplebatterynotifier.util.AppPrefs;
import com.almothafar.simplebatterynotifier.util.BatteryPercentFormatter;
import java.util.List;
@@ -305,10 +305,9 @@ private static HorseshoeProgressBar.Flow flowOf(final int batteryStatus) {
*/
private void initializeFirstValues() {
fillBatteryInfo();
- final SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this);
- batteryGauge.setThresholds(sharedPref.getInt(getString(R.string._pref_key_critical_battery_level), 20),
- sharedPref.getInt(getString(R.string._pref_key_warn_battery_level), 40));
+ final LevelThresholds levels = AppPrefs.batteryLevels(this);
+ batteryGauge.setThresholds(levels.critical(), levels.warning());
// Keep the in-fly slider in sync with values that may have changed in Settings.
syncThresholdSlider();
@@ -356,15 +355,11 @@ public void onStartTrackingTouch(@NonNull final RangeSlider slider) {
@Override
public void onStopTrackingTouch(@NonNull final RangeSlider slider) {
final List values = slider.getValues();
- final int critical = Math.round(values.get(0));
- final int warning = Math.round(values.get(1));
+ final LevelThresholds levels = new LevelThresholds(Math.round(values.get(0)), Math.round(values.get(1)));
- PreferenceManager.getDefaultSharedPreferences(MainActivity.this).edit()
- .putInt(getString(R.string._pref_key_critical_battery_level), critical)
- .putInt(getString(R.string._pref_key_warn_battery_level), warning)
- .apply();
+ AppPrefs.setBatteryLevels(MainActivity.this, levels);
- batteryGauge.setThresholds(critical, warning);
+ batteryGauge.setThresholds(levels.critical(), levels.warning());
}
});
@@ -379,18 +374,13 @@ private void syncThresholdSlider() {
if (isNull(thresholdSlider)) {
return;
}
- final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
- final int critical = prefs.getInt(getString(R.string._pref_key_critical_battery_level),
- BatteryRangeSliderHelper.DEFAULT_CRITICAL);
- final int warning = prefs.getInt(getString(R.string._pref_key_warn_battery_level),
- BatteryRangeSliderHelper.DEFAULT_WARNING);
- final int[] pair = BatteryRangeSliderHelper.clampPair(critical, warning,
+ final LevelThresholds pair = BatteryRangeSliderHelper.clampPair(AppPrefs.batteryLevels(this),
BatteryRangeSliderHelper.LEVEL_FROM, BatteryRangeSliderHelper.LEVEL_TO,
BatteryRangeSliderHelper.MIN_SEPARATION);
- thresholdSlider.setValues((float) pair[0], (float) pair[1]);
+ thresholdSlider.setValues((float) pair.critical(), (float) pair.warning());
updateThresholdCaptions(findViewById(R.id.thresholdCriticalCaption),
- findViewById(R.id.thresholdWarningCaption), pair[0], pair[1]);
+ findViewById(R.id.thresholdWarningCaption), pair.critical(), pair.warning());
}
private void updateThresholdCaptions(final TextView criticalCaption, final TextView warningCaption,
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 10f89d4..c35bc11 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
@@ -28,6 +28,7 @@
import com.almothafar.simplebatterynotifier.service.BatteryHealthTracker;
import com.almothafar.simplebatterynotifier.service.BatteryRateTracker;
import com.almothafar.simplebatterynotifier.service.SystemService;
+import com.almothafar.simplebatterynotifier.util.AppPrefs;
import com.almothafar.simplebatterynotifier.util.GeneralHelper;
import com.almothafar.simplebatterynotifier.util.TemperatureUtils;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
@@ -632,7 +633,7 @@ private int rateColor(final Context context, final BatteryRateTracker.BatteryRat
if (rate.charging()) {
return 0;
}
- final int limit = BatteryRateTracker.getDrainLimitPercentPerHour(context);
+ final int limit = AppPrefs.drainLimitPph(context);
if (rate.percentPerHour() >= limit) {
return GeneralHelper.getColor(getResources(), R.color.battery_rate_high);
}
diff --git a/app/src/main/java/com/almothafar/simplebatterynotifier/ui/preference/BatteryRangeSliderHelper.java b/app/src/main/java/com/almothafar/simplebatterynotifier/ui/preference/BatteryRangeSliderHelper.java
index 7312e14..ccba834 100644
--- a/app/src/main/java/com/almothafar/simplebatterynotifier/ui/preference/BatteryRangeSliderHelper.java
+++ b/app/src/main/java/com/almothafar/simplebatterynotifier/ui/preference/BatteryRangeSliderHelper.java
@@ -1,6 +1,7 @@
package com.almothafar.simplebatterynotifier.ui.preference;
import com.almothafar.simplebatterynotifier.R;
+import com.almothafar.simplebatterynotifier.model.LevelThresholds;
import com.google.android.material.slider.RangeSlider;
/**
@@ -20,10 +21,6 @@ public final class BatteryRangeSliderHelper {
public static final int LEVEL_TO = 50;
/** Minimum gap kept between the critical and warning thumbs, in percent. */
public static final int MIN_SEPARATION = 5;
- /** Default critical level, matching the historical {@code SeekBarPreference} default. */
- public static final int DEFAULT_CRITICAL = 20;
- /** Default warning level, matching the historical {@code SeekBarPreference} default. */
- public static final int DEFAULT_WARNING = 40;
private BatteryRangeSliderHelper() {
// Utility class
@@ -32,7 +29,7 @@ private BatteryRangeSliderHelper() {
/**
* Apply the shared bounds, integer step, minimum thumb separation, and a "{@code N%}" label
* formatter to a slider. Does not set the thumb values — callers set those after clamping via
- * {@link #clampPair(int, int, int, int, int)}.
+ * {@link #clampPair(LevelThresholds, int, int, int)}.
*
* @param slider the range slider to configure
* @param from combined track start (critical floor)
@@ -50,22 +47,27 @@ public static void configure(final RangeSlider slider, final int from, final int
}
/**
- * Clamp a (critical, warning) pair into {@code [from, to]} while keeping them at least
- * {@code minSeparation} apart, so persisted or corrupted values can always be shown on the
- * slider without tripping its validation.
+ * Clamp a {@link LevelThresholds} pair into {@code [from, to]} while keeping the two levels at least
+ * {@code minSeparation} apart, so persisted or corrupted values can always be shown on the slider
+ * without tripping its validation.
*
- * @return a two-element array {@code {critical, warning}}
+ * @param levels the raw (critical, warning) pair to reconcile
+ * @param from combined track start (critical floor)
+ * @param to combined track end (warning ceiling)
+ * @param minSeparation minimum gap between the two thumbs, in value units
+ *
+ * @return a clamped {@code (critical, warning)} pair the slider can always accept
*/
- public static int[] clampPair(final int critical, final int warning,
- final int from, final int to, final int minSeparation) {
- int low = clamp(critical, from, to);
- int high = clamp(warning, from, to);
+ public static LevelThresholds clampPair(final LevelThresholds levels,
+ final int from, final int to, final int minSeparation) {
+ int low = clamp(levels.critical(), from, to);
+ int high = clamp(levels.warning(), from, to);
if (high - low < minSeparation) {
// Push the warning thumb up first; if that hits the ceiling, pull critical back down.
high = Math.min(to, low + minSeparation);
low = Math.max(from, high - minSeparation);
}
- return new int[]{low, high};
+ return new LevelThresholds(low, high);
}
private static int clamp(final int value, final int lo, final int hi) {
diff --git a/app/src/main/java/com/almothafar/simplebatterynotifier/ui/preference/BatteryRangeSliderPreference.java b/app/src/main/java/com/almothafar/simplebatterynotifier/ui/preference/BatteryRangeSliderPreference.java
index 6139da7..c4e386c 100644
--- a/app/src/main/java/com/almothafar/simplebatterynotifier/ui/preference/BatteryRangeSliderPreference.java
+++ b/app/src/main/java/com/almothafar/simplebatterynotifier/ui/preference/BatteryRangeSliderPreference.java
@@ -11,6 +11,8 @@
import androidx.preference.PreferenceViewHolder;
import com.almothafar.simplebatterynotifier.R;
+import com.almothafar.simplebatterynotifier.model.LevelThresholds;
+import com.almothafar.simplebatterynotifier.util.AppPrefs;
import com.google.android.material.slider.RangeSlider;
import java.util.List;
@@ -37,8 +39,8 @@ public class BatteryRangeSliderPreference extends Preference {
private int from = BatteryRangeSliderHelper.LEVEL_FROM;
private int to = BatteryRangeSliderHelper.LEVEL_TO;
private int minSeparation = BatteryRangeSliderHelper.MIN_SEPARATION;
- private int criticalDefault = BatteryRangeSliderHelper.DEFAULT_CRITICAL;
- private int warningDefault = BatteryRangeSliderHelper.DEFAULT_WARNING;
+ private int criticalDefault = AppPrefs.DEFAULT_CRITICAL_LEVEL;
+ private int warningDefault = AppPrefs.DEFAULT_WARNING_LEVEL;
public BatteryRangeSliderPreference(final Context context, final AttributeSet attrs,
final int defStyleAttr, final int defStyleRes) {
@@ -102,15 +104,15 @@ public void onBindViewHolder(@NonNull final PreferenceViewHolder holder) {
slider.clearOnChangeListeners();
slider.clearOnSliderTouchListeners();
- final int[] pair = readClampedValues();
- slider.setValues((float) pair[0], (float) pair[1]);
- updateCaptions(criticalCaption, warningCaption, pair[0], pair[1]);
+ final LevelThresholds pair = readClampedValues();
+ slider.setValues((float) pair.critical(), (float) pair.warning());
+ updateCaptions(criticalCaption, warningCaption, pair.critical(), pair.warning());
// Update the captions live while dragging; persist only when the drag ends to avoid a burst
// of writes and readers seeing half-applied intermediate values.
slider.addOnChangeListener((s, value, fromUser) -> {
- final int[] current = currentValues(s);
- updateCaptions(criticalCaption, warningCaption, current[0], current[1]);
+ final LevelThresholds current = currentValues(s);
+ updateCaptions(criticalCaption, warningCaption, current.critical(), current.warning());
});
slider.addOnSliderTouchListener(new RangeSlider.OnSliderTouchListener() {
@@ -121,8 +123,7 @@ public void onStartTrackingTouch(@NonNull final RangeSlider s) {
@Override
public void onStopTrackingTouch(@NonNull final RangeSlider s) {
- final int[] current = currentValues(s);
- persist(current[0], current[1]);
+ persist(currentValues(s));
}
});
}
@@ -131,19 +132,19 @@ public void onStopTrackingTouch(@NonNull final RangeSlider s) {
* Read the persisted critical/warning values (falling back to the defaults) and clamp them into
* the slider's bounds and separation so they can always be applied safely.
*/
- private int[] readClampedValues() {
+ private LevelThresholds readClampedValues() {
final SharedPreferences prefs = getSharedPreferences();
final int critical = nonNull(prefs) ? prefs.getInt(criticalKey, criticalDefault) : criticalDefault;
final int warning = nonNull(prefs) ? prefs.getInt(warningKey, warningDefault) : warningDefault;
- return BatteryRangeSliderHelper.clampPair(critical, warning, from, to, minSeparation);
+ return BatteryRangeSliderHelper.clampPair(new LevelThresholds(critical, warning), from, to, minSeparation);
}
- private void persist(final int critical, final int warning) {
+ private void persist(final LevelThresholds levels) {
final SharedPreferences prefs = getSharedPreferences();
if (nonNull(prefs)) {
prefs.edit()
- .putInt(criticalKey, critical)
- .putInt(warningKey, warning)
+ .putInt(criticalKey, levels.critical())
+ .putInt(warningKey, levels.warning())
.apply();
}
}
@@ -158,9 +159,9 @@ private void updateCaptions(final TextView criticalCaption, final TextView warni
}
}
- private static int[] currentValues(final RangeSlider slider) {
+ private static LevelThresholds currentValues(final RangeSlider slider) {
final List values = slider.getValues();
- return new int[]{Math.round(values.get(0)), Math.round(values.get(1))};
+ return new LevelThresholds(Math.round(values.get(0)), Math.round(values.get(1)));
}
private static String orDefault(final String value, final String fallback) {
diff --git a/app/src/main/java/com/almothafar/simplebatterynotifier/util/AppPrefs.java b/app/src/main/java/com/almothafar/simplebatterynotifier/util/AppPrefs.java
new file mode 100644
index 0000000..a768e08
--- /dev/null
+++ b/app/src/main/java/com/almothafar/simplebatterynotifier/util/AppPrefs.java
@@ -0,0 +1,130 @@
+package com.almothafar.simplebatterynotifier.util;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import androidx.preference.PreferenceManager;
+
+import com.almothafar.simplebatterynotifier.R;
+import com.almothafar.simplebatterynotifier.model.LevelThresholds;
+
+/**
+ * Typed facade over the app's default {@link SharedPreferences} (#162): the single owner of each
+ * migrated setting's key + default + clamp. Read sites call the typed accessor instead of
+ * repeating {@code getInt(getString(R.string._pref_key_…), )}, so a default can never
+ * silently drift between the alert engine, the receiver and the UI.
+ *
+ * Migrated so far:
+ *
+ *
the critical/warning battery levels — the {@code 20}/{@code 40} literals that previously lived
+ * in {@code NotificationService}, {@code BatteryLevelReceiver}, {@code MainActivity} and the range
+ * slider's helper now derive from {@link #DEFAULT_CRITICAL_LEVEL} / {@link #DEFAULT_WARNING_LEVEL},
+ * and the pair travels as a {@link LevelThresholds};
+ *
the shared "high drain" limit — its default, accepted range and clamp ({@link #drainLimitPph}
+ * + {@link #clampDrainLimit}) moved here from {@code BatteryRateTracker}, so "a corrupt stored
+ * value can't defeat the feature" lives in one place.
+ *
+ * The one restatement that remains is the XML-declared slider default in {@code pref_alerts.xml}, which
+ * the framework instantiates from XML and so cannot share a constant with — a comment ties the two, and
+ * {@code AppPrefsTest} asserts they stay equal. Remaining settings migrate incrementally.
+ */
+public final class AppPrefs {
+
+ /** Default critical battery level in percent — the single owner of this value (#162). */
+ public static final int DEFAULT_CRITICAL_LEVEL = 20;
+ /** Default warning battery level in percent — the single owner of this value (#162). */
+ public static final int DEFAULT_WARNING_LEVEL = 40;
+
+ /** Default "high drain" limit in %/h. */
+ public static final int DEFAULT_DRAIN_LIMIT_PPH = 20;
+ /** Lowest accepted drain limit in %/h; mirrors the slider's {@code android:min} in pref_alerts.xml. */
+ public static final int MIN_DRAIN_LIMIT_PPH = 5;
+ /** Highest accepted drain limit in %/h; mirrors the slider's {@code android:max} in pref_alerts.xml. */
+ public static final int MAX_DRAIN_LIMIT_PPH = 60;
+
+ private AppPrefs() {
+ // Utility class
+ }
+
+ /**
+ * The critical battery level in percent: the level at/below which the critical alert fires and the
+ * home gauge turns red. Falls back to {@link #DEFAULT_CRITICAL_LEVEL} when unset.
+ *
+ * @param context Application context
+ *
+ * @return the configured critical level
+ */
+ public static int criticalLevel(final Context context) {
+ return prefs(context).getInt(context.getString(R.string._pref_key_critical_battery_level), DEFAULT_CRITICAL_LEVEL);
+ }
+
+ /**
+ * The warning battery level in percent: the level at/below which the warning alert fires and the
+ * home gauge turns amber. Falls back to {@link #DEFAULT_WARNING_LEVEL} when unset.
+ *
+ * @param context Application context
+ *
+ * @return the configured warning level
+ */
+ public static int warningLevel(final Context context) {
+ return prefs(context).getInt(context.getString(R.string._pref_key_warn_battery_level), DEFAULT_WARNING_LEVEL);
+ }
+
+ /**
+ * Both battery-level thresholds as one value, so critical and warning travel together instead of as a
+ * loose (int, int) pair callers must keep in the right order.
+ *
+ * @param context Application context
+ *
+ * @return the configured {@code (critical, warning)} thresholds
+ */
+ public static LevelThresholds batteryLevels(final Context context) {
+ return new LevelThresholds(criticalLevel(context), warningLevel(context));
+ }
+
+ /**
+ * Persist both battery-level thresholds together. The settings-screen slider and the home-screen
+ * in-fly slider both write this pair, so the write lives here beside the matching reads.
+ *
+ * @param context Application context
+ * @param levels the thresholds to store
+ */
+ public static void setBatteryLevels(final Context context, final LevelThresholds levels) {
+ prefs(context).edit()
+ .putInt(context.getString(R.string._pref_key_critical_battery_level), levels.critical())
+ .putInt(context.getString(R.string._pref_key_warn_battery_level), levels.warning())
+ .apply();
+ }
+
+ /**
+ * The user's shared "high drain" limit in %/h — the red line in the details table (#108) and the
+ * fast-drain alert trigger (#109). Reads {@link #DEFAULT_DRAIN_LIMIT_PPH} when unset and always
+ * {@link #clampDrainLimit clamps} the stored value, so a corrupt preference can't skew the red line
+ * or the alert trigger.
+ *
+ * @param context Application context
+ *
+ * @return the configured limit in %/h
+ */
+ public static int drainLimitPph(final Context context) {
+ return clampDrainLimit(prefs(context).getInt(
+ context.getString(R.string._pref_key_fast_drain_limit), DEFAULT_DRAIN_LIMIT_PPH));
+ }
+
+ /**
+ * Clamps a stored drain limit to {@code [MIN_DRAIN_LIMIT_PPH, MAX_DRAIN_LIMIT_PPH]}. The bounds mirror
+ * the slider's {@code android:min}/{@code android:max} in {@code pref_alerts.xml}. Pure so it is
+ * unit-testable.
+ *
+ * @param stored the raw persisted limit in %/h
+ *
+ * @return the limit clamped to {@code [MIN_DRAIN_LIMIT_PPH, MAX_DRAIN_LIMIT_PPH]}
+ */
+ public static int clampDrainLimit(final int stored) {
+ return Math.max(MIN_DRAIN_LIMIT_PPH, Math.min(MAX_DRAIN_LIMIT_PPH, stored));
+ }
+
+ private static SharedPreferences prefs(final Context context) {
+ return PreferenceManager.getDefaultSharedPreferences(context);
+ }
+}
diff --git a/app/src/main/res/xml/pref_alerts.xml b/app/src/main/res/xml/pref_alerts.xml
index 5212863..25367a0 100644
--- a/app/src/main/res/xml/pref_alerts.xml
+++ b/app/src/main/res/xml/pref_alerts.xml
@@ -11,6 +11,10 @@
android:title="@string/pref_cat_title_thresholds"
app:iconSpaceReserved="false">
+
{1}")
- public static Collection