From 93ce4f7d9d6cd2c9d992d152bd8c55b7dc23b6d7 Mon Sep 17 00:00:00 2001 From: Josh Date: Thu, 5 Feb 2026 07:11:56 -0500 Subject: [PATCH 1/3] fix(device): improve battery charging status detection and robustness Signed-off-by: Josh --- .../device/PowerManagementServiceImpl.kt | 84 ++++++++++++++----- 1 file changed, 62 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/device/PowerManagementServiceImpl.kt b/app/src/main/java/com/nextcloud/client/device/PowerManagementServiceImpl.kt index 3c8d56c280de..61fa99c599f4 100644 --- a/app/src/main/java/com/nextcloud/client/device/PowerManagementServiceImpl.kt +++ b/app/src/main/java/com/nextcloud/client/device/PowerManagementServiceImpl.kt @@ -2,6 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2020 Chris Narkiewicz + * SPDX-FileCopyrightText: 2026 Josh Richards * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.device @@ -13,13 +14,22 @@ import android.os.BatteryManager import android.os.PowerManager import com.nextcloud.utils.extensions.registerBroadcastReceiver import com.owncloud.android.datamodel.ReceiverFlag +import com.owncloud.android.lib.common.utils.Log_OC +/** + * Implementation of [PowerManagementService] that reports device power-saving mode and battery status. + */ internal class PowerManagementServiceImpl( private val context: Context, private val platformPowerManager: PowerManager ) : PowerManagementService { companion object { + private const val TAG = "PowerManagementServiceImpl" + + /** + * Convenient factory to create [PowerManagementServiceImpl] from [Context]. + */ @JvmStatic fun fromContext(context: Context): PowerManagementServiceImpl { val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager @@ -27,32 +37,62 @@ internal class PowerManagementServiceImpl( } } + /** + * True if the device's power saving (battery saver) mode is currently active. + */ override val isPowerSavingEnabled: Boolean - get() { - return platformPowerManager.isPowerSaveMode - } + get() = platformPowerManager.isPowerSaveMode - @Suppress("MagicNumber") // 100% is 100, we're not doing Cobol + /** + * Returns the current [BatteryStatus], including charging status and a calculated charge + * percentage (0-100). + * + * Charging logic combines reported charging status and plugged-in state for robustness. + * Logs discrepancies between Android-reported plugged state and charging status for diagnostics. + * If battery information is unavailable, returns safe defaults (0% and not charging). + */ override val battery: BatteryStatus get() { - val intent: Intent? = context.registerBroadcastReceiver( - null, - IntentFilter(Intent.ACTION_BATTERY_CHANGED), - ReceiverFlag.NotExported + val intent = context.registerBroadcastReceiver( + receiver = null, + filter = IntentFilter(Intent.ACTION_BATTERY_CHANGED), + flag = ReceiverFlag.NotExported + ) ?: return BatteryStatus(isCharging = false, level = 0) + + // Defensive calculation of battery percentage: only computes if valid, else returns 0. + val level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) + val scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1) + val chargePercent = if (level >= 0 && scale > 0) { + ((level * 100) / scale.toFloat()).toInt() + } else { + Log_OC.w(TAG, "Invalid battery info: level=$level, scale=$scale") + 0 // Unavailable data + } + + // Robust charging determination: first use status, then plugged fallback for buggy/missing cases. + val status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1) + val plugged = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) + + val pluggedIn = plugged in setOf( + BatteryManager.BATTERY_PLUGGED_USB, + BatteryManager.BATTERY_PLUGGED_AC, + BatteryManager.BATTERY_PLUGGED_WIRELESS ) - val isCharging = intent?.let { - when (it.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0)) { - BatteryManager.BATTERY_PLUGGED_USB -> true - BatteryManager.BATTERY_PLUGGED_AC -> true - BatteryManager.BATTERY_PLUGGED_WIRELESS -> true - else -> false - } - } ?: false - val level = intent?.let { it -> - val level: Int = it.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) - val scale: Int = it.getIntExtra(BatteryManager.EXTRA_SCALE, -1) - (level * 100 / scale.toFloat()).toInt() - } ?: 0 - return BatteryStatus(isCharging, level) + val statusChargingOrFull = + status == BatteryManager.BATTERY_STATUS_CHARGING || + status == BatteryManager.BATTERY_STATUS_FULL + + val isCharging = when { + statusChargingOrFull -> true // Reliable for 99% of devices + status == -1 && pluggedIn -> true // Status missing but plugged in? Likely charging (fallback for weird devices) + else -> false + } + + // Log disagreements between plugged-in and charging status for device/ROM diagnostics. + if (pluggedIn != statusChargingOrFull) { + Log_OC.w(TAG, "BatteryManager disagrees: status=$status, plugged=$plugged") + } + + return BatteryStatus(isCharging, chargePercent) } } From dca79e3d8c8f6bf14e6c33627368081025b0e5d9 Mon Sep 17 00:00:00 2001 From: Josh Date: Thu, 5 Feb 2026 13:08:18 -0500 Subject: [PATCH 2/3] chore: fixup PowerManagementServiceImpl for lint Signed-off-by: Josh --- .../com/nextcloud/client/device/PowerManagementServiceImpl.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/nextcloud/client/device/PowerManagementServiceImpl.kt b/app/src/main/java/com/nextcloud/client/device/PowerManagementServiceImpl.kt index 61fa99c599f4..c5c146031ad1 100644 --- a/app/src/main/java/com/nextcloud/client/device/PowerManagementServiceImpl.kt +++ b/app/src/main/java/com/nextcloud/client/device/PowerManagementServiceImpl.kt @@ -84,7 +84,7 @@ internal class PowerManagementServiceImpl( val isCharging = when { statusChargingOrFull -> true // Reliable for 99% of devices - status == -1 && pluggedIn -> true // Status missing but plugged in? Likely charging (fallback for weird devices) + status == -1 && pluggedIn -> true // Status missing but plugged in? Likely charging (fallback) else -> false } From 07ece92271147a1cb60ab23d7c4489f09c91acde Mon Sep 17 00:00:00 2001 From: Josh Date: Thu, 5 Feb 2026 13:17:00 -0500 Subject: [PATCH 3/3] chore: eliminate magic number in PowerManagementServiceImpl Signed-off-by: Josh --- .../com/nextcloud/client/device/PowerManagementServiceImpl.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/nextcloud/client/device/PowerManagementServiceImpl.kt b/app/src/main/java/com/nextcloud/client/device/PowerManagementServiceImpl.kt index c5c146031ad1..239fa29c2cec 100644 --- a/app/src/main/java/com/nextcloud/client/device/PowerManagementServiceImpl.kt +++ b/app/src/main/java/com/nextcloud/client/device/PowerManagementServiceImpl.kt @@ -26,6 +26,7 @@ internal class PowerManagementServiceImpl( companion object { private const val TAG = "PowerManagementServiceImpl" + private const val PERCENT = 100 /** * Convenient factory to create [PowerManagementServiceImpl] from [Context]. @@ -63,7 +64,7 @@ internal class PowerManagementServiceImpl( val level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) val scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1) val chargePercent = if (level >= 0 && scale > 0) { - ((level * 100) / scale.toFloat()).toInt() + ((level * PERCENT) / scale.toFloat()).toInt() } else { Log_OC.w(TAG, "Invalid battery info: level=$level, scale=$scale") 0 // Unavailable data