Skip to content
Open
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
75 changes: 53 additions & 22 deletions app/src/main/java/com/sameerasw/essentials/utils/BatteryInfoUtil.kt
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,17 @@ object BatteryInfoUtil {
val present = intent?.getBooleanExtra(BatteryManager.EXTRA_PRESENT, true) ?: true

// Android 14+ public Extras
val cycleCount = if (android.os.Build.VERSION.SDK_INT >= 34) {
intent?.getIntExtra("android.os.extra.CYCLE_COUNT", -1)?.takeIf { it >= 0 }
var cycleCount = if (android.os.Build.VERSION.SDK_INT >= 34) {
intent?.getIntExtra("android.os.extra.CYCLE_COUNT", -1)?.takeIf { it > 0 }
} else null

// Vendor-specific extras fallback for basic details
if (cycleCount == null) {
val vendorCycle = intent?.getIntExtra("cycle_count", -1)?.takeIf { it > 0 }
?: intent?.getIntExtra("battery_cycle", -1)?.takeIf { it > 0 }
if (vendorCycle != null) cycleCount = vendorCycle
}

val chargingStatusNew = if (android.os.Build.VERSION.SDK_INT >= 34) {
intent?.getIntExtra("android.os.extra.CHARGING_STATUS", -1)?.takeIf { it >= 0 }
} else null
Expand All @@ -93,7 +100,9 @@ object BatteryInfoUtil {
val rawEnergy = bm?.getLongProperty(BatteryManager.BATTERY_PROPERTY_ENERGY_COUNTER)?.takeIf { it != Long.MIN_VALUE }
val remainingEnergyMwh = rawEnergy?.let { it / 1_000_000 } // nWh to mWh

val chargeTimeRemaining = bm?.computeChargeTimeRemaining()?.takeIf { it >= 0 }
val chargeTimeRemaining = if (android.os.Build.VERSION.SDK_INT >= 28) {
bm?.computeChargeTimeRemaining()?.takeIf { it >= 0 }
} else null

val hasStatsPerm = hasBatteryStatsPermission(context)

Expand Down Expand Up @@ -160,30 +169,45 @@ object BatteryInfoUtil {
val dumpsysOutput = ShellUtils.runCommandWithOutput(context, "dumpsys battery")
val dumpsysMap = parseDumpsysBattery(dumpsysOutput)

// SAMSUNG SPECIFIC: mSavedBattery... fields
val samsungCycleCount = (dumpsysMap["mSavedBatteryUsage"]?.cleanNumericValue()?.toIntOrNull()
?: dumpsysMap["mUsage"]?.cleanNumericValue()?.toIntOrNull())?.let {
if (it > 10000) it / 100 else it // Handle cumulative percentage vs cycles
} ?: dumpsysMap["mSavedBatteryCycle"]?.cleanNumericValue()?.toIntOrNull()
?: dumpsysMap["mCycle"]?.cleanNumericValue()?.toIntOrNull()
?: dumpsysMap["CycleCount"]?.cleanNumericValue()?.toIntOrNull()
?: readSysfsLong(context, "/sys/class/power_supply/battery/cycle_count")?.toInt()
?: readSysfsLong(context, "/efs/FactoryApp/batt_discharge_level")?.toInt()?.let { it / 100 }
?: readSysfsLong(context, "/sys/class/power_supply/battery/battery_cycle")?.toInt()

val samsungSoH = dumpsysMap["mSavedBatteryAsoc"]?.cleanNumericValue()?.toIntOrNull()
?: dumpsysMap["mAsoc"]?.cleanNumericValue()?.toIntOrNull()
?: readSysfsLong(context, "/sys/class/power_supply/battery/fg_asoc")?.toInt()

if (chargeFull == null) {
dumpsysMap["Charge counter"]?.toLongOrNull()?.let {
// dumpsys sometimes gives charge counter or max capacity
}
dumpsysMap["mSavedBatteryMax"]?.cleanNumericValue()?.toLongOrNull()?.let { chargeFull = it * 1000 }
}

val chargeCounter = dumpsysMap["Charge counter"]?.toLongOrNull() ?: readSysfsLong(context, "/sys/class/power_supply/battery/charge_counter")
val maxChargingCurrent = dumpsysMap["Max charging current"]?.toIntOrNull()
val maxChargingVoltage = dumpsysMap["Max charging voltage"]?.toIntOrNull()
val chargingState = dumpsysMap["Charging state"]?.toIntOrNull()
val chargingPolicy = dumpsysMap["Charging policy"]?.toIntOrNull()
val capacityLevel = dumpsysMap["Capacity level"]?.toIntOrNull()
val chargeCounter = dumpsysMap["Charge counter"]?.cleanNumericValue()?.toLongOrNull()
?: dumpsysMap["mSavedBattery"]?.cleanNumericValue()?.toLongOrNull()?.let { it * 1000 }
?: readSysfsLong(context, "/sys/class/power_supply/battery/charge_counter")

val dumpsysSerial = dumpsysMap["Serial number"] ?: dumpsysMap["serial_number"] ?: dumpsysMap["Serial Number"]
val dumpsysPart = dumpsysMap["Part status"]?.toIntOrNull() ?: dumpsysMap["part_status"]?.toIntOrNull() ?: dumpsysMap["Part Status"]?.toIntOrNull()
val maxChargingCurrent = dumpsysMap["Max charging current"]?.cleanNumericValue()?.toIntOrNull()
val maxChargingVoltage = dumpsysMap["Max charging voltage"]?.cleanNumericValue()?.toIntOrNull()
val chargingState = dumpsysMap["Charging state"]?.cleanNumericValue()?.toIntOrNull()
val chargingPolicy = dumpsysMap["Charging policy"]?.cleanNumericValue()?.toIntOrNull()
val capacityLevel = dumpsysMap["Capacity level"]?.cleanNumericValue()?.toIntOrNull()

val samsungSerial = dumpsysMap["serial_number"] ?: dumpsysMap["mSerialNumber"] ?: dumpsysMap["Serial number"]
val samsungPartStatus = dumpsysMap["mSavedBatteryEfuse"]?.cleanNumericValue()?.toIntOrNull()?.let { if (it == 0) 1 else 2 }
?: dumpsysMap["Part status"]?.cleanNumericValue()?.toIntOrNull()

val powerProfileOutput = ShellUtils.runCommandWithOutput(context, "dumpsys batterystats --power-profile")
val powerProfileMap = parsePowerProfile(powerProfileOutput)

val settingsOutput = ShellUtils.runCommandWithOutput(context, "dumpsys batterystats --settings")
val enforceLevel = parseSettingsEnforceLevel(settingsOutput)

val thermalInfo = ThermalUtil.getThermalInfo(context)

return basic.copy(
chargeFull = chargeFull,
chargeFullDesign = chargeFullDesign,
Expand All @@ -197,9 +221,11 @@ object BatteryInfoUtil {
voltageNow = voltageNow,
powerProfile = powerProfileMap.takeIf { it.isNotEmpty() },
batteryChargingEnforceLevel = enforceLevel,
serialNumber = basic.serialNumber ?: dumpsysSerial,
partStatus = basic.partStatus ?: dumpsysPart,
thermalInfo = thermalInfo
cycleCount = samsungCycleCount?.takeIf { it > 0 } ?: basic.cycleCount,
stateOfHealth = samsungSoH?.takeIf { it > 0 } ?: basic.stateOfHealth,
serialNumber = samsungSerial ?: basic.serialNumber,
partStatus = samsungPartStatus ?: basic.partStatus,
thermalInfo = ThermalUtil.getThermalInfo(context)
)
}

Expand Down Expand Up @@ -231,16 +257,21 @@ object BatteryInfoUtil {

private fun readSysfsLong(context: Context, path: String): Long? {
val out = ShellUtils.runCommandWithOutput(context, "cat $path") ?: return null
return out.trim().toLongOrNull()
return out.trim().cleanNumericValue().toLongOrNull()
}

private fun String.cleanNumericValue(): String {
return this.filter { it.isDigit() || it == '-' }
}

private fun parseDumpsysBattery(output: String?): Map<String, String> {
if (output.isNullOrBlank()) return emptyMap()
val map = mutableMapOf<String, String>()
output.lines().forEach { line ->
val trimmed = line.trim()
if (trimmed.contains(":")) {
val parts = trimmed.split(":", limit = 2)
val delimiter = if (trimmed.contains(":")) ":" else if (trimmed.contains("=")) "=" else null
if (delimiter != null) {
val parts = trimmed.split(delimiter, limit = 2)
if (parts.size == 2) {
map[parts[0].trim()] = parts[1].trim()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ object DeviceUtils {
Build.PRODUCT.contains("pixel", ignoreCase = true)
}

fun isSamsungDevice(): Boolean {
return Build.MANUFACTURER.equalsIgnoreCase("samsung") ||
Build.BRAND.equalsIgnoreCase("samsung")
}

fun getDeviceInfo(context: Context): DeviceInfo {
val deviceName = try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
Expand Down