Trim SystemUI rendering infrastructure

This commit is contained in:
ajp_anton
2026-09-03 00:55:12 +00:00
parent 1d2144e72c
commit eef433ce88
7 changed files with 77 additions and 143 deletions
@@ -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
}
}
@@ -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)
}
scheduleRender(container, state)
}
@@ -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"
@@ -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<Method> = 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
@@ -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)
@@ -170,26 +170,26 @@ internal class SamsungAodBackend(private val framework: XposedInterface) : Visib
}
private fun beginDozeTransition(container: ViewGroup, origin: Pair<Int, Int>) {
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<Int, Int>,
current: Pair<Int, Int>,
moved: Boolean,
) {
val now = SystemClock.uptimeMillis()
cardsTransitions[container] = CardsTransition(
origin,
current,
current,
moved = true,
moved,
stableSince = now,
deadline = now + TRANSITION_FALLBACK_DELAY,
)
@@ -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()