Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -356,15 +355,11 @@ public void onStartTrackingTouch(@NonNull final RangeSlider slider) {
@Override
public void onStopTrackingTouch(@NonNull final RangeSlider slider) {
final List<Float> 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());
}
});

Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

/**
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) {
Expand Down Expand Up @@ -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() {
Expand All @@ -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));
}
});
}
Expand All @@ -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();
}
}
Expand All @@ -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<Float> 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) {
Expand Down
Loading