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/HideBatteryIconHook.kt b/app/src/main/java/io/github/soclear/oneuix/hook/HideBatteryIconHook.kt new file mode 100644 index 0000000..90ef923 --- /dev/null +++ b/app/src/main/java/io/github/soclear/oneuix/hook/HideBatteryIconHook.kt @@ -0,0 +1,420 @@ +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.XposedHelpers.findFieldIfExists +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 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 + } + + 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 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? { + names.forEach { name -> + findFieldIfExists(instance.javaClass, name)?.let { return it } + } + return null + } +} 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..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 @@ -217,6 +217,10 @@ class Main : IXposedHookLoadPackage, IXposedHookInitPackageResources { SystemUI.setBatteryIconScale(lpparam, widthScale, heightScale) } + if (preference.systemUI.statusBar.hideBatteryIcon) { + HideBatteryIconHook.apply(lpparam) + } + if (preference.systemUI.statusBar.supportRealTimeNetworkSpeed) { Network.supportRealTimeNetworkSpeed(lpparam) } 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..2692824 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 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 b8454db..1ac718e 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..a0514d0 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..f202c40 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 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