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..239fa29c2cec 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,23 @@ 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" + private const val PERCENT = 100 + + /** + * Convenient factory to create [PowerManagementServiceImpl] from [Context]. + */ @JvmStatic fun fromContext(context: Context): PowerManagementServiceImpl { val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager @@ -27,32 +38,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 * PERCENT) / 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) + 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) } }