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
Expand Up @@ -29,11 +29,6 @@ public class PowerConnectionReceiver extends BroadcastReceiver {

private static final String TAG = PowerConnectionReceiver.class.getSimpleName();

/**
* Battery percentage threshold for healthy charging (charge when battery is low)
*/
private static final int HEALTHY_CHARGE_THRESHOLD = 20;

/**
* Delay before sampling the charging current, giving it time to stabilise after plug-in.
* Package-visible so tests can advance the main looper by exactly this amount.
Expand Down Expand Up @@ -103,18 +98,15 @@ public void onReceive(final Context context, final Intent intent) {
/**
* Handle charger connected event.
* <p>
* Records whether this is a "healthy" charge (started at low battery), determines wired vs
* wireless, and schedules the speed sample + notification for a short delay later.
* Determines wired vs wireless and schedules the speed sample + notification for a short delay
* later. (The old "healthy charge" flag — plugged in at low battery — was retired in #114:
* starting low isn't a virtue under modern 20-80% guidance, so the titles no longer flip on it.)
*
* @param context The application context
* @param pluggedState The type of charger plugged in
* @param percentage Current battery percentage
*/
private void handleChargerConnected(final Context context, final int pluggedState, final int percentage) {
// Determine if this is a "healthy" charge (starting at low battery level)
final boolean isHealthyCharge = percentage <= HEALTHY_CHARGE_THRESHOLD;
NotificationService.setIsHealthy(isHealthyCharge);

final boolean wireless = pluggedState == BatteryManager.BATTERY_PLUGGED_WIRELESS;
final Context appContext = context.getApplicationContext();

Expand All @@ -125,11 +117,10 @@ private void handleChargerConnected(final Context context, final int pluggedStat
return;
}
final ChargeSpeed speed = SystemService.getChargeSpeed(appContext);
NotificationService.notifyChargeConnected(appContext, speed, wireless, isHealthyCharge);
NotificationService.notifyChargeConnected(appContext, speed, wireless);
});

Log.i(TAG, String.format("Charger connected (Battery: %d%%, Wireless: %s, Healthy: %s)",
percentage, wireless, isHealthyCharge));
Log.i(TAG, String.format("Charger connected (Battery: %d%%, Wireless: %s)", percentage, wireless));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,26 @@ public static int getEffectiveCycleCount(final Context context) {
return base + getDebugChargeCycles(context);
}

/**
* Whether the effective cycle count comes from the OS ({@code EXTRA_CYCLE_COUNT}, Android 14+)
* rather than this app's own tracking.
* <p>
* The distinction matters for honesty (#114): the OS count covers the battery's whole life, while
* the app-tracked estimate only counts charge delivered since the app was installed — on an older
* phone it starts at zero and therefore understates real wear. The insights screen labels the
* health basis differently for the two sources.
*
* @param context Application context
*
* @return true when the OS reports a cycle count for this device
*/
public static boolean isCycleCountFromOs(final Context context) {
if (isNull(context)) {
return false;
}
return SystemService.getChargeCycleCount(context) > 0;
}

/**
* Gets the number of debug-injected charge cycles (0 in normal use). Tracked separately from real
* cycles so {@link #resetDebugData} can clear them without touching genuine tracking.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,6 @@ public final class NotificationService {
*/
private static WeakReference<Bitmap> cachedLauncherIcon;

// Thread-safe healthy charge state
private static volatile boolean isHealthyCharge;

private NotificationService() {
// Utility class - prevent instantiation
}
Expand Down Expand Up @@ -164,13 +161,12 @@ public static void sendNotification(final Context context, final int type) {
* <li>{@link #CHARGE_STYLE_NONE} — nothing at all.</li>
* </ul>
*
* @param context The application context
* @param speed The estimated charging speed (may be {@link ChargeSpeed#unknown()})
* @param wireless true when charging over a wireless charger, false when wired
* @param isHealthy true when charging started at a low battery level ("healthy" charge)
* @param context The application context
* @param speed The estimated charging speed (may be {@link ChargeSpeed#unknown()})
* @param wireless true when charging over a wireless charger, false when wired
*/
public static void notifyChargeConnected(final Context context, final ChargeSpeed speed,
final boolean wireless, final boolean isHealthy) {
final boolean wireless) {
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
final String style = resolveChargeStyle(prefs.getString(
context.getString(R.string._pref_key_charge_notification_style), CHARGE_STYLE_TOAST));
Expand All @@ -186,7 +182,7 @@ public static void notifyChargeConnected(final Context context, final ChargeSpee
return;
}

postChargeNotification(context, content, isHealthy);
postChargeNotification(context, content);
}

/**
Expand Down Expand Up @@ -277,27 +273,20 @@ private static void showChargeToast(final Context context, final String message)
* Plugging in during quiet hours shouldn't ding: shown on the silent channel outside the window
* instead of the audible full-battery channel (issue #111).
*
* @param context The application context
* @param content The charge message to display
* @param isHealthy true when charging started at a low battery level ("healthy" charge)
* @param context The application context
* @param content The charge message to display
*/
private static void postChargeNotification(final Context context, final String content, final boolean isHealthy) {
private static void postChargeNotification(final Context context, final String content) {
if (lacksNotificationPermission(context)) {
Log.w(TAG, "Missing POST_NOTIFICATIONS permission, notification not sent");
return;
}

createNotificationChannels(context);

final String title = isHealthy
? context.getString(R.string.notification_charge_started_title_healthy)
: context.getString(R.string.notification_charge_started_title_regular);

final String title = context.getString(R.string.notification_charge_started_title);
final String ticker = title.concat(", ").concat(content);

final int iconRes = isHealthy
? R.drawable.ic_stat_device_battery_charging_20
: R.drawable.ic_stat_device_battery_charging_50;
final int iconRes = R.drawable.ic_stat_device_battery_charging_50;

final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
final boolean withinWindow = isWithinNotificationWindow(context, prefs);
Expand Down Expand Up @@ -522,17 +511,6 @@ public static void updateOngoingNotification(final Context context, final Batter
}
}

/**
* Set healthy charge mode
* <p>
* Thread-safe setter for the healthy charge state flag.
*
* @param healthy true if charging in healthy mode
*/
public static void setIsHealthy(final boolean healthy) {
isHealthyCharge = healthy;
}

/**
* Shutdown the sound executor service
* <p>
Expand Down Expand Up @@ -1120,9 +1098,7 @@ private static class NotificationConfig {
this.iconRes = R.drawable.ic_stat_device_battery_charging_full;
this.alarmSound = prefs.getString(context.getString(R.string._pref_key_notifications_full_sound_ringtone), defaultSound);
this.ticker = context.getString(R.string.notification_full_level_ticker);
this.title = isHealthyCharge
? context.getString(R.string.notification_full_level_title_healthy)
: context.getString(R.string.notification_full_level_title_regular);
this.title = context.getString(R.string.notification_full_level_title);
this.content = context.getString(R.string.notification_full_level_content);
this.bigContent = context.getString(R.string.notification_full_level_content_big);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,15 @@ private void showResolvedHealth() {
healthStatusText.setText(BatteryHealthTracker.labelResId(grade));
healthStatusText.setTextColor(getHealthColor(grade));

// Tell the user whether the figure is measured (honest) or a cycle-based estimate
healthBasisText.setText(measured ? R.string.health_basis_measured : R.string.health_basis_estimated);
// Tell the user what the figure is based on. The cycle-based estimate distinguishes OS-reported
// cycles (whole battery life) from cycles this app tracked since install, which understate real
// wear on a phone older than the app (#114).
final int basisRes = measured
? R.string.health_basis_measured
: BatteryHealthTracker.isCycleCountFromOs(this)
? R.string.health_basis_estimated_os
: R.string.health_basis_estimated_tracked;
healthBasisText.setText(basisRes);

healthDescriptionText.setText(BatteryHealthTracker.describeHealthGrade(this, grade));
}
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/res/layout/activity_battery_insights.xml
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@
android:id="@+id/healthBasisText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/health_basis_estimated"
android:text="@string/health_basis_estimated_os"
android:textSize="12sp"
android:textColor="@color/battery_details_label_color"
android:layout_marginTop="4dp"/>
Expand Down
41 changes: 18 additions & 23 deletions app/src/main/res/values-ar/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,6 @@
<string name="app_name">منبه البطارية البسيط</string>
<string name="action_settings">الإعدادات</string>
<string name="title_activity_settings">الإعدادات</string>
<string name="charger_connected">الشاحن موصول</string>
<string name="charger_connected_ac">AC charger connected</string>
<string name="charger_connected_usb">USB charger connected</string>
<string name="charger_connected_wireless">Wireless charger connected</string>
<string name="charger_disconnected">Charger Disconnected</string>
<string name="charging">تشحن</string>
<string name="discharging">تستهلك</string>
<string name="not_charging">لا تشحن</string>
Expand Down Expand Up @@ -97,23 +92,21 @@
<string name="critical_ignore_quiet_hours_summary_on">البطارية المنخفضة جداً تنبّهك حتى أثناء ساعات الهدوء</string>
<string name="critical_ignore_quiet_hours_summary_off">التنبيهات الحرجة تبقى صامتة أثناء ساعات الهدوء</string>

<string name="notification_critical_ticker">مستوى حرج للبطارية أقل من %1$d%%</string>
<string name="notification_critical_title">وقت الشحن الصحي</string>
<string name="notification_critical_content">البطارية على وشك النفاذ (%1$d%%)</string>
<string name="notification_critical_content_big">البطارية في مستوى حرج وأقل من (%1$d%%)، حان الوقت لشحن الهاتف</string>
<string name="notification_warning_ticker">البطارية في منتصف الطريق، أقل من %1$d%%</string>
<string name="notification_warning_title">اقتصد في الاستخدام</string>
<string name="notification_warning_content">تحتاج فقط للانتباه للبطارية (%1$d%%)</string>
<string name="notification_warning_content_big">مستوى البطارية %1$d%%. لا تحتاج للشاحن بعد، لكن انتبه لاستهلاك البطارية</string>
<string name="notification_full_level_ticker">البطارية تم شحنها بالكامل، بإمكانك فصل الشاحن</string>
<string name="notification_full_level_title_healthy">الشحن الصحي اكتمل</string>
<string name="notification_full_level_title_regular">الشحن الاعتيادي اكتمل</string>
<string name="notification_full_level_content">استخدم هاتفك بحرية الآن</string>
<string name="notification_full_level_content_big">بإمكانك استخدام الهاتف الآن، ولا يضر إن أبقيت الشاحن، لا تخف لن تصاب البطارية بالتخمة 😋</string>
<!-- Alert copy reworked in #114: neutral titles and 20-80% guidance -->
<string name="notification_critical_ticker">البطارية منخفضة جداً — أقل من %1$d%%</string>
<string name="notification_critical_title">البطارية منخفضة جداً</string>
<string name="notification_critical_content">البطارية على وشك النفاد (%1$d%%)</string>
<string name="notification_critical_content_big">مستوى البطارية أقل من المستوى الحرج (%1$d%%) — حان وقت الشحن.</string>
<string name="notification_warning_ticker">البطارية أقل من %1$d%%</string>
<string name="notification_warning_title">البطارية بدأت تنخفض</string>
<string name="notification_warning_content">البطارية عند %1$d%%انتبه لاستهلاكك</string>
<string name="notification_warning_content_big">مستوى البطارية %1$d%%. لا حاجة للشحن الآن — لكن حين يتيسّر لك، الشحن قبل أن تنخفض البطارية كثيراً أرفق بها.</string>
<string name="notification_full_level_ticker">اكتمل شحن البطارية — يمكنك فصل الشاحن الآن</string>
<string name="notification_full_level_title">اكتمل شحن البطارية</string>
<string name="notification_full_level_content">يمكنك فصل الشاحن الآن</string>
<string name="notification_full_level_content_big">اكتمل الشحن — يمكنك فصل الشاحن الآن. تتقادم البطاريات أسرع ما يكون عند إبقائها على 100%، لذا فصل الشاحن قريباً (والبقاء تقريباً بين 20% و80% في الاستخدام اليومي) يساعد بطاريتك على أن تدوم أطول.</string>

<string name="notification_charge_started_title_healthy">شحن صحي بدأ</string>
<string name="notification_charge_started_title_regular">شحن اعتيادي بدأ</string>
<string name="notification_charge_started_content">البطارية تشحن بواسطة %1$s</string>
<string name="notification_charge_started_title">بدأ الشحن</string>

<!-- Charge-connected message (issue #122): charging speed + wired/wireless -->
<string name="charge_source_wired">سلكي</string>
Expand Down Expand Up @@ -203,13 +196,15 @@
<string name="clear">مسح</string>
<string name="error_design_capacity_range">أدخل سعة بين %1$d و %2$d mAh</string>
<string name="health_basis_measured">مقاسة من السعة المقدّرة</string>
<string name="health_basis_estimated">مقدّرة من دورات الشحن</string>
<!-- #114: cycle-based estimate labelled by its source -->
<string name="health_basis_estimated_os">مقدّرة من دورات الشحن (كما يبلغ عنها أندرويد)</string>
<string name="health_basis_estimated_tracked">مقدّرة من الدورات المتتبَّعة منذ التثبيت — التآكل الفعلي قد يكون أعلى</string>
<!-- #94 unreliable battery-reading copy (drafted by Claude — pending maintainer review) -->
<string name="battery_reading_unreliable_cd">لماذا قد تكون قراءة البطارية غير موثوقة</string>
<string name="battery_reading_unreliable_dialog_title">قد تكون قراءة البطارية غير موثوقة</string>
<string name="battery_reading_unreliable_dialog_message" formatted="false">يُبلّغ هذا الجهاز عن سعة شحن كاملة لا تُطابق السعة المقدّرة التي أدخلتها، لذا لا يمكن الوثوق بالسعة المقاسة — ولا بنسبة الصحة المبنية عليها.\n\nغالباً ما يكون هذا خللاً في عتاد بطارية الهاتف (إذ تُبلّغ بعض المعالجات عن عدّاد الشحن بصورة غير صحيحة)، وليس مشكلة في بطاريتك.\n\nلكن قد يعني ذلك أيضاً أنّ البطارية بدأت تتلف فعلاً. انتبه لهذه العلامات:\n\n• ينطفئ الهاتف أو يُعيد التشغيل تحت الحِمل الثقيل رغم وجود شحن كافٍ\n• ينخفض المستوى فجأة قرب النهاية، فيسلك مستوى 20% وكأنه 2%\n• ينفد الشحن أسرع بكثير من ذي قبل\n\nإذا لاحظت هذه العلامات، ففكّر في استبدال البطارية.</string>
<string name="how_it_works">كيف يعمل</string>
<string name="how_battery_health_works" formatted="false" tools:ignore="TypographyDashes">تُقدَّر صحة البطارية بناءً على دورات الشحن. تُحتسب دورة شحن عندما تُشحن بطاريتك من 20% أو أقل إلى 95% أو أكثر. يبدأ هذا التتبع عند أول استخدام لهذا التطبيق.\n\nتقديرات الصحة:\n• 0–300 دورة: ممتازة (95–100%)\n• 300-500 دورة: جيدة (85-95%)\n• 500-800 دورة: مقبولة (70-85%)\n• 800+ دورة: ضعيفة (&lt;70%)</string>
<string name="how_battery_health_works" formatted="false" tools:ignore="TypographyDashes">تُعرض صحة البطارية من أفضل أساس متاح:\n\n• مقاسة — عند إدخال السعة المقدّرة لبطاريتك، تكون الصحة هي السعة الكاملة المقاسة مقسومة على السعة المقدّرة. الأساس الأدق.\n• دورات الشحن (كما يبلغ عنها أندرويد) — بعض الأجهزة تبلغ عن عدد دورات البطارية طوال عمرها؛ عندها تُقدَّر الصحة من منحنى تآكل نموذجي لبطاريات الليثيوم.\n• دورات الشحن (المتتبَّعة بواسطة هذا التطبيق) — خلاف ذلك تُحتسب الدورات من الشحن الموصول منذ تثبيت التطبيق (100 نقطة مئوية = دورة واحدة)، لذا على هاتف أقدم من التطبيق تُقلِّل من التآكل الفعلي.\n\nتستخدم التقديرات المبنية على الدورات منحنى عاماً — العمر الفعلي للدورات يختلف حسب المصنّع والخلية، سواء كانت التسمية Li-ion أو Li-Po:\n• 0–300 دورة: ممتازة (95–100%)\n• 300–500 دورة: جيدة (85–95%)\n• 500–800 دورة: مقبولة (70–85%)\n• 800+ دورة: ضعيفة (&lt;70%)\n\nنصيحة: تدوم بطاريات الليثيوم أطول عند إبقائها تقريباً بين 20% و80% وبعيداً عن الحرارة — الشحن المعتاد إلى 100% والتفريغ العميق كلاهما يزيد التآكل.</string>
<string name="developed_by">طوّره</string>
<!-- Proper name — transliteration; maintainer to confirm or keep Latin -->
<string name="developer_name">المظفّر الحسن</string>
Expand Down
Loading