From 18175853528a2c963d97cd89b405a998333886c9 Mon Sep 17 00:00:00 2001 From: Ivan Date: Sun, 17 May 2026 14:29:26 +0300 Subject: [PATCH 1/6] feat: hide battery icon --- .../github/soclear/oneuix/data/Preference.kt | 1 + .../io/github/soclear/oneuix/hook/Main.kt | 4 + .../io/github/soclear/oneuix/hook/SystemUI.kt | 307 ++++++++++++++++++ .../oneuix/ui/category/DetailPaneSystemUI.kt | 22 ++ app/src/main/res/values-fr/strings.xml | 2 + app/src/main/res/values-ru/strings.xml | 2 + app/src/main/res/values-zh/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + 8 files changed, 342 insertions(+) diff --git a/app/src/main/java/io/github/soclear/oneuix/data/Preference.kt b/app/src/main/java/io/github/soclear/oneuix/data/Preference.kt index 3ab8bea..e6cb121 100644 --- a/app/src/main/java/io/github/soclear/oneuix/data/Preference.kt +++ b/app/src/main/java/io/github/soclear/oneuix/data/Preference.kt @@ -39,6 +39,7 @@ data class Preference( val setBatteryIconHeightScale: Boolean = false, val batteryIconHeightScale: Float = 1f, val hideBatteryPercentageSign: Boolean = false, + val hideBatteryIcon: Boolean = false, val supportRealTimeNetworkSpeed: Boolean = true, val showSeparateUpDownNetworkSpeeds: Boolean = false, val setStatusBarClockFormat: Boolean = false, diff --git a/app/src/main/java/io/github/soclear/oneuix/hook/Main.kt b/app/src/main/java/io/github/soclear/oneuix/hook/Main.kt index 64bbda8..e25b78c 100644 --- a/app/src/main/java/io/github/soclear/oneuix/hook/Main.kt +++ b/app/src/main/java/io/github/soclear/oneuix/hook/Main.kt @@ -217,6 +217,10 @@ class Main : IXposedHookLoadPackage, IXposedHookInitPackageResources { SystemUI.setBatteryIconScale(lpparam, widthScale, heightScale) } + if (preference.systemUI.statusBar.hideBatteryIcon) { + SystemUI.hideBatteryIcon(lpparam) + } + if (preference.systemUI.statusBar.supportRealTimeNetworkSpeed) { Network.supportRealTimeNetworkSpeed(lpparam) } diff --git a/app/src/main/java/io/github/soclear/oneuix/hook/SystemUI.kt b/app/src/main/java/io/github/soclear/oneuix/hook/SystemUI.kt index 98bae34..7250169 100644 --- a/app/src/main/java/io/github/soclear/oneuix/hook/SystemUI.kt +++ b/app/src/main/java/io/github/soclear/oneuix/hook/SystemUI.kt @@ -23,6 +23,7 @@ import de.robv.android.xposed.XC_MethodHook import de.robv.android.xposed.XC_MethodReplacement import de.robv.android.xposed.XC_MethodReplacement.returnConstant import de.robv.android.xposed.XposedBridge +import de.robv.android.xposed.XposedBridge.hookAllConstructors import de.robv.android.xposed.XposedBridge.hookAllMethods import de.robv.android.xposed.XposedHelpers.callMethod import de.robv.android.xposed.XposedHelpers.findAndHookConstructor @@ -36,12 +37,26 @@ import de.robv.android.xposed.callbacks.XC_InitPackageResources.InitPackageResou import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam import io.github.soclear.oneuix.data.Package import io.github.soclear.oneuix.hook.util.TraditionalChineseCalendar +import java.lang.reflect.Field import java.time.LocalDateTime import java.time.format.DateTimeFormatter +import java.util.Collections +import java.util.WeakHashMap import kotlin.math.roundToInt object SystemUI { + private val applyingBatteryIconViews: MutableSet = + Collections.synchronizedSet(Collections.newSetFromMap(WeakHashMap())) + private val originalBatteryIconLayouts: MutableMap = + Collections.synchronizedMap(WeakHashMap()) + private val originalBatteryMeterPaddings: MutableMap = + Collections.synchronizedMap(WeakHashMap()) + private val originalBatteryMeterLayouts: MutableMap = + Collections.synchronizedMap(WeakHashMap()) + private val originalBatteryPercentLayouts: MutableMap = + Collections.synchronizedMap(WeakHashMap()) + enum class QsBar { MediaPlayer, NearbyDevicesAndDeviceControl, @@ -101,6 +116,7 @@ object SystemUI { object : XC_MethodHook() { override fun afterHookedMethod(param: MethodHookParam) { val mBatteryIconView = getObjectField(param.thisObject, "mBatteryIconView") as ImageView + restoreBatteryIconLayout(mBatteryIconView) mBatteryIconView.layoutParams = mBatteryIconView.layoutParams.apply { if (widthScale != null) { width = (width * widthScale).roundToInt() @@ -109,6 +125,7 @@ object SystemUI { height = (height * heightScale).roundToInt() } } + forgetBatteryIconLayout(mBatteryIconView) } } ) @@ -117,6 +134,296 @@ object SystemUI { } } + fun hideBatteryIcon(loadPackageParam: LoadPackageParam) { + if (loadPackageParam.packageName != Package.SYSTEMUI) return + + val callback = object : XC_MethodHook() { + override fun afterHookedMethod(param: MethodHookParam) { + applyBatteryIconVisibility(param.thisObject) + } + } + + try { + hookAllConstructors( + findClass( + "com.android.systemui.battery.BatteryMeterView", + loadPackageParam.classLoader + ), + callback + ) + } catch (t: Throwable) { + XposedBridge.log(t) + } + + listOf( + "onFinishInflate", + "onAttachedToWindow", + "onBatteryLevelChanged", + "updatePercentText", + "updateShowPercent", + "scaleBatteryMeterViewsLegacy" + ).forEach { methodName -> + try { + hookAllMethods( + findClass( + "com.android.systemui.battery.BatteryMeterView", + loadPackageParam.classLoader + ), + methodName, + callback + ) + } catch (_: Throwable) { + } + } + } + + private fun applyBatteryIconVisibility(batteryMeterView: Any) { + if (!applyingBatteryIconViews.add(batteryMeterView)) return + try { + val view = batteryMeterView as? View ?: return + val iconView = readFieldValue( + batteryMeterView, + listOf("mBatteryIconView", "batteryIconView") + ) as? ImageView ?: return + val percentView = readFieldValue( + batteryMeterView, + listOf("mBatteryPercentView", "batteryPercentView") + ) as? TextView + + collapseBatteryIconLayout(iconView) + iconView.visibility = View.GONE + view.visibility = if (percentView?.isVisibleWithText() == true) { + collapseBatteryMeterEndPadding(view) + collapseBatteryMeterEndSpacing(view) + collapseBatteryPercentEndSpacing(percentView) + View.VISIBLE + } else { + View.GONE + } + } finally { + applyingBatteryIconViews.remove(batteryMeterView) + } + } + + private fun collapseBatteryIconLayout(iconView: ImageView) { + rememberBatteryIconLayout(iconView) + val params = iconView.layoutParams ?: return + params.width = 0 + params.height = 0 + (params as? ViewGroup.MarginLayoutParams)?.setMargins(0, 0, 0, 0) + iconView.layoutParams = params + } + + private fun restoreBatteryIconLayout(iconView: ImageView) { + val original = originalBatteryIconLayouts[iconView] ?: return + val params = iconView.layoutParams ?: return + params.width = original.width + params.height = original.height + (params as? ViewGroup.MarginLayoutParams)?.setMargins( + original.leftMargin, + original.topMargin, + original.rightMargin, + original.bottomMargin + ) + iconView.layoutParams = params + iconView.setPaddingRelative( + original.paddingStart, + original.paddingTop, + original.paddingEnd, + original.paddingBottom + ) + } + + private fun collapseBatteryMeterEndPadding(view: View) { + rememberBatteryMeterPadding(view) + view.setPaddingRelative( + view.paddingStart, + view.paddingTop, + 0, + view.paddingBottom + ) + } + + private fun restoreBatteryMeterPadding(view: View) { + val original = originalBatteryMeterPaddings[view] ?: return + view.setPaddingRelative( + original.start, + original.top, + original.end, + original.bottom + ) + } + + private fun rememberBatteryMeterPadding(view: View) { + if (originalBatteryMeterPaddings.containsKey(view)) return + originalBatteryMeterPaddings[view] = ViewPadding( + start = view.paddingStart, + top = view.paddingTop, + end = view.paddingEnd, + bottom = view.paddingBottom + ) + } + + private fun collapseBatteryMeterEndSpacing(view: View) { + rememberBatteryMeterLayout(view) + val params = view.layoutParams + (params as? ViewGroup.MarginLayoutParams)?.let { + it.marginEnd = 0 + it.rightMargin = 0 + view.layoutParams = it + } + } + + private fun restoreBatteryMeterLayout(view: View) { + val original = originalBatteryMeterLayouts[view] ?: return + val params = view.layoutParams ?: return + params.width = original.width + params.height = original.height + (params as? ViewGroup.MarginLayoutParams)?.setMargins( + original.leftMargin, + original.topMargin, + original.rightMargin, + original.bottomMargin + ) + view.layoutParams = params + } + + private fun rememberBatteryMeterLayout(view: View) { + if (originalBatteryMeterLayouts.containsKey(view)) return + val params = view.layoutParams ?: return + val marginParams = params as? ViewGroup.MarginLayoutParams + originalBatteryMeterLayouts[view] = BatteryIconLayout( + width = params.width, + height = params.height, + leftMargin = marginParams?.leftMargin ?: 0, + topMargin = marginParams?.topMargin ?: 0, + rightMargin = marginParams?.rightMargin ?: 0, + bottomMargin = marginParams?.bottomMargin ?: 0, + paddingStart = view.paddingStart, + paddingTop = view.paddingTop, + paddingEnd = view.paddingEnd, + paddingBottom = view.paddingBottom + ) + } + + private fun collapseBatteryPercentEndSpacing(percentView: TextView) { + rememberBatteryPercentLayout(percentView) + val params = percentView.layoutParams + (params as? ViewGroup.MarginLayoutParams)?.let { + it.marginEnd = 0 + it.rightMargin = 0 + percentView.layoutParams = it + } + percentView.setPaddingRelative( + percentView.paddingStart, + percentView.paddingTop, + 0, + percentView.paddingBottom + ) + } + + private fun restoreBatteryPercentLayout(percentView: TextView) { + val original = originalBatteryPercentLayouts[percentView] ?: return + val params = percentView.layoutParams ?: return + params.width = original.width + params.height = original.height + (params as? ViewGroup.MarginLayoutParams)?.setMargins( + original.leftMargin, + original.topMargin, + original.rightMargin, + original.bottomMargin + ) + percentView.layoutParams = params + percentView.setPaddingRelative( + original.paddingStart, + original.paddingTop, + original.paddingEnd, + original.paddingBottom + ) + } + + private fun rememberBatteryPercentLayout(percentView: TextView) { + if (originalBatteryPercentLayouts.containsKey(percentView)) return + val params = percentView.layoutParams ?: return + val marginParams = params as? ViewGroup.MarginLayoutParams + originalBatteryPercentLayouts[percentView] = BatteryIconLayout( + width = params.width, + height = params.height, + leftMargin = marginParams?.leftMargin ?: 0, + topMargin = marginParams?.topMargin ?: 0, + rightMargin = marginParams?.rightMargin ?: 0, + bottomMargin = marginParams?.bottomMargin ?: 0, + paddingStart = percentView.paddingStart, + paddingTop = percentView.paddingTop, + paddingEnd = percentView.paddingEnd, + paddingBottom = percentView.paddingBottom + ) + } + + private fun rememberBatteryIconLayout(iconView: ImageView) { + if (originalBatteryIconLayouts.containsKey(iconView)) return + val params = iconView.layoutParams ?: return + if (params.width == 0 && params.height == 0) return + val marginParams = params as? ViewGroup.MarginLayoutParams + originalBatteryIconLayouts[iconView] = BatteryIconLayout( + width = params.width, + height = params.height, + leftMargin = marginParams?.leftMargin ?: 0, + topMargin = marginParams?.topMargin ?: 0, + rightMargin = marginParams?.rightMargin ?: 0, + bottomMargin = marginParams?.bottomMargin ?: 0, + paddingStart = iconView.paddingStart, + paddingTop = iconView.paddingTop, + paddingEnd = iconView.paddingEnd, + paddingBottom = iconView.paddingBottom + ) + } + + private fun forgetBatteryIconLayout(iconView: ImageView) { + originalBatteryIconLayouts.remove(iconView) + } + + private fun TextView.isVisibleWithText(): Boolean = + visibility == View.VISIBLE && text.isNotBlank() + + private data class BatteryIconLayout( + val width: Int, + val height: Int, + val leftMargin: Int, + val topMargin: Int, + val rightMargin: Int, + val bottomMargin: Int, + val paddingStart: Int, + val paddingTop: Int, + val paddingEnd: Int, + val paddingBottom: Int + ) + + private data class ViewPadding( + val start: Int, + val top: Int, + val end: Int, + val bottom: Int + ) + + private fun readFieldValue(instance: Any, names: List): Any? = + findField(instance, names)?.let { field -> runCatching { field.get(instance) }.getOrNull() } + + private fun findField(instance: Any, names: List): 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 + } + fun hideBatteryPercentageSign(resparam: InitPackageResourcesParam) { if (resparam.packageName != Package.SYSTEMUI || Build.VERSION.SDK_INT > Build.VERSION_CODES.UPSIDE_DOWN_CAKE diff --git a/app/src/main/java/io/github/soclear/oneuix/ui/category/DetailPaneSystemUI.kt b/app/src/main/java/io/github/soclear/oneuix/ui/category/DetailPaneSystemUI.kt index 25045de..72c9066 100644 --- a/app/src/main/java/io/github/soclear/oneuix/ui/category/DetailPaneSystemUI.kt +++ b/app/src/main/java/io/github/soclear/oneuix/ui/category/DetailPaneSystemUI.kt @@ -222,6 +222,15 @@ fun DetailPaneSystemUI( } ) } + SwitchItem( + icon = ImageVector.vectorResource(id = R.drawable.battery), + title = stringResource(id = R.string.hideBatteryIcon_title), + summary = stringResource(id = R.string.hideBatteryIcon_summary), + checked = uiState.statusBar.hideBatteryIcon, + onCheckedChange = { + onEvent(SystemUIEvent.StatusBar.HideBatteryIcon(it)) + } + ) SwitchItem( icon = ImageVector.vectorResource(id = R.drawable.net_speed), title = stringResource(id = R.string.supportRealTimeNetworkSpeed_title), @@ -652,6 +661,9 @@ sealed interface SystemUIEvent { @JvmInline value class HideBatteryPercentageSign(val value: Boolean) : StatusBar + @JvmInline + value class HideBatteryIcon(val value: Boolean) : StatusBar + @JvmInline value class SupportRealTimeNetworkSpeed(val value: Boolean) : StatusBar @@ -862,6 +874,16 @@ private fun SettingViewModel.onStatusBarEvent(event: SystemUIEvent.StatusBar) { ) } + is SystemUIEvent.StatusBar.HideBatteryIcon -> { + preference.copy( + systemUI = preference.systemUI.copy( + statusBar = preference.systemUI.statusBar.copy( + hideBatteryIcon = event.value + ) + ) + ) + } + is SystemUIEvent.StatusBar.SupportRealTimeNetworkSpeed -> { preference.copy( systemUI = preference.systemUI.copy( diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index e33f549..6476686 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -14,6 +14,8 @@ Définir le facteur d\'échelle de hauteur de l\'icône de batterie Masquer le symbole de pourcentage de la batterie Supprimer uniquement le symbole %, conserver les chiffres + Masquer l\'icône de batterie + Masque uniquement l\'icône de batterie sans modifier le réglage système du pourcentage Afficher la vitesse du réseau en temps réel Paramètres -> Notifications -> Paramètres avancés -> Afficher la vitesse du réseau Afficher les vitesses d\'envoi/téléchargement (Barre d\'état) diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index b8454db..5b93578 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -14,6 +14,8 @@ Масштаб высоты иконки батареи Скрыть символ процента заряда батареи Удалить только символ %, оставив цифры + Скрыть иконку батареи + Скрывает только иконку батареи, не меняя системную настройку процента заряда Отображение скорости сети в реальном времени Настройки -> Уведомления -> Дополнительные параметры -> Скорость сети в реальном времени Раздельная скорость приема/передачи в строке состояния diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index cac69bd..9f9a940 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -14,6 +14,8 @@ 设置电池图标高度缩放倍数 隐藏状态栏电池的百分号 仅去掉百分号,保留数字 + 隐藏电池图标 + 仅隐藏电池图标,不更改系统电量百分比设置 支持显示实时网速 设置->通知->高级设置->显示实时网速 状态栏显示上传下载网速 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 672b0b6..3c1bda4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -14,6 +14,8 @@ Set battery icon height scale factor Hide battery percentage sign in status bar Remove the % sign only, keep the numbers + Hide battery icon + Hides only the battery icon without changing the system battery percentage setting Support displaying real-time network speed Settings -> Notifications -> Advanced settings -> Show real-time network speed Show separate upload/download speeds in status bar From c22a100331f75bfcc55ccd81661131d37c5eec86 Mon Sep 17 00:00:00 2001 From: Ivan Date: Tue, 19 May 2026 11:21:56 +0300 Subject: [PATCH 2/6] feat: show charging symbol when battery icon is hidden --- .../io/github/soclear/oneuix/hook/SystemUI.kt | 105 +++++++++++++++++- 1 file changed, 104 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/io/github/soclear/oneuix/hook/SystemUI.kt b/app/src/main/java/io/github/soclear/oneuix/hook/SystemUI.kt index 7250169..8ffea63 100644 --- a/app/src/main/java/io/github/soclear/oneuix/hook/SystemUI.kt +++ b/app/src/main/java/io/github/soclear/oneuix/hook/SystemUI.kt @@ -46,8 +46,13 @@ import kotlin.math.roundToInt object SystemUI { + private const val STATUS_BAR_CHARGING_ICON = "stat_sys_battery_charging" + private const val FALLBACK_CHARGING_ICON = "ic_icon_charging" + private val applyingBatteryIconViews: MutableSet = Collections.synchronizedSet(Collections.newSetFromMap(WeakHashMap())) + private val appliedBatteryChargingIconIds: MutableMap = + Collections.synchronizedMap(WeakHashMap()) private val originalBatteryIconLayouts: MutableMap = Collections.synchronizedMap(WeakHashMap()) private val originalBatteryMeterPaddings: MutableMap = @@ -175,9 +180,45 @@ object SystemUI { } catch (_: Throwable) { } } + + try { + hookAllMethods( + findClass( + "com.android.systemui.battery.BatteryMeterViewController\$3", + loadPackageParam.classLoader + ), + "onBatteryLevelChanged", + callback + ) + } catch (_: Throwable) { + } + + try { + findAndHookMethod( + "com.android.systemui.battery.BatteryMeterView", + loadPackageParam.classLoader, + "updateColors", + Int::class.javaPrimitiveType, + Int::class.javaPrimitiveType, + object : XC_MethodHook() { + override fun afterHookedMethod(param: MethodHookParam) { + val batteryMeterView = resolveBatteryMeterView(param.thisObject) ?: return + if (!isBatteryCharging(batteryMeterView)) return + val iconView = readFieldValue( + batteryMeterView, + listOf("mBatteryIconView", "batteryIconView") + ) as? ImageView ?: return + applyBatteryChargingIconTint(batteryMeterView, iconView) + } + } + ) + } catch (t: Throwable) { + XposedBridge.log(t) + } } - private fun applyBatteryIconVisibility(batteryMeterView: Any) { + private fun applyBatteryIconVisibility(instance: Any) { + val batteryMeterView = resolveBatteryMeterView(instance) ?: return if (!applyingBatteryIconViews.add(batteryMeterView)) return try { val view = batteryMeterView as? View ?: return @@ -190,6 +231,18 @@ object SystemUI { listOf("mBatteryPercentView", "batteryPercentView") ) as? TextView + if (isBatteryCharging(batteryMeterView)) { + restoreBatteryIconLayout(iconView) + restoreBatteryMeterPadding(view) + restoreBatteryMeterLayout(view) + percentView?.let(::restoreBatteryPercentLayout) + setBatteryChargingIcon(iconView) + applyBatteryChargingIconTint(batteryMeterView, iconView) + iconView.visibility = View.VISIBLE + view.visibility = View.VISIBLE + return + } + collapseBatteryIconLayout(iconView) iconView.visibility = View.GONE view.visibility = if (percentView?.isVisibleWithText() == true) { @@ -205,6 +258,56 @@ object SystemUI { } } + private fun resolveBatteryMeterView(instance: Any): Any? { + if (instance is View) return instance + val controller = readFieldValue(instance, listOf("this\$0")) ?: return null + return readFieldValue(controller, listOf("mView")) + } + + private fun isBatteryCharging(batteryMeterView: Any): Boolean { + val meterCharging = readFieldValue( + batteryMeterView, + listOf("mCharging", "charging") + ) as? Boolean == true + if (meterCharging) return true + + val samsungDrawable = readFieldValue( + batteryMeterView, + listOf("mSamsungDrawable", "samsungDrawable") + ) ?: return false + val batteryState = readFieldValue( + samsungDrawable, + listOf("batteryState", "mBatteryState") + ) ?: return false + return readFieldValue(batteryState, listOf("charging")) as? Boolean == true || + readFieldValue(batteryState, listOf("isDirectPowerMode")) as? Boolean == true + } + + 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 + } + + private fun applyBatteryChargingIconTint(batteryMeterView: Any, iconView: ImageView) { + val samsungDrawable = readFieldValue( + batteryMeterView, + listOf("mSamsungDrawable", "samsungDrawable") + ) ?: return + val iconTint = readFieldValue(samsungDrawable, listOf("iconTint")) as? Int ?: return + iconView.setColorFilter(iconTint) + } + private fun collapseBatteryIconLayout(iconView: ImageView) { rememberBatteryIconLayout(iconView) val params = iconView.layoutParams ?: return From a43fb9442b40f8f96c6926542ed83e4038046e2c Mon Sep 17 00:00:00 2001 From: Ivan Date: Tue, 19 May 2026 11:35:28 +0300 Subject: [PATCH 3/6] chore: reword `hide battery icon` feature description --- app/src/main/res/values-fr/strings.xml | 2 +- app/src/main/res/values-ru/strings.xml | 2 +- app/src/main/res/values-zh/strings.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 6476686..2692824 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -15,7 +15,7 @@ Masquer le symbole de pourcentage de la batterie Supprimer uniquement le symbole %, conserver les chiffres Masquer l\'icône de batterie - Masque uniquement l\'icône de batterie sans modifier le réglage système du pourcentage + Masque l\'icône de batterie tout en affichant l\'icône de charge lorsque l\'appareil est branché. N\'affecte pas le pourcentage de batterie Afficher la vitesse du réseau en temps réel Paramètres -> Notifications -> Paramètres avancés -> Afficher la vitesse du réseau Afficher les vitesses d\'envoi/téléchargement (Barre d\'état) diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 5b93578..1ac718e 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -15,7 +15,7 @@ Скрыть символ процента заряда батареи Удалить только символ %, оставив цифры Скрыть иконку батареи - Скрывает только иконку батареи, не меняя системную настройку процента заряда + Скрывает иконку батареи, показывая иконку зарядки при подключении питания. Не влияет на процент заряда батареи Отображение скорости сети в реальном времени Настройки -> Уведомления -> Дополнительные параметры -> Скорость сети в реальном времени Раздельная скорость приема/передачи в строке состояния diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 9f9a940..a0514d0 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -15,7 +15,7 @@ 隐藏状态栏电池的百分号 仅去掉百分号,保留数字 隐藏电池图标 - 仅隐藏电池图标,不更改系统电量百分比设置 + 隐藏电池图标,插入电源时显示充电图标。不影响电池百分比 支持显示实时网速 设置->通知->高级设置->显示实时网速 状态栏显示上传下载网速 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3c1bda4..f202c40 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -15,7 +15,7 @@ Hide battery percentage sign in status bar Remove the % sign only, keep the numbers Hide battery icon - Hides only the battery icon without changing the system battery percentage setting + Hides the battery icon while displaying the charging icon when plugged. Doesn\'t affect the battery percentage Support displaying real-time network speed Settings -> Notifications -> Advanced settings -> Show real-time network speed Show separate upload/download speeds in status bar From 34bf90c495f201a705012c0322660c0543828ff8 Mon Sep 17 00:00:00 2001 From: Ivan Date: Tue, 19 May 2026 12:38:15 +0300 Subject: [PATCH 4/6] perf: optimize `hide battery icon` feature --- .../io/github/soclear/oneuix/hook/SystemUI.kt | 44 +++++++++++++++---- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/io/github/soclear/oneuix/hook/SystemUI.kt b/app/src/main/java/io/github/soclear/oneuix/hook/SystemUI.kt index 8ffea63..b97c9f3 100644 --- a/app/src/main/java/io/github/soclear/oneuix/hook/SystemUI.kt +++ b/app/src/main/java/io/github/soclear/oneuix/hook/SystemUI.kt @@ -48,7 +48,11 @@ import kotlin.math.roundToInt object SystemUI { private const val STATUS_BAR_CHARGING_ICON = "stat_sys_battery_charging" private const val FALLBACK_CHARGING_ICON = "ic_icon_charging" + private const val UNRESOLVED_CHARGING_ICON_ID = -1 + private var batteryChargingIconId = UNRESOLVED_CHARGING_ICON_ID + private val fieldCache: MutableMap = + Collections.synchronizedMap(mutableMapOf()) private val applyingBatteryIconViews: MutableSet = Collections.synchronizedSet(Collections.newSetFromMap(WeakHashMap())) private val appliedBatteryChargingIconIds: MutableMap = @@ -284,8 +288,16 @@ object SystemUI { } private fun setBatteryChargingIcon(iconView: ImageView) { + val drawableId = resolveBatteryChargingIconId(iconView) + if (drawableId == 0 || appliedBatteryChargingIconIds[iconView] == drawableId) return + iconView.setImageResource(drawableId) + appliedBatteryChargingIconIds[iconView] = drawableId + } + + private fun resolveBatteryChargingIconId(iconView: ImageView): Int { + if (batteryChargingIconId != UNRESOLVED_CHARGING_ICON_ID) return batteryChargingIconId val resources = iconView.resources - val drawableId = resources.getIdentifier( + batteryChargingIconId = resources.getIdentifier( STATUS_BAR_CHARGING_ICON, "drawable", Package.SYSTEMUI @@ -294,9 +306,7 @@ object SystemUI { "drawable", Package.SYSTEMUI ) - if (drawableId == 0 || appliedBatteryChargingIconIds[iconView] == drawableId) return - iconView.setImageResource(drawableId) - appliedBatteryChargingIconIds[iconView] = drawableId + return batteryChargingIconId } private fun applyBatteryChargingIconTint(batteryMeterView: Any, iconView: ImageView) { @@ -509,6 +519,11 @@ object SystemUI { val bottom: Int ) + private data class FieldCacheKey( + val clazz: Class<*>, + val name: String + ) + private fun readFieldValue(instance: Any, names: List): Any? = findField(instance, names)?.let { field -> runCatching { field.get(instance) }.getOrNull() } @@ -516,17 +531,28 @@ object SystemUI { var clazz: Class<*>? = instance.javaClass while (clazz != null) { names.forEach { name -> - runCatching { - val field = clazz.getDeclaredField(name) - field.isAccessible = true - return field - } + findDeclaredField(clazz, name)?.let { return it } } clazz = clazz.superclass } return null } + private fun findDeclaredField(clazz: Class<*>, name: String): Field? { + val cacheKey = FieldCacheKey(clazz, name) + synchronized(fieldCache) { + if (fieldCache.containsKey(cacheKey)) return fieldCache[cacheKey] + } + + val field = runCatching { + clazz.getDeclaredField(name).apply { isAccessible = true } + }.getOrNull() + synchronized(fieldCache) { + fieldCache[cacheKey] = field + } + return field + } + fun hideBatteryPercentageSign(resparam: InitPackageResourcesParam) { if (resparam.packageName != Package.SYSTEMUI || Build.VERSION.SDK_INT > Build.VERSION_CODES.UPSIDE_DOWN_CAKE From e1f500fb22c3fa908fbc2c8a0d91eb8ac98da67c Mon Sep 17 00:00:00 2001 From: Ivan Date: Tue, 19 May 2026 13:40:52 +0300 Subject: [PATCH 5/6] refactor: isolate `hide battery icon` hook --- .../oneuix/hook/HideBatteryIconHook.kt | 449 ++++++++++++++++++ .../io/github/soclear/oneuix/hook/Main.kt | 2 +- .../io/github/soclear/oneuix/hook/SystemUI.kt | 438 +---------------- 3 files changed, 452 insertions(+), 437 deletions(-) create mode 100644 app/src/main/java/io/github/soclear/oneuix/hook/HideBatteryIconHook.kt diff --git a/app/src/main/java/io/github/soclear/oneuix/hook/HideBatteryIconHook.kt b/app/src/main/java/io/github/soclear/oneuix/hook/HideBatteryIconHook.kt new file mode 100644 index 0000000..9b1548f --- /dev/null +++ b/app/src/main/java/io/github/soclear/oneuix/hook/HideBatteryIconHook.kt @@ -0,0 +1,449 @@ +package io.github.soclear.oneuix.hook + +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import de.robv.android.xposed.XC_MethodHook +import de.robv.android.xposed.XposedBridge +import de.robv.android.xposed.XposedBridge.hookAllConstructors +import de.robv.android.xposed.XposedBridge.hookAllMethods +import de.robv.android.xposed.XposedHelpers.findAndHookMethod +import de.robv.android.xposed.XposedHelpers.findClass +import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam +import io.github.soclear.oneuix.data.Package +import java.lang.reflect.Field +import java.util.Collections +import java.util.WeakHashMap + +internal object HideBatteryIconHook { + private const val STATUS_BAR_CHARGING_ICON = "stat_sys_battery_charging" + private const val FALLBACK_CHARGING_ICON = "ic_icon_charging" + private const val UNRESOLVED_CHARGING_ICON_ID = -1 + + private var batteryChargingIconId = UNRESOLVED_CHARGING_ICON_ID + private val fieldCache: MutableMap = + Collections.synchronizedMap(mutableMapOf()) + private val applyingBatteryIconViews: MutableSet = + Collections.synchronizedSet(Collections.newSetFromMap(WeakHashMap())) + private val appliedBatteryChargingIconIds: MutableMap = + Collections.synchronizedMap(WeakHashMap()) + private val originalBatteryIconLayouts: MutableMap = + Collections.synchronizedMap(WeakHashMap()) + private val originalBatteryMeterPaddings: MutableMap = + Collections.synchronizedMap(WeakHashMap()) + private val originalBatteryMeterLayouts: MutableMap = + Collections.synchronizedMap(WeakHashMap()) + private val originalBatteryPercentLayouts: MutableMap = + Collections.synchronizedMap(WeakHashMap()) + + fun apply(loadPackageParam: LoadPackageParam) { + if (loadPackageParam.packageName != Package.SYSTEMUI) return + + val callback = object : XC_MethodHook() { + override fun afterHookedMethod(param: MethodHookParam) { + applyBatteryIconVisibility(param.thisObject) + } + } + + try { + hookAllConstructors( + findClass( + "com.android.systemui.battery.BatteryMeterView", + loadPackageParam.classLoader + ), + callback + ) + } catch (t: Throwable) { + XposedBridge.log(t) + } + + listOf( + "onFinishInflate", + "onAttachedToWindow", + "onBatteryLevelChanged", + "updatePercentText", + "updateShowPercent", + "scaleBatteryMeterViewsLegacy" + ).forEach { methodName -> + try { + hookAllMethods( + findClass( + "com.android.systemui.battery.BatteryMeterView", + loadPackageParam.classLoader + ), + methodName, + callback + ) + } catch (_: Throwable) { + } + } + + try { + hookAllMethods( + findClass( + "com.android.systemui.battery.BatteryMeterViewController\$3", + loadPackageParam.classLoader + ), + "onBatteryLevelChanged", + callback + ) + } catch (_: Throwable) { + } + + try { + findAndHookMethod( + "com.android.systemui.battery.BatteryMeterView", + loadPackageParam.classLoader, + "updateColors", + Int::class.javaPrimitiveType, + Int::class.javaPrimitiveType, + object : XC_MethodHook() { + override fun afterHookedMethod(param: MethodHookParam) { + val batteryMeterView = resolveBatteryMeterView(param.thisObject) ?: return + if (!isBatteryCharging(batteryMeterView)) return + val iconView = readFieldValue( + batteryMeterView, + listOf("mBatteryIconView", "batteryIconView") + ) as? ImageView ?: return + applyBatteryChargingIconTint(batteryMeterView, iconView) + } + } + ) + } catch (t: Throwable) { + XposedBridge.log(t) + } + } + + private fun applyBatteryIconVisibility(instance: Any) { + val batteryMeterView = resolveBatteryMeterView(instance) ?: return + if (!applyingBatteryIconViews.add(batteryMeterView)) return + try { + val view = batteryMeterView as? View ?: return + val iconView = readFieldValue( + batteryMeterView, + listOf("mBatteryIconView", "batteryIconView") + ) as? ImageView ?: return + val percentView = readFieldValue( + batteryMeterView, + listOf("mBatteryPercentView", "batteryPercentView") + ) as? TextView + + if (isBatteryCharging(batteryMeterView)) { + restoreBatteryIconLayout(iconView) + restoreBatteryMeterPadding(view) + restoreBatteryMeterLayout(view) + percentView?.let(::restoreBatteryPercentLayout) + setBatteryChargingIcon(iconView) + applyBatteryChargingIconTint(batteryMeterView, iconView) + iconView.visibility = View.VISIBLE + view.visibility = View.VISIBLE + return + } + + collapseBatteryIconLayout(iconView) + iconView.visibility = View.GONE + view.visibility = if (percentView?.isVisibleWithText() == true) { + collapseBatteryMeterEndPadding(view) + collapseBatteryMeterEndSpacing(view) + collapseBatteryPercentEndSpacing(percentView) + View.VISIBLE + } else { + View.GONE + } + } finally { + applyingBatteryIconViews.remove(batteryMeterView) + } + } + + private fun resolveBatteryMeterView(instance: Any): Any? { + if (instance is View) return instance + val controller = readFieldValue(instance, listOf("this\$0")) ?: return null + return readFieldValue(controller, listOf("mView")) + } + + private fun isBatteryCharging(batteryMeterView: Any): Boolean { + val meterCharging = readFieldValue( + batteryMeterView, + listOf("mCharging", "charging") + ) as? Boolean == true + if (meterCharging) return true + + val samsungDrawable = readFieldValue( + batteryMeterView, + listOf("mSamsungDrawable", "samsungDrawable") + ) ?: return false + val batteryState = readFieldValue( + samsungDrawable, + listOf("batteryState", "mBatteryState") + ) ?: return false + return readFieldValue(batteryState, listOf("charging")) as? Boolean == true || + readFieldValue(batteryState, listOf("isDirectPowerMode")) as? Boolean == true + } + + private fun setBatteryChargingIcon(iconView: ImageView) { + val drawableId = resolveBatteryChargingIconId(iconView) + if (drawableId == 0 || appliedBatteryChargingIconIds[iconView] == drawableId) return + iconView.setImageResource(drawableId) + appliedBatteryChargingIconIds[iconView] = drawableId + } + + private fun resolveBatteryChargingIconId(iconView: ImageView): Int { + if (batteryChargingIconId != UNRESOLVED_CHARGING_ICON_ID) return batteryChargingIconId + val resources = iconView.resources + batteryChargingIconId = resources.getIdentifier( + STATUS_BAR_CHARGING_ICON, + "drawable", + Package.SYSTEMUI + ).takeIf { it != 0 } ?: resources.getIdentifier( + FALLBACK_CHARGING_ICON, + "drawable", + Package.SYSTEMUI + ) + return batteryChargingIconId + } + + private fun applyBatteryChargingIconTint(batteryMeterView: Any, iconView: ImageView) { + val samsungDrawable = readFieldValue( + batteryMeterView, + listOf("mSamsungDrawable", "samsungDrawable") + ) ?: return + val iconTint = readFieldValue(samsungDrawable, listOf("iconTint")) as? Int ?: return + iconView.setColorFilter(iconTint) + } + + private fun collapseBatteryIconLayout(iconView: ImageView) { + rememberBatteryIconLayout(iconView) + val params = iconView.layoutParams ?: return + params.width = 0 + params.height = 0 + (params as? ViewGroup.MarginLayoutParams)?.setMargins(0, 0, 0, 0) + iconView.layoutParams = params + } + + internal fun restoreBatteryIconLayout(iconView: ImageView) { + val original = originalBatteryIconLayouts[iconView] ?: return + val params = iconView.layoutParams ?: return + params.width = original.width + params.height = original.height + (params as? ViewGroup.MarginLayoutParams)?.setMargins( + original.leftMargin, + original.topMargin, + original.rightMargin, + original.bottomMargin + ) + iconView.layoutParams = params + iconView.setPaddingRelative( + original.paddingStart, + original.paddingTop, + original.paddingEnd, + original.paddingBottom + ) + } + + private fun collapseBatteryMeterEndPadding(view: View) { + rememberBatteryMeterPadding(view) + view.setPaddingRelative( + view.paddingStart, + view.paddingTop, + 0, + view.paddingBottom + ) + } + + private fun restoreBatteryMeterPadding(view: View) { + val original = originalBatteryMeterPaddings[view] ?: return + view.setPaddingRelative( + original.start, + original.top, + original.end, + original.bottom + ) + } + + private fun rememberBatteryMeterPadding(view: View) { + if (originalBatteryMeterPaddings.containsKey(view)) return + originalBatteryMeterPaddings[view] = ViewPadding( + start = view.paddingStart, + top = view.paddingTop, + end = view.paddingEnd, + bottom = view.paddingBottom + ) + } + + private fun collapseBatteryMeterEndSpacing(view: View) { + rememberBatteryMeterLayout(view) + val params = view.layoutParams + (params as? ViewGroup.MarginLayoutParams)?.let { + it.marginEnd = 0 + it.rightMargin = 0 + view.layoutParams = it + } + } + + private fun restoreBatteryMeterLayout(view: View) { + val original = originalBatteryMeterLayouts[view] ?: return + val params = view.layoutParams ?: return + params.width = original.width + params.height = original.height + (params as? ViewGroup.MarginLayoutParams)?.setMargins( + original.leftMargin, + original.topMargin, + original.rightMargin, + original.bottomMargin + ) + view.layoutParams = params + } + + private fun rememberBatteryMeterLayout(view: View) { + if (originalBatteryMeterLayouts.containsKey(view)) return + val params = view.layoutParams ?: return + val marginParams = params as? ViewGroup.MarginLayoutParams + originalBatteryMeterLayouts[view] = BatteryIconLayout( + width = params.width, + height = params.height, + leftMargin = marginParams?.leftMargin ?: 0, + topMargin = marginParams?.topMargin ?: 0, + rightMargin = marginParams?.rightMargin ?: 0, + bottomMargin = marginParams?.bottomMargin ?: 0, + paddingStart = view.paddingStart, + paddingTop = view.paddingTop, + paddingEnd = view.paddingEnd, + paddingBottom = view.paddingBottom + ) + } + + private fun collapseBatteryPercentEndSpacing(percentView: TextView) { + rememberBatteryPercentLayout(percentView) + val params = percentView.layoutParams + (params as? ViewGroup.MarginLayoutParams)?.let { + it.marginEnd = 0 + it.rightMargin = 0 + percentView.layoutParams = it + } + percentView.setPaddingRelative( + percentView.paddingStart, + percentView.paddingTop, + 0, + percentView.paddingBottom + ) + } + + private fun restoreBatteryPercentLayout(percentView: TextView) { + val original = originalBatteryPercentLayouts[percentView] ?: return + val params = percentView.layoutParams ?: return + params.width = original.width + params.height = original.height + (params as? ViewGroup.MarginLayoutParams)?.setMargins( + original.leftMargin, + original.topMargin, + original.rightMargin, + original.bottomMargin + ) + percentView.layoutParams = params + percentView.setPaddingRelative( + original.paddingStart, + original.paddingTop, + original.paddingEnd, + original.paddingBottom + ) + } + + private fun rememberBatteryPercentLayout(percentView: TextView) { + if (originalBatteryPercentLayouts.containsKey(percentView)) return + val params = percentView.layoutParams ?: return + val marginParams = params as? ViewGroup.MarginLayoutParams + originalBatteryPercentLayouts[percentView] = BatteryIconLayout( + width = params.width, + height = params.height, + leftMargin = marginParams?.leftMargin ?: 0, + topMargin = marginParams?.topMargin ?: 0, + rightMargin = marginParams?.rightMargin ?: 0, + bottomMargin = marginParams?.bottomMargin ?: 0, + paddingStart = percentView.paddingStart, + paddingTop = percentView.paddingTop, + paddingEnd = percentView.paddingEnd, + paddingBottom = percentView.paddingBottom + ) + } + + private fun rememberBatteryIconLayout(iconView: ImageView) { + if (originalBatteryIconLayouts.containsKey(iconView)) return + val params = iconView.layoutParams ?: return + if (params.width == 0 && params.height == 0) return + val marginParams = params as? ViewGroup.MarginLayoutParams + originalBatteryIconLayouts[iconView] = BatteryIconLayout( + width = params.width, + height = params.height, + leftMargin = marginParams?.leftMargin ?: 0, + topMargin = marginParams?.topMargin ?: 0, + rightMargin = marginParams?.rightMargin ?: 0, + bottomMargin = marginParams?.bottomMargin ?: 0, + paddingStart = iconView.paddingStart, + paddingTop = iconView.paddingTop, + paddingEnd = iconView.paddingEnd, + paddingBottom = iconView.paddingBottom + ) + } + + internal fun forgetBatteryIconLayout(iconView: ImageView) { + originalBatteryIconLayouts.remove(iconView) + } + + private fun TextView.isVisibleWithText(): Boolean = + visibility == View.VISIBLE && text.isNotBlank() + + private data class BatteryIconLayout( + val width: Int, + val height: Int, + val leftMargin: Int, + val topMargin: Int, + val rightMargin: Int, + val bottomMargin: Int, + val paddingStart: Int, + val paddingTop: Int, + val paddingEnd: Int, + val paddingBottom: Int + ) + + private data class ViewPadding( + val start: Int, + val top: Int, + val end: Int, + val bottom: Int + ) + + private data class FieldCacheKey( + val clazz: Class<*>, + val name: String + ) + + private fun readFieldValue(instance: Any, names: List): Any? = + findField(instance, names)?.let { field -> runCatching { field.get(instance) }.getOrNull() } + + private fun findField(instance: Any, names: List): Field? { + var clazz: Class<*>? = instance.javaClass + while (clazz != null) { + names.forEach { name -> + findDeclaredField(clazz, name)?.let { return it } + } + clazz = clazz.superclass + } + return null + } + + private fun findDeclaredField(clazz: Class<*>, name: String): Field? { + val cacheKey = FieldCacheKey(clazz, name) + synchronized(fieldCache) { + if (fieldCache.containsKey(cacheKey)) return fieldCache[cacheKey] + } + + val field = runCatching { + clazz.getDeclaredField(name).apply { isAccessible = true } + }.getOrNull() + synchronized(fieldCache) { + fieldCache[cacheKey] = field + } + return field + } +} diff --git a/app/src/main/java/io/github/soclear/oneuix/hook/Main.kt b/app/src/main/java/io/github/soclear/oneuix/hook/Main.kt index e25b78c..522529d 100644 --- a/app/src/main/java/io/github/soclear/oneuix/hook/Main.kt +++ b/app/src/main/java/io/github/soclear/oneuix/hook/Main.kt @@ -218,7 +218,7 @@ class Main : IXposedHookLoadPackage, IXposedHookInitPackageResources { } if (preference.systemUI.statusBar.hideBatteryIcon) { - SystemUI.hideBatteryIcon(lpparam) + HideBatteryIconHook.apply(lpparam) } if (preference.systemUI.statusBar.supportRealTimeNetworkSpeed) { diff --git a/app/src/main/java/io/github/soclear/oneuix/hook/SystemUI.kt b/app/src/main/java/io/github/soclear/oneuix/hook/SystemUI.kt index b97c9f3..e4e74d2 100644 --- a/app/src/main/java/io/github/soclear/oneuix/hook/SystemUI.kt +++ b/app/src/main/java/io/github/soclear/oneuix/hook/SystemUI.kt @@ -23,7 +23,6 @@ import de.robv.android.xposed.XC_MethodHook import de.robv.android.xposed.XC_MethodReplacement import de.robv.android.xposed.XC_MethodReplacement.returnConstant import de.robv.android.xposed.XposedBridge -import de.robv.android.xposed.XposedBridge.hookAllConstructors import de.robv.android.xposed.XposedBridge.hookAllMethods import de.robv.android.xposed.XposedHelpers.callMethod import de.robv.android.xposed.XposedHelpers.findAndHookConstructor @@ -37,35 +36,12 @@ import de.robv.android.xposed.callbacks.XC_InitPackageResources.InitPackageResou import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam import io.github.soclear.oneuix.data.Package import io.github.soclear.oneuix.hook.util.TraditionalChineseCalendar -import java.lang.reflect.Field import java.time.LocalDateTime import java.time.format.DateTimeFormatter -import java.util.Collections -import java.util.WeakHashMap import kotlin.math.roundToInt object SystemUI { - private const val STATUS_BAR_CHARGING_ICON = "stat_sys_battery_charging" - private const val FALLBACK_CHARGING_ICON = "ic_icon_charging" - private const val UNRESOLVED_CHARGING_ICON_ID = -1 - - private var batteryChargingIconId = UNRESOLVED_CHARGING_ICON_ID - private val fieldCache: MutableMap = - Collections.synchronizedMap(mutableMapOf()) - private val applyingBatteryIconViews: MutableSet = - Collections.synchronizedSet(Collections.newSetFromMap(WeakHashMap())) - private val appliedBatteryChargingIconIds: MutableMap = - Collections.synchronizedMap(WeakHashMap()) - private val originalBatteryIconLayouts: MutableMap = - Collections.synchronizedMap(WeakHashMap()) - private val originalBatteryMeterPaddings: MutableMap = - Collections.synchronizedMap(WeakHashMap()) - private val originalBatteryMeterLayouts: MutableMap = - Collections.synchronizedMap(WeakHashMap()) - private val originalBatteryPercentLayouts: MutableMap = - Collections.synchronizedMap(WeakHashMap()) - enum class QsBar { MediaPlayer, NearbyDevicesAndDeviceControl, @@ -125,7 +101,7 @@ object SystemUI { object : XC_MethodHook() { override fun afterHookedMethod(param: MethodHookParam) { val mBatteryIconView = getObjectField(param.thisObject, "mBatteryIconView") as ImageView - restoreBatteryIconLayout(mBatteryIconView) + HideBatteryIconHook.restoreBatteryIconLayout(mBatteryIconView) mBatteryIconView.layoutParams = mBatteryIconView.layoutParams.apply { if (widthScale != null) { width = (width * widthScale).roundToInt() @@ -134,85 +110,7 @@ object SystemUI { height = (height * heightScale).roundToInt() } } - forgetBatteryIconLayout(mBatteryIconView) - } - } - ) - } catch (t: Throwable) { - XposedBridge.log(t) - } - } - - fun hideBatteryIcon(loadPackageParam: LoadPackageParam) { - if (loadPackageParam.packageName != Package.SYSTEMUI) return - - val callback = object : XC_MethodHook() { - override fun afterHookedMethod(param: MethodHookParam) { - applyBatteryIconVisibility(param.thisObject) - } - } - - try { - hookAllConstructors( - findClass( - "com.android.systemui.battery.BatteryMeterView", - loadPackageParam.classLoader - ), - callback - ) - } catch (t: Throwable) { - XposedBridge.log(t) - } - - listOf( - "onFinishInflate", - "onAttachedToWindow", - "onBatteryLevelChanged", - "updatePercentText", - "updateShowPercent", - "scaleBatteryMeterViewsLegacy" - ).forEach { methodName -> - try { - hookAllMethods( - findClass( - "com.android.systemui.battery.BatteryMeterView", - loadPackageParam.classLoader - ), - methodName, - callback - ) - } catch (_: Throwable) { - } - } - - try { - hookAllMethods( - findClass( - "com.android.systemui.battery.BatteryMeterViewController\$3", - loadPackageParam.classLoader - ), - "onBatteryLevelChanged", - callback - ) - } catch (_: Throwable) { - } - - try { - findAndHookMethod( - "com.android.systemui.battery.BatteryMeterView", - loadPackageParam.classLoader, - "updateColors", - Int::class.javaPrimitiveType, - Int::class.javaPrimitiveType, - object : XC_MethodHook() { - override fun afterHookedMethod(param: MethodHookParam) { - val batteryMeterView = resolveBatteryMeterView(param.thisObject) ?: return - if (!isBatteryCharging(batteryMeterView)) return - val iconView = readFieldValue( - batteryMeterView, - listOf("mBatteryIconView", "batteryIconView") - ) as? ImageView ?: return - applyBatteryChargingIconTint(batteryMeterView, iconView) + HideBatteryIconHook.forgetBatteryIconLayout(mBatteryIconView) } } ) @@ -221,338 +119,6 @@ object SystemUI { } } - private fun applyBatteryIconVisibility(instance: Any) { - val batteryMeterView = resolveBatteryMeterView(instance) ?: return - if (!applyingBatteryIconViews.add(batteryMeterView)) return - try { - val view = batteryMeterView as? View ?: return - val iconView = readFieldValue( - batteryMeterView, - listOf("mBatteryIconView", "batteryIconView") - ) as? ImageView ?: return - val percentView = readFieldValue( - batteryMeterView, - listOf("mBatteryPercentView", "batteryPercentView") - ) as? TextView - - if (isBatteryCharging(batteryMeterView)) { - restoreBatteryIconLayout(iconView) - restoreBatteryMeterPadding(view) - restoreBatteryMeterLayout(view) - percentView?.let(::restoreBatteryPercentLayout) - setBatteryChargingIcon(iconView) - applyBatteryChargingIconTint(batteryMeterView, iconView) - iconView.visibility = View.VISIBLE - view.visibility = View.VISIBLE - return - } - - collapseBatteryIconLayout(iconView) - iconView.visibility = View.GONE - view.visibility = if (percentView?.isVisibleWithText() == true) { - collapseBatteryMeterEndPadding(view) - collapseBatteryMeterEndSpacing(view) - collapseBatteryPercentEndSpacing(percentView) - View.VISIBLE - } else { - View.GONE - } - } finally { - applyingBatteryIconViews.remove(batteryMeterView) - } - } - - private fun resolveBatteryMeterView(instance: Any): Any? { - if (instance is View) return instance - val controller = readFieldValue(instance, listOf("this\$0")) ?: return null - return readFieldValue(controller, listOf("mView")) - } - - private fun isBatteryCharging(batteryMeterView: Any): Boolean { - val meterCharging = readFieldValue( - batteryMeterView, - listOf("mCharging", "charging") - ) as? Boolean == true - if (meterCharging) return true - - val samsungDrawable = readFieldValue( - batteryMeterView, - listOf("mSamsungDrawable", "samsungDrawable") - ) ?: return false - val batteryState = readFieldValue( - samsungDrawable, - listOf("batteryState", "mBatteryState") - ) ?: return false - return readFieldValue(batteryState, listOf("charging")) as? Boolean == true || - readFieldValue(batteryState, listOf("isDirectPowerMode")) as? Boolean == true - } - - private fun setBatteryChargingIcon(iconView: ImageView) { - val drawableId = resolveBatteryChargingIconId(iconView) - if (drawableId == 0 || appliedBatteryChargingIconIds[iconView] == drawableId) return - iconView.setImageResource(drawableId) - appliedBatteryChargingIconIds[iconView] = drawableId - } - - private fun resolveBatteryChargingIconId(iconView: ImageView): Int { - if (batteryChargingIconId != UNRESOLVED_CHARGING_ICON_ID) return batteryChargingIconId - val resources = iconView.resources - batteryChargingIconId = resources.getIdentifier( - STATUS_BAR_CHARGING_ICON, - "drawable", - Package.SYSTEMUI - ).takeIf { it != 0 } ?: resources.getIdentifier( - FALLBACK_CHARGING_ICON, - "drawable", - Package.SYSTEMUI - ) - return batteryChargingIconId - } - - private fun applyBatteryChargingIconTint(batteryMeterView: Any, iconView: ImageView) { - val samsungDrawable = readFieldValue( - batteryMeterView, - listOf("mSamsungDrawable", "samsungDrawable") - ) ?: return - val iconTint = readFieldValue(samsungDrawable, listOf("iconTint")) as? Int ?: return - iconView.setColorFilter(iconTint) - } - - private fun collapseBatteryIconLayout(iconView: ImageView) { - rememberBatteryIconLayout(iconView) - val params = iconView.layoutParams ?: return - params.width = 0 - params.height = 0 - (params as? ViewGroup.MarginLayoutParams)?.setMargins(0, 0, 0, 0) - iconView.layoutParams = params - } - - private fun restoreBatteryIconLayout(iconView: ImageView) { - val original = originalBatteryIconLayouts[iconView] ?: return - val params = iconView.layoutParams ?: return - params.width = original.width - params.height = original.height - (params as? ViewGroup.MarginLayoutParams)?.setMargins( - original.leftMargin, - original.topMargin, - original.rightMargin, - original.bottomMargin - ) - iconView.layoutParams = params - iconView.setPaddingRelative( - original.paddingStart, - original.paddingTop, - original.paddingEnd, - original.paddingBottom - ) - } - - private fun collapseBatteryMeterEndPadding(view: View) { - rememberBatteryMeterPadding(view) - view.setPaddingRelative( - view.paddingStart, - view.paddingTop, - 0, - view.paddingBottom - ) - } - - private fun restoreBatteryMeterPadding(view: View) { - val original = originalBatteryMeterPaddings[view] ?: return - view.setPaddingRelative( - original.start, - original.top, - original.end, - original.bottom - ) - } - - private fun rememberBatteryMeterPadding(view: View) { - if (originalBatteryMeterPaddings.containsKey(view)) return - originalBatteryMeterPaddings[view] = ViewPadding( - start = view.paddingStart, - top = view.paddingTop, - end = view.paddingEnd, - bottom = view.paddingBottom - ) - } - - private fun collapseBatteryMeterEndSpacing(view: View) { - rememberBatteryMeterLayout(view) - val params = view.layoutParams - (params as? ViewGroup.MarginLayoutParams)?.let { - it.marginEnd = 0 - it.rightMargin = 0 - view.layoutParams = it - } - } - - private fun restoreBatteryMeterLayout(view: View) { - val original = originalBatteryMeterLayouts[view] ?: return - val params = view.layoutParams ?: return - params.width = original.width - params.height = original.height - (params as? ViewGroup.MarginLayoutParams)?.setMargins( - original.leftMargin, - original.topMargin, - original.rightMargin, - original.bottomMargin - ) - view.layoutParams = params - } - - private fun rememberBatteryMeterLayout(view: View) { - if (originalBatteryMeterLayouts.containsKey(view)) return - val params = view.layoutParams ?: return - val marginParams = params as? ViewGroup.MarginLayoutParams - originalBatteryMeterLayouts[view] = BatteryIconLayout( - width = params.width, - height = params.height, - leftMargin = marginParams?.leftMargin ?: 0, - topMargin = marginParams?.topMargin ?: 0, - rightMargin = marginParams?.rightMargin ?: 0, - bottomMargin = marginParams?.bottomMargin ?: 0, - paddingStart = view.paddingStart, - paddingTop = view.paddingTop, - paddingEnd = view.paddingEnd, - paddingBottom = view.paddingBottom - ) - } - - private fun collapseBatteryPercentEndSpacing(percentView: TextView) { - rememberBatteryPercentLayout(percentView) - val params = percentView.layoutParams - (params as? ViewGroup.MarginLayoutParams)?.let { - it.marginEnd = 0 - it.rightMargin = 0 - percentView.layoutParams = it - } - percentView.setPaddingRelative( - percentView.paddingStart, - percentView.paddingTop, - 0, - percentView.paddingBottom - ) - } - - private fun restoreBatteryPercentLayout(percentView: TextView) { - val original = originalBatteryPercentLayouts[percentView] ?: return - val params = percentView.layoutParams ?: return - params.width = original.width - params.height = original.height - (params as? ViewGroup.MarginLayoutParams)?.setMargins( - original.leftMargin, - original.topMargin, - original.rightMargin, - original.bottomMargin - ) - percentView.layoutParams = params - percentView.setPaddingRelative( - original.paddingStart, - original.paddingTop, - original.paddingEnd, - original.paddingBottom - ) - } - - private fun rememberBatteryPercentLayout(percentView: TextView) { - if (originalBatteryPercentLayouts.containsKey(percentView)) return - val params = percentView.layoutParams ?: return - val marginParams = params as? ViewGroup.MarginLayoutParams - originalBatteryPercentLayouts[percentView] = BatteryIconLayout( - width = params.width, - height = params.height, - leftMargin = marginParams?.leftMargin ?: 0, - topMargin = marginParams?.topMargin ?: 0, - rightMargin = marginParams?.rightMargin ?: 0, - bottomMargin = marginParams?.bottomMargin ?: 0, - paddingStart = percentView.paddingStart, - paddingTop = percentView.paddingTop, - paddingEnd = percentView.paddingEnd, - paddingBottom = percentView.paddingBottom - ) - } - - private fun rememberBatteryIconLayout(iconView: ImageView) { - if (originalBatteryIconLayouts.containsKey(iconView)) return - val params = iconView.layoutParams ?: return - if (params.width == 0 && params.height == 0) return - val marginParams = params as? ViewGroup.MarginLayoutParams - originalBatteryIconLayouts[iconView] = BatteryIconLayout( - width = params.width, - height = params.height, - leftMargin = marginParams?.leftMargin ?: 0, - topMargin = marginParams?.topMargin ?: 0, - rightMargin = marginParams?.rightMargin ?: 0, - bottomMargin = marginParams?.bottomMargin ?: 0, - paddingStart = iconView.paddingStart, - paddingTop = iconView.paddingTop, - paddingEnd = iconView.paddingEnd, - paddingBottom = iconView.paddingBottom - ) - } - - private fun forgetBatteryIconLayout(iconView: ImageView) { - originalBatteryIconLayouts.remove(iconView) - } - - private fun TextView.isVisibleWithText(): Boolean = - visibility == View.VISIBLE && text.isNotBlank() - - private data class BatteryIconLayout( - val width: Int, - val height: Int, - val leftMargin: Int, - val topMargin: Int, - val rightMargin: Int, - val bottomMargin: Int, - val paddingStart: Int, - val paddingTop: Int, - val paddingEnd: Int, - val paddingBottom: Int - ) - - private data class ViewPadding( - val start: Int, - val top: Int, - val end: Int, - val bottom: Int - ) - - private data class FieldCacheKey( - val clazz: Class<*>, - val name: String - ) - - private fun readFieldValue(instance: Any, names: List): Any? = - findField(instance, names)?.let { field -> runCatching { field.get(instance) }.getOrNull() } - - private fun findField(instance: Any, names: List): Field? { - var clazz: Class<*>? = instance.javaClass - while (clazz != null) { - names.forEach { name -> - findDeclaredField(clazz, name)?.let { return it } - } - clazz = clazz.superclass - } - return null - } - - private fun findDeclaredField(clazz: Class<*>, name: String): Field? { - val cacheKey = FieldCacheKey(clazz, name) - synchronized(fieldCache) { - if (fieldCache.containsKey(cacheKey)) return fieldCache[cacheKey] - } - - val field = runCatching { - clazz.getDeclaredField(name).apply { isAccessible = true } - }.getOrNull() - synchronized(fieldCache) { - fieldCache[cacheKey] = field - } - return field - } - fun hideBatteryPercentageSign(resparam: InitPackageResourcesParam) { if (resparam.packageName != Package.SYSTEMUI || Build.VERSION.SDK_INT > Build.VERSION_CODES.UPSIDE_DOWN_CAKE From 3df0dea093d40eefe3bd3c92f177e4aca1974e22 Mon Sep 17 00:00:00 2001 From: Ivan Date: Wed, 20 May 2026 20:21:19 +0300 Subject: [PATCH 6/6] fix: decouple battery scaling from hidden icon layout --- .../oneuix/hook/HideBatteryIconHook.kt | 37 ++----------------- .../io/github/soclear/oneuix/hook/SystemUI.kt | 2 - 2 files changed, 4 insertions(+), 35 deletions(-) diff --git a/app/src/main/java/io/github/soclear/oneuix/hook/HideBatteryIconHook.kt b/app/src/main/java/io/github/soclear/oneuix/hook/HideBatteryIconHook.kt index 9b1548f..90ef923 100644 --- a/app/src/main/java/io/github/soclear/oneuix/hook/HideBatteryIconHook.kt +++ b/app/src/main/java/io/github/soclear/oneuix/hook/HideBatteryIconHook.kt @@ -10,6 +10,7 @@ import de.robv.android.xposed.XposedBridge.hookAllConstructors import de.robv.android.xposed.XposedBridge.hookAllMethods import de.robv.android.xposed.XposedHelpers.findAndHookMethod import de.robv.android.xposed.XposedHelpers.findClass +import de.robv.android.xposed.XposedHelpers.findFieldIfExists import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam import io.github.soclear.oneuix.data.Package import java.lang.reflect.Field @@ -22,8 +23,6 @@ internal object HideBatteryIconHook { private const val UNRESOLVED_CHARGING_ICON_ID = -1 private var batteryChargingIconId = UNRESOLVED_CHARGING_ICON_ID - private val fieldCache: MutableMap = - Collections.synchronizedMap(mutableMapOf()) private val applyingBatteryIconViews: MutableSet = Collections.synchronizedSet(Collections.newSetFromMap(WeakHashMap())) private val appliedBatteryChargingIconIds: MutableMap = @@ -221,7 +220,7 @@ internal object HideBatteryIconHook { iconView.layoutParams = params } - internal fun restoreBatteryIconLayout(iconView: ImageView) { + private fun restoreBatteryIconLayout(iconView: ImageView) { val original = originalBatteryIconLayouts[iconView] ?: return val params = iconView.layoutParams ?: return params.width = original.width @@ -386,10 +385,6 @@ internal object HideBatteryIconHook { ) } - internal fun forgetBatteryIconLayout(iconView: ImageView) { - originalBatteryIconLayouts.remove(iconView) - } - private fun TextView.isVisibleWithText(): Boolean = visibility == View.VISIBLE && text.isNotBlank() @@ -413,37 +408,13 @@ internal object HideBatteryIconHook { val bottom: Int ) - private data class FieldCacheKey( - val clazz: Class<*>, - val name: String - ) - private fun readFieldValue(instance: Any, names: List): Any? = findField(instance, names)?.let { field -> runCatching { field.get(instance) }.getOrNull() } private fun findField(instance: Any, names: List): Field? { - var clazz: Class<*>? = instance.javaClass - while (clazz != null) { - names.forEach { name -> - findDeclaredField(clazz, name)?.let { return it } - } - clazz = clazz.superclass + names.forEach { name -> + findFieldIfExists(instance.javaClass, name)?.let { return it } } return null } - - private fun findDeclaredField(clazz: Class<*>, name: String): Field? { - val cacheKey = FieldCacheKey(clazz, name) - synchronized(fieldCache) { - if (fieldCache.containsKey(cacheKey)) return fieldCache[cacheKey] - } - - val field = runCatching { - clazz.getDeclaredField(name).apply { isAccessible = true } - }.getOrNull() - synchronized(fieldCache) { - fieldCache[cacheKey] = field - } - return field - } } diff --git a/app/src/main/java/io/github/soclear/oneuix/hook/SystemUI.kt b/app/src/main/java/io/github/soclear/oneuix/hook/SystemUI.kt index e4e74d2..98bae34 100644 --- a/app/src/main/java/io/github/soclear/oneuix/hook/SystemUI.kt +++ b/app/src/main/java/io/github/soclear/oneuix/hook/SystemUI.kt @@ -101,7 +101,6 @@ object SystemUI { object : XC_MethodHook() { override fun afterHookedMethod(param: MethodHookParam) { val mBatteryIconView = getObjectField(param.thisObject, "mBatteryIconView") as ImageView - HideBatteryIconHook.restoreBatteryIconLayout(mBatteryIconView) mBatteryIconView.layoutParams = mBatteryIconView.layoutParams.apply { if (widthScale != null) { width = (width * widthScale).roundToInt() @@ -110,7 +109,6 @@ object SystemUI { height = (height * heightScale).roundToInt() } } - HideBatteryIconHook.forgetBatteryIconLayout(mBatteryIconView) } } )