Skip to content

feat: hide battery icon - #33

Merged
SoClear merged 6 commits into
SoClear:mainfrom
reddxae:patch-2
May 24, 2026
Merged

feat: hide battery icon#33
SoClear merged 6 commits into
SoClear:mainfrom
reddxae:patch-2

Conversation

@reddxae

@reddxae reddxae commented May 19, 2026

Copy link
Copy Markdown
Contributor

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.

preview

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +52 to +63
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())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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())

Comment on lines +286 to +300
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
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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
    }

Comment on lines +515 to +528
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
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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
    }

@reddxae
reddxae marked this pull request as draft May 19, 2026 10:41
@reddxae
reddxae marked this pull request as ready for review May 19, 2026 10:55
@SoClear

SoClear commented May 19, 2026

Copy link
Copy Markdown
Owner

“Set battery icon scale factor” works on your One UI 6.1? I do it for >= one ui 7

@SoClear

SoClear commented May 19, 2026

Copy link
Copy Markdown
Owner

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 ?
(by the way, It is incredibly ironic to reverse engineer a Samsung phone using Xiaomi's AI model.)

@reddxae

reddxae commented May 19, 2026

Copy link
Copy Markdown
Contributor Author

“Set battery icon scale factor” works 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.

HideBatteryIconHook - does it work on your One UI 6.1 ?

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.

@SoClear

SoClear commented May 20, 2026

Copy link
Copy Markdown
Owner

Good news: it works on oneui8
Bad news: turning on “Set battery icon scale factor” with it cause charging animation abnormal
My suggestion: just remove HideBatteryIconHook.restoreBatteryIconLayout(mBatteryIconView) and HideBatteryIconHook.forgetBatteryIconLayout(mBatteryIconView) in SystemUI.kt

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>>();

@reddxae

reddxae commented May 20, 2026

Copy link
Copy Markdown
Contributor Author

Let's see if things have changed.

@SoClear
SoClear merged commit 316ca25 into SoClear:main May 24, 2026
@reddxae
reddxae deleted the patch-2 branch May 26, 2026 23:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants