feat: hide battery icon - #33
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a feature to hide the battery icon in the status bar while maintaining visibility of the charging icon when the device is plugged in. The implementation involves extensive hooking of the BatteryMeterView in SystemUI using Xposed, along with logic to manage layout visibility and state. Feedback focuses on optimizing performance by caching reflection fields and resource identifiers to minimize overhead during frequent UI updates.
| private val applyingBatteryIconViews: MutableSet<Any> = | ||
| Collections.synchronizedSet(Collections.newSetFromMap(WeakHashMap())) | ||
| private val appliedBatteryChargingIconIds: MutableMap<ImageView, Int> = | ||
| Collections.synchronizedMap(WeakHashMap()) | ||
| private val originalBatteryIconLayouts: MutableMap<ImageView, BatteryIconLayout> = | ||
| Collections.synchronizedMap(WeakHashMap()) | ||
| private val originalBatteryMeterPaddings: MutableMap<View, ViewPadding> = | ||
| Collections.synchronizedMap(WeakHashMap()) | ||
| private val originalBatteryMeterLayouts: MutableMap<View, BatteryIconLayout> = | ||
| Collections.synchronizedMap(WeakHashMap()) | ||
| private val originalBatteryPercentLayouts: MutableMap<TextView, BatteryIconLayout> = | ||
| Collections.synchronizedMap(WeakHashMap()) |
There was a problem hiding this comment.
The findField and setBatteryChargingIcon methods perform reflection and resource lookups frequently. To improve performance, especially since these are called on UI-heavy paths like battery updates, it is recommended to cache the Field objects and the resolved drawable IDs.
private var chargingIconId: Int? = null
private val fieldCache = Collections.synchronizedMap(mutableMapOf<String, Field>())
private val applyingBatteryIconViews: MutableSet<Any> =
Collections.synchronizedSet(Collections.newSetFromMap(WeakHashMap()))
private val appliedBatteryChargingIconIds: MutableMap<ImageView, Int> =
Collections.synchronizedMap(WeakHashMap())
private val originalBatteryIconLayouts: MutableMap<ImageView, BatteryIconLayout> =
Collections.synchronizedMap(WeakHashMap())
private val originalBatteryMeterPaddings: MutableMap<View, ViewPadding> =
Collections.synchronizedMap(WeakHashMap())
private val originalBatteryMeterLayouts: MutableMap<View, BatteryIconLayout> =
Collections.synchronizedMap(WeakHashMap())
private val originalBatteryPercentLayouts: MutableMap<TextView, BatteryIconLayout> =
Collections.synchronizedMap(WeakHashMap())| private fun setBatteryChargingIcon(iconView: ImageView) { | ||
| val resources = iconView.resources | ||
| val drawableId = resources.getIdentifier( | ||
| STATUS_BAR_CHARGING_ICON, | ||
| "drawable", | ||
| Package.SYSTEMUI | ||
| ).takeIf { it != 0 } ?: resources.getIdentifier( | ||
| FALLBACK_CHARGING_ICON, | ||
| "drawable", | ||
| Package.SYSTEMUI | ||
| ) | ||
| if (drawableId == 0 || appliedBatteryChargingIconIds[iconView] == drawableId) return | ||
| iconView.setImageResource(drawableId) | ||
| appliedBatteryChargingIconIds[iconView] = drawableId | ||
| } |
There was a problem hiding this comment.
Resource ID lookups via getIdentifier are relatively expensive. Caching the resolved ID for the charging icon will improve performance during battery state updates.
private fun setBatteryChargingIcon(iconView: ImageView) {
if (chargingIconId == null) {
val resources = iconView.resources
chargingIconId = resources.getIdentifier(
STATUS_BAR_CHARGING_ICON,
"drawable",
Package.SYSTEMUI
).takeIf { it != 0 } ?: resources.getIdentifier(
FALLBACK_CHARGING_ICON,
"drawable",
Package.SYSTEMUI
)
}
val drawableId = chargingIconId ?: 0
if (drawableId == 0 || appliedBatteryChargingIconIds[iconView] == drawableId) return
iconView.setImageResource(drawableId)
appliedBatteryChargingIconIds[iconView] = drawableId
}| private fun findField(instance: Any, names: List<String>): Field? { | ||
| var clazz: Class<*>? = instance.javaClass | ||
| while (clazz != null) { | ||
| names.forEach { name -> | ||
| runCatching { | ||
| val field = clazz.getDeclaredField(name) | ||
| field.isAccessible = true | ||
| return field | ||
| } | ||
| } | ||
| clazz = clazz.superclass | ||
| } | ||
| return null | ||
| } |
There was a problem hiding this comment.
The findField method performs reflection lookups every time it is called. Caching the Field objects in a map will significantly reduce the overhead of repeated field access across various hooks.
private fun findField(instance: Any, names: List<String>): Field? {
val clazz = instance.javaClass
val cacheKey = "${clazz.name}:${names.joinToString(",")}"
fieldCache[cacheKey]?.let { return it }
var currentClass: Class<*>? = clazz
while (currentClass != null) {
for (name in names) {
try {
val field = currentClass.getDeclaredField(name)
field.isAccessible = true
fieldCache[cacheKey] = field
return field
} catch (_: NoSuchFieldException) {
}
}
currentClass = currentClass.superclass
}
return null
}|
“Set battery icon scale factor” works on your One UI 6.1? I do it for >= one ui 7 |
|
I have a simplified version, 100% AI code , but works on my note20u oneui5.1: package io.github.soclear.oneuix.hook
import android.widget.ImageView
import de.robv.android.xposed.XC_MethodHook
import de.robv.android.xposed.XposedHelpers.findAndHookMethod
import de.robv.android.xposed.XposedHelpers.findClass
import de.robv.android.xposed.XposedHelpers.getBooleanField
import de.robv.android.xposed.XposedHelpers.getIntField
import de.robv.android.xposed.XposedHelpers.getObjectField
import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam
import io.github.soclear.oneuix.data.Package
import io.github.soclear.oneuix.hook.util.xlog
internal object HideBatteryIconHook {
private const val CHARGING_ICON = "stat_sys_battery_charging"
private const val FALLBACK_CHARGING_ICON = "ic_icon_charging"
private var chargingIconId = 0
fun apply(loadPackageParam: LoadPackageParam) {
if (loadPackageParam.packageName != Package.SYSTEMUI) return
val viewClass = findClass(
"com.android.systemui.battery.BatteryMeterView",
loadPackageParam.classLoader
)
val updateVisibility = object : XC_MethodHook() {
override fun afterHookedMethod(param: MethodHookParam) {
applyIconVisibility(param.thisObject)
}
}
try {
findAndHookMethod(viewClass, "scaleBatteryMeterViews", updateVisibility)
} catch (t: Throwable) {
xlog(t.stackTraceToString())
}
try {
findAndHookMethod(
viewClass,
"onBatteryLevelChanged",
Int::class.javaPrimitiveType,
Boolean::class.javaPrimitiveType,
updateVisibility
)
} catch (t: Throwable) {
xlog(t.stackTraceToString())
}
try {
findAndHookMethod(
viewClass,
"updateColors",
Int::class.javaPrimitiveType,
Int::class.javaPrimitiveType,
Int::class.javaPrimitiveType,
object : XC_MethodHook() {
override fun afterHookedMethod(param: MethodHookParam) {
val view = param.thisObject
if (!isCharging(view)) return
applyTint(view)
}
}
)
} catch (t: Throwable) {
xlog(t.stackTraceToString())
}
}
private fun applyIconVisibility(batteryMeterView: Any) {
val iconView = getObjectField(batteryMeterView, "mBatteryIconView") as? ImageView ?: return
if (isCharging(batteryMeterView)) {
iconView.visibility = android.view.View.VISIBLE
ensureChargingIcon(iconView)
applyTint(batteryMeterView)
return
}
iconView.visibility = android.view.View.GONE
}
private fun isCharging(batteryMeterView: Any): Boolean {
if (getBooleanField(batteryMeterView, "mCharging")) return true
if (getBooleanField(batteryMeterView, "mIsDirectPowerMode")) return true
val samsungDrawable = getObjectField(batteryMeterView, "mSamsungDrawable") ?: return false
val batteryState = getObjectField(samsungDrawable, "batteryState") ?: return false
return getBooleanField(batteryState, "charging") ||
getBooleanField(batteryState, "isDirectPowerMode")
}
private fun ensureChargingIcon(iconView: ImageView) {
val id = resolveChargingIconId(iconView)
if (id == 0) return
iconView.setImageResource(id)
}
private fun resolveChargingIconId(iconView: ImageView): Int {
if (chargingIconId != 0) return chargingIconId
val res = iconView.resources
chargingIconId = res.getIdentifier(CHARGING_ICON, "drawable", Package.SYSTEMUI)
.takeIf { it != 0 }
?: res.getIdentifier(FALLBACK_CHARGING_ICON, "drawable", Package.SYSTEMUI)
return chargingIconId
}
private fun applyTint(batteryMeterView: Any) {
val iconView = getObjectField(batteryMeterView, "mBatteryIconView") as? ImageView ?: return
val samsungDrawable = getObjectField(batteryMeterView, "mSamsungDrawable") ?: return
val tint = getIntField(samsungDrawable, "iconTint")
iconView.setColorFilter(tint)
}
}does it work on your One UI 6.1 ? |
Nah, it doesn't work at all. Well, generally speaking, I don't really see the point of this setting in One UI 6.1, where the battery icon is the same size as the other icons and fits there perfectly.
Not really. It’s quite buggy—for example, to actually hide the battery icon, you have to switch off the percentage display once in the phone settings. A noticeable empty space remains to the right of the hidden icon, and suddenly it still appears in the QS header. The charging icon only appears when the percentage is hidden, and it doesn’t disappear after the power is unplugged. Codex with GPT 5.5 gave me pretty much the same sloppy result at first, so you're not alone with this. ;) However, I spent quite a bit of time writing relatively bug-free code with manual refinements, though, as you can guess, it was also AI-assisted. |
|
Good news: it works on oneui8 by the way, xposed will automatically cache for you: public final class XposedHelpers {
private XposedHelpers() {}
private static final HashMap<String, Field> fieldCache = new HashMap<String, Field>();
private static final HashMap<String, Method> methodCache = new HashMap<String, Method>();
private static final HashMap<String, Constructor<?>> constructorCache = new HashMap<String, Constructor<?>>();
private static final WeakHashMap<Object, HashMap<String, Object>> additionalFields = new WeakHashMap<Object, HashMap<String, Object>>();
private static final HashMap<String, ThreadLocal<AtomicInteger>> sMethodDepth = new HashMap<String, ThreadLocal<AtomicInteger>>(); |
|
Let's see if things have changed. |
Since the existing implementation in Good Lock is flawed—it leaves a blank space in the status bar, making the indents asymmetrical when the icon is hidden, and doesn't display the charging indicator—we're here. So, it does what it says correctly.
This PR adds an option to hide the status bar battery icon while still showing the native SystemUI bolt charging indicator when the device is plugged. Keeps the battery percentage visible if the user has percentage display enabled. Please note that I only tested this on One UI 6.1, therefore can't be really sure that it will work just as well in newer versions. This needs to be tested.