From eef433ce883ec0de4f4b45dba815033ecd5a545a Mon Sep 17 00:00:00 2001 From: ajp_anton Date: Thu, 3 Sep 2026 00:55:12 +0000 Subject: [PATCH] Trim SystemUI rendering infrastructure --- .../module/AospLockscreenBackend.kt | 53 +++--------------- .../module/OneUiCardsGridRenderer.kt | 8 ++- .../module/OneUiLockscreenCardsBackend.kt | 54 +++---------------- .../module/OneUiMiscBackend.kt | 27 ++-------- .../module/ProcessVisibilityPolicyCache.kt | 6 --- .../module/SamsungAodBackend.kt | 26 ++++----- .../module/SystemUiReflection.kt | 46 ++++++++++++++++ 7 files changed, 77 insertions(+), 143 deletions(-) create mode 100644 app/src/main/java/se/ajpanton/notificationsmaster/module/SystemUiReflection.kt diff --git a/app/src/main/java/se/ajpanton/notificationsmaster/module/AospLockscreenBackend.kt b/app/src/main/java/se/ajpanton/notificationsmaster/module/AospLockscreenBackend.kt index 3a966d5..c1a3b40 100644 --- a/app/src/main/java/se/ajpanton/notificationsmaster/module/AospLockscreenBackend.kt +++ b/app/src/main/java/se/ajpanton/notificationsmaster/module/AospLockscreenBackend.kt @@ -29,11 +29,7 @@ internal class AospLockscreenBackend( private lateinit var calculateShelfIcons: Method private lateinit var shelfNotification: Field private lateinit var shelfIconStates: Field - private lateinit var shelfClampedAmount: Field - private lateinit var shelfAppearAmount: Field - private lateinit var shelfAlpha: Field - private lateinit var shelfHidden: Field - private lateinit var shelfVisibleState: Field + private lateinit var iconState: IconStateAccess @Volatile private var blockedObserved = false override fun install(classLoader: ClassLoader): Boolean { @@ -108,13 +104,13 @@ internal class AospLockscreenBackend( val entryMethod = rowClass.methods.firstOrNull { it.name == "getEntry" && it.parameterCount == 0 } val entryField = findField(rowClass, "mEntry", "entry") require(entryMethod != null || entryField != null) { "AOSP notification row entry is unavailable" } - entryForRow = accessor(entryMethod, entryField) + entryForRow = memberAccessor(entryMethod, entryField) val entryClass = entryMethod?.returnType ?: entryField!!.type val sbnMethod = entryClass.methods.firstOrNull { it.name == "getSbn" && it.parameterCount == 0 } val sbnField = findField(entryClass, "mSbn", "sbn", "mStatusBarNotification") require(sbnMethod != null || sbnField != null) { "AOSP notification entry SBN is unavailable" } - val accessor = accessor(sbnMethod, sbnField) + val accessor = memberAccessor(sbnMethod, sbnField) sbnForEntry = { accessor(it) as? StatusBarNotification } val attachedChildren = rowClass.methods.firstOrNull { it.name == "getAttachedChildren" && it.parameterCount == 0 @@ -139,17 +135,12 @@ internal class AospLockscreenBackend( private fun installShelfFilter(classLoader: ClassLoader) { val iconContainer = Class.forName(ICON_CONTAINER, false, classLoader) val iconView = Class.forName(STATUS_BAR_ICON_VIEW, false, classLoader) - val iconState = Class.forName(ICON_STATE, false, classLoader) + iconState = IconStateAccess(Class.forName(ICON_STATE, false, classLoader)) calculateShelfIcons = iconContainer.declaredMethods.singleOrNull { it.name == "calculateIconXTranslations" && it.parameterCount == 0 }?.also { it.isAccessible = true } ?: error("AOSP shelf icon layout method not found") shelfNotification = requireField(iconView, "mNotification") shelfIconStates = requireField(iconContainer, "mIconStates") - shelfClampedAmount = requireField(iconState, "clampedAppearAmount") - shelfAppearAmount = requireField(iconState, "iconAppearAmount") - shelfAlpha = requireField(iconState, "mAlpha") - shelfHidden = requireField(iconState, "hidden") - shelfVisibleState = requireField(iconState, "visibleState") framework.hook(calculateShelfIcons).intercept { chain -> val container = chain.thisObject as? ViewGroup @@ -161,14 +152,14 @@ internal class AospLockscreenBackend( val states = shelfIconStates.get(container) as? Map<*, *> ?: return@intercept chain.proceed() val blocked = children.filter(::isBlockedShelfIcon) - blocked.forEach { states[it]?.let(::hideShelfIconState) } + blocked.forEach { states[it]?.let(iconState::hide) } children.filterNot(blocked::contains).forEach(container::bringChildToFront) val result = try { chain.proceed() } finally { children.forEach(container::bringChildToFront) } - blocked.forEach { states[it]?.let(::hideShelfIconState) } + blocked.forEach { states[it]?.let(iconState::hide) } result } } @@ -245,14 +236,6 @@ internal class AospLockscreenBackend( return runCatching { onKeyguard.invoke(stack) as Boolean }.getOrDefault(false) } - private fun hideShelfIconState(state: Any) { - shelfClampedAmount.setFloat(state, 0f) - shelfAppearAmount.setFloat(state, 0f) - shelfAlpha.setFloat(state, 0f) - shelfHidden.setBoolean(state, true) - shelfVisibleState.setInt(state, ICON_HIDDEN) - } - private fun hideRow(row: View) { if (row.visibility == View.GONE && row.alpha == 0f) return hiddenRows.putIfAbsent(row, RowState(row.visibility, row.alpha)) @@ -281,29 +264,6 @@ internal class AospLockscreenBackend( setGroupCount(row, current.size) } - private fun accessor(method: Method?, field: Field?): (Any) -> Any? { - method?.isAccessible = true - field?.isAccessible = true - return if (method != null) ({ target -> method.invoke(target) }) else ({ target -> field!!.get(target) }) - } - - private fun findField(type: Class<*>, vararg names: String): Field? { - var current: Class<*>? = type - while (current != null) { - names.forEach { name -> - runCatching { current.getDeclaredField(name) }.getOrNull()?.let { - it.isAccessible = true - return it - } - } - current = current.superclass - } - return null - } - - private fun requireField(type: Class<*>, name: String) = - findField(type, name) ?: error("AOSP $name field is unavailable") - private data class RowState(val visibility: Int, val alpha: Float) private companion object { @@ -314,6 +274,5 @@ internal class AospLockscreenBackend( const val ICON_CONTAINER = "com.android.systemui.statusbar.phone.NotificationIconContainer" const val ICON_STATE = "$ICON_CONTAINER\$IconState" const val STATUS_BAR_ICON_VIEW = "com.android.systemui.statusbar.StatusBarIconView" - const val ICON_HIDDEN = 2 } } diff --git a/app/src/main/java/se/ajpanton/notificationsmaster/module/OneUiCardsGridRenderer.kt b/app/src/main/java/se/ajpanton/notificationsmaster/module/OneUiCardsGridRenderer.kt index c3b3b59..3497d0f 100644 --- a/app/src/main/java/se/ajpanton/notificationsmaster/module/OneUiCardsGridRenderer.kt +++ b/app/src/main/java/se/ajpanton/notificationsmaster/module/OneUiCardsGridRenderer.kt @@ -37,13 +37,11 @@ internal class OneUiCardsGridRenderer { suspend(container, false) return } - state.active = active + state.active = true state.visibleCardKeys = visibleCardKeys state.config = config - if (active) { - val native = if (config.hideShelf) findNativeSurface(container) else container - if (native !== container.rootView) hideNative(state, native) - } + val native = if (config.hideShelf) findNativeSurface(container) else container + if (native !== container.rootView) hideNative(state, native) scheduleRender(container, state) } diff --git a/app/src/main/java/se/ajpanton/notificationsmaster/module/OneUiLockscreenCardsBackend.kt b/app/src/main/java/se/ajpanton/notificationsmaster/module/OneUiLockscreenCardsBackend.kt index 4b4e840..13f4d05 100644 --- a/app/src/main/java/se/ajpanton/notificationsmaster/module/OneUiLockscreenCardsBackend.kt +++ b/app/src/main/java/se/ajpanton/notificationsmaster/module/OneUiLockscreenCardsBackend.kt @@ -8,7 +8,6 @@ import android.view.ViewGroup import io.github.libxposed.api.XposedInterface import se.ajpanton.notificationsmaster.visibility.NotificationSurface import java.lang.reflect.Field -import java.lang.reflect.Method import java.util.Collections import java.util.WeakHashMap import java.util.function.Function @@ -24,11 +23,7 @@ internal class OneUiLockscreenCardsBackend( private lateinit var sbnForEntry: (Any) -> StatusBarNotification? private lateinit var shelfNotification: Field private lateinit var shelfIconStates: Field - private lateinit var shelfClampedAmount: Field - private lateinit var shelfAppearAmount: Field - private lateinit var shelfAlpha: Field - private lateinit var shelfHidden: Field - private lateinit var shelfVisibleState: Field + private lateinit var iconState: IconStateAccess @Volatile private var statusBarStateKnown = false @Volatile private var statusBarState = SHADE @Volatile private var dozing = false @@ -117,13 +112,13 @@ internal class OneUiLockscreenCardsBackend( val entryMethod = rowClass.methods.firstOrNull { it.name == "getEntry" && it.parameterCount == 0 } val entryField = findField(rowClass, "mEntry", "entry") require(entryMethod != null || entryField != null) { "OneUI notification row entry is unavailable" } - entryForRow = accessor(entryMethod, entryField) + entryForRow = memberAccessor(entryMethod, entryField) val entryClass = entryMethod?.returnType ?: entryField!!.type val sbnMethod = entryClass.methods.firstOrNull { it.name == "getSbn" && it.parameterCount == 0 } val sbnField = findField(entryClass, "mSbn", "sbn", "mStatusBarNotification") require(sbnMethod != null || sbnField != null) { "OneUI notification entry SBN is unavailable" } - val accessor = accessor(sbnMethod, sbnField) + val accessor = memberAccessor(sbnMethod, sbnField) sbnForEntry = { accessor(it) as? StatusBarNotification } } @@ -136,12 +131,7 @@ internal class OneUiLockscreenCardsBackend( } shelfIconStates = findField(shelfIcons, "mIconStates") ?: error("OneUI shelf icon states are unavailable") - val iconState = Class.forName(ICON_STATE, false, classLoader) - shelfClampedAmount = requireField(iconState, "clampedAppearAmount") - shelfAppearAmount = requireField(iconState, "iconAppearAmount") - shelfAlpha = requireField(iconState, "mAlpha") - shelfHidden = requireField(iconState, "hidden") - shelfVisibleState = requireField(iconState, "visibleState") + iconState = IconStateAccess(Class.forName(ICON_STATE, false, classLoader)) val calculate = shelfIcons.declaredMethods.singleOrNull { it.name == "calculateIconXTranslations" && it.parameterCount == 0 } ?: error("OneUI shelf icon layout method not found") @@ -156,7 +146,7 @@ internal class OneUiLockscreenCardsBackend( val states = shelfIconStates.get(container) as? Map<*, *> ?: return@intercept chain.proceed() val blocked = children.filter(::isBlockedShelfIcon) - blocked.forEach { icon -> states[icon]?.let(::hideShelfIconState) } + blocked.forEach { icon -> states[icon]?.let(iconState::hide) } children.filterNot(blocked::contains).forEach(container::bringChildToFront) val result = try { chain.proceed() @@ -165,7 +155,7 @@ internal class OneUiLockscreenCardsBackend( } // OneUI applies these states after this calculation returns. Suppress every // stock shelf icon in Cards mode; the renderer owns the supplemental grid. - children.forEach { icon -> states[icon]?.let(::hideShelfIconState) } + children.forEach { icon -> states[icon]?.let(iconState::hide) } if (!nativeShelfSuppressedObserved) { nativeShelfSuppressedObserved = true Log.i(TAG, "Suppressed the native OneUI Cards shelf icon states") @@ -216,12 +206,6 @@ internal class OneUiLockscreenCardsBackend( } } - private fun accessor(method: Method?, field: Field?): (Any) -> Any? { - method?.isAccessible = true - field?.isAccessible = true - return if (method != null) ({ target -> method.invoke(target) }) else ({ target -> field!!.get(target) }) - } - private fun updateState(controller: Any) { (findField(controller.javaClass, "mState")?.get(controller) as? Int)?.let { statusBarState = it @@ -297,14 +281,6 @@ internal class OneUiLockscreenCardsBackend( } } - private fun hideShelfIconState(state: Any) { - shelfClampedAmount.setFloat(state, 0f) - shelfAppearAmount.setFloat(state, 0f) - shelfAlpha.setFloat(state, 0f) - shelfHidden.setBoolean(state, true) - shelfVisibleState.setInt(state, ICON_HIDDEN) - } - private fun hideRow(row: View) { if (row.visibility == View.GONE && row.alpha == 0f) return hiddenRows.putIfAbsent(row, RowState(row.visibility, row.alpha)) @@ -350,23 +326,6 @@ internal class OneUiLockscreenCardsBackend( private fun booleanField(target: Any, name: String) = runCatching { findField(target.javaClass, name)?.getBoolean(target) ?: false }.getOrDefault(false) - private fun requireField(type: Class<*>, name: String) = - findField(type, name) ?: error("OneUI $name field is unavailable") - - private fun findField(type: Class<*>, vararg names: String): Field? { - var current: Class<*>? = type - while (current != null) { - names.forEach { name -> - runCatching { current.getDeclaredField(name) }.getOrNull()?.let { - it.isAccessible = true - return it - } - } - current = current.superclass - } - return null - } - private fun gridConfig() = OneUiCardsGridRenderer.Config( ProcessVisibilityPolicyCache.lockscreenCards, showPills = true, @@ -380,7 +339,6 @@ internal class OneUiLockscreenCardsBackend( const val SHADE = 0 const val KEYGUARD = 1 const val CARDS = 0 - const val ICON_HIDDEN = 2 const val LOCKSCREEN_STYLE = "lockscreen_minimizing_notification" const val STACK = "com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout" const val ROW = "com.android.systemui.statusbar.notification.row.ExpandableNotificationRow" diff --git a/app/src/main/java/se/ajpanton/notificationsmaster/module/OneUiMiscBackend.kt b/app/src/main/java/se/ajpanton/notificationsmaster/module/OneUiMiscBackend.kt index 7f9554f..4d05657 100644 --- a/app/src/main/java/se/ajpanton/notificationsmaster/module/OneUiMiscBackend.kt +++ b/app/src/main/java/se/ajpanton/notificationsmaster/module/OneUiMiscBackend.kt @@ -18,7 +18,6 @@ import android.view.ViewGroup import android.widget.FrameLayout import android.widget.TextView import io.github.libxposed.api.XposedInterface -import java.lang.reflect.Field import java.lang.reflect.Method import java.text.NumberFormat import java.util.WeakHashMap @@ -390,11 +389,7 @@ private data class BatteryState(val level: Int, val plugged: Boolean, val charge private fun currentBatteryState(context: Context): BatteryState? { val app = context.applicationContext val filter = IntentFilter(Intent.ACTION_BATTERY_CHANGED) - val intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - app.registerReceiver(null, filter, Context.RECEIVER_EXPORTED) - } else { - app.registerReceiver(null, filter) - } ?: return null + val intent = app.registerReceiver(null, filter, Context.RECEIVER_EXPORTED) ?: return null val level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) val scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, 100) if (level < 0 || scale <= 0) return null @@ -405,31 +400,15 @@ private fun currentBatteryState(context: Context): BatteryState? { return BatteryState(percent, plugged, status == BatteryManager.BATTERY_STATUS_FULL || percent >= 100) } -private fun currentApplicationContext(): Context? = runCatching { - Class.forName("android.app.ActivityThread").getMethod("currentApplication").invoke(null) as? Context -}.getOrNull() - private fun ClassLoader.findClass(name: String) = runCatching { Class.forName(name, false, this) }.getOrNull() private fun Class<*>.methodsNamed(name: String): List = declaredMethods.filter { it.name == name }.onEach { it.isAccessible = true } -private fun Class<*>.staticField(name: String): Any? = findField(name)?.get(null) +private fun Class<*>.staticField(name: String): Any? = findField(this, name)?.get(null) -private fun Any?.field(name: String): Any? = this?.javaClass?.findField(name)?.get(this) - -private fun Class<*>.findField(name: String): Field? { - var type: Class<*>? = this - while (type != null) { - runCatching { type.getDeclaredField(name) }.getOrNull()?.let { - it.isAccessible = true - return it - } - type = type.superclass - } - return null -} +private fun Any?.field(name: String): Any? = this?.let { findField(it.javaClass, name)?.get(it) } private fun Any?.invokeNamed(name: String, vararg args: Any?): Any? { val target = this ?: return null diff --git a/app/src/main/java/se/ajpanton/notificationsmaster/module/ProcessVisibilityPolicyCache.kt b/app/src/main/java/se/ajpanton/notificationsmaster/module/ProcessVisibilityPolicyCache.kt index 4174036..3cf270c 100644 --- a/app/src/main/java/se/ajpanton/notificationsmaster/module/ProcessVisibilityPolicyCache.kt +++ b/app/src/main/java/se/ajpanton/notificationsmaster/module/ProcessVisibilityPolicyCache.kt @@ -73,12 +73,6 @@ internal object ProcessVisibilityPolicyCache { } } - private fun currentApplicationContext(): Context? = runCatching { - Class.forName("android.app.ActivityThread") - .getMethod("currentApplication") - .invoke(null) as? Context - }.getOrNull() - private fun retryOrGiveUp() { if (++attempts < MAX_ATTEMPTS) { Handler(Looper.getMainLooper()).postDelayed(::tryInstall, RETRY_DELAY_MILLIS) diff --git a/app/src/main/java/se/ajpanton/notificationsmaster/module/SamsungAodBackend.kt b/app/src/main/java/se/ajpanton/notificationsmaster/module/SamsungAodBackend.kt index 33ab3f0..1d003b2 100644 --- a/app/src/main/java/se/ajpanton/notificationsmaster/module/SamsungAodBackend.kt +++ b/app/src/main/java/se/ajpanton/notificationsmaster/module/SamsungAodBackend.kt @@ -170,26 +170,26 @@ internal class SamsungAodBackend(private val framework: XposedInterface) : Visib } private fun beginDozeTransition(container: ViewGroup, origin: Pair) { - val now = SystemClock.uptimeMillis() val current = screenLocation(container) - cardsTransitions[container] = CardsTransition( - origin, - current, - current != origin, - now, - now + TRANSITION_FALLBACK_DELAY, - ) - cardsRenderer.suspend(container, isCardsMode(container), container) - refreshCardsGrid(POSITION_POLL_DELAY) + beginTransition(container, origin, current, current != origin) } private fun beginAttachedTransition(container: ViewGroup) { - val now = SystemClock.uptimeMillis() val current = screenLocation(container) + beginTransition(container, current, current, true) + } + + private fun beginTransition( + container: ViewGroup, + origin: Pair, + current: Pair, + moved: Boolean, + ) { + val now = SystemClock.uptimeMillis() cardsTransitions[container] = CardsTransition( + origin, current, - current, - moved = true, + moved, stableSince = now, deadline = now + TRANSITION_FALLBACK_DELAY, ) diff --git a/app/src/main/java/se/ajpanton/notificationsmaster/module/SystemUiReflection.kt b/app/src/main/java/se/ajpanton/notificationsmaster/module/SystemUiReflection.kt new file mode 100644 index 0000000..012e9c4 --- /dev/null +++ b/app/src/main/java/se/ajpanton/notificationsmaster/module/SystemUiReflection.kt @@ -0,0 +1,46 @@ +package se.ajpanton.notificationsmaster.module + +import android.content.Context +import java.lang.reflect.Field +import java.lang.reflect.Method + +internal fun findField(type: Class<*>, vararg names: String): Field? { + var current: Class<*>? = type + while (current != null) { + names.firstNotNullOfOrNull { runCatching { current.getDeclaredField(it) }.getOrNull() }?.let { + it.isAccessible = true + return it + } + current = current.superclass + } + return null +} + +internal fun memberAccessor(method: Method?, field: Field?): (Any) -> Any? { + method?.isAccessible = true + field?.isAccessible = true + return if (method != null) ({ method.invoke(it) }) else ({ field!!.get(it) }) +} + +internal fun requireField(type: Class<*>, name: String) = + findField(type, name) ?: error("${type.name}.$name is unavailable") + +internal class IconStateAccess(type: Class<*>) { + private val clampedAmount = requireField(type, "clampedAppearAmount") + private val appearAmount = requireField(type, "iconAppearAmount") + private val alpha = requireField(type, "mAlpha") + private val hidden = requireField(type, "hidden") + private val visibleState = requireField(type, "visibleState") + + fun hide(state: Any) { + clampedAmount.setFloat(state, 0f) + appearAmount.setFloat(state, 0f) + alpha.setFloat(state, 0f) + hidden.setBoolean(state, true) + visibleState.setInt(state, 2) + } +} + +internal fun currentApplicationContext(): Context? = runCatching { + Class.forName("android.app.ActivityThread").getMethod("currentApplication").invoke(null) as? Context +}.getOrNull()