Filter OneUI lockscreen notification cards

This commit is contained in:
ajp_anton
2026-08-29 20:13:28 +00:00
parent cab90de5af
commit c831e9adfb
2 changed files with 215 additions and 0 deletions
@@ -25,6 +25,7 @@ class NotificationsMasterModule : XposedModule() {
).firstOrNull { it.install(param.classLoader) } ).firstOrNull { it.install(param.classLoader) }
systemUiBackends = listOfNotNull(unlocked) + listOf( systemUiBackends = listOfNotNull(unlocked) + listOf(
OneUiLockscreenBackend(this), OneUiLockscreenBackend(this),
OneUiLockscreenCardsBackend(this),
).filter { it.install(param.classLoader) } ).filter { it.install(param.classLoader) }
ProcessVisibilityPolicyCache.installWhenReady { ProcessVisibilityPolicyCache.installWhenReady {
systemUiBackends.orEmpty().forEach(VisibilitySurfaceBackend::onPolicyChanged) systemUiBackends.orEmpty().forEach(VisibilitySurfaceBackend::onPolicyChanged)
@@ -0,0 +1,214 @@
package se.ajpanton.notificationsmaster.module
import android.provider.Settings
import android.service.notification.StatusBarNotification
import android.util.Log
import android.view.View
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
/** Hides blocked OneUI notification rows only on the collapsed cards lockscreen. */
internal class OneUiLockscreenCardsBackend(
private val framework: XposedInterface,
) : VisibilitySurfaceBackend {
private val stacks = Collections.newSetFromMap(WeakHashMap<ViewGroup, Boolean>())
private val hiddenRows = WeakHashMap<View, RowState>()
private lateinit var entryForRow: (View) -> Any?
private lateinit var sbnForEntry: (Any) -> StatusBarNotification?
@Volatile private var statusBarStateKnown = false
@Volatile private var statusBarState = SHADE
@Volatile private var dozing = false
@Volatile private var blockedObserved = false
override fun install(classLoader: ClassLoader): Boolean = runCatching {
Class.forName(ONE_UI_LOCKSCREEN_COORDINATOR, false, classLoader)
val stackClass = Class.forName(STACK, false, classLoader)
val rowClass = Class.forName(ROW, false, classLoader)
val stateController = Class.forName(STATE_CONTROLLER, false, classLoader)
prepareNotificationAccess(rowClass)
stackClass.declaredMethods.filter { it.name == "onViewAdded" }.forEach { method ->
method.isAccessible = true
framework.hook(method).intercept { chain ->
val result = chain.proceed()
(chain.thisObject as? ViewGroup)?.let { stack ->
synchronized(stacks) { stacks += stack }
(chain.args.firstOrNull() as? View)?.let(::applyRow)
}
result
}
}
stackClass.declaredMethods.filter { it.name == "onViewRemoved" }.forEach { method ->
method.isAccessible = true
framework.hook(method).intercept { chain ->
(chain.args.firstOrNull() as? View)?.let(::restoreRow)
chain.proceed()
}
}
stackClass.declaredMethods.filter { it.name == "onMeasure" }.forEach { method ->
method.isAccessible = true
framework.hook(method).intercept { chain ->
(chain.thisObject as? ViewGroup)?.let { stack ->
synchronized(stacks) { stacks += stack }
applyStack(stack)
}
chain.proceed()
}
}
stateController.declaredConstructors.forEach { constructor ->
constructor.isAccessible = true
framework.hook(constructor).intercept { chain ->
val result = chain.proceed()
updateState(chain.thisObject)
result
}
}
stateController.declaredMethods.filter {
it.name == "setState" || it.name == "setDozeAmountInternal"
}.forEach { method ->
method.isAccessible = true
framework.hook(method).intercept { chain ->
val result = chain.proceed()
updateState(chain.thisObject)
result
}
}
}.onSuccess {
Log.i(TAG, "Installed OneUI lockscreen-cards notification backend")
}.onFailure {
Log.d(TAG, "OneUI lockscreen-cards boundary is unavailable", it)
}.isSuccess
override fun onPolicyChanged() {
blockedObserved = false
applyTrackedStacks()
}
private fun prepareNotificationAccess(rowClass: Class<*>) {
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)
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)
sbnForEntry = { accessor(it) as? StatusBarNotification }
}
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
statusBarStateKnown = true
}
(findField(controller.javaClass, "mIsDozing")?.get(controller) as? Boolean)?.let { dozing = it }
applyTrackedStacks()
}
private fun applyTrackedStacks() {
synchronized(stacks) { stacks.toList() }.forEach(::applyStack)
}
private fun applyStack(stack: ViewGroup) {
val active = statusBarStateKnown && statusBarState == KEYGUARD && !dozing && isCardsMode(stack)
repeat(stack.childCount) { index ->
val row = stack.getChildAt(index)
if (row.javaClass.name == ROW) {
if (active && isBlocked(row)) hideRow(row) else restoreRow(row)
}
}
}
private fun applyRow(row: View) {
val stack = row.parent as? ViewGroup ?: return
val active = statusBarStateKnown && statusBarState == KEYGUARD && !dozing && isCardsMode(stack)
if (active && isBlocked(row)) hideRow(row) else restoreRow(row)
}
private fun isCardsMode(view: View) = runCatching {
Settings.System.getInt(view.context.contentResolver, LOCKSCREEN_STYLE) == CARDS
}.getOrDefault(false)
private fun isBlocked(row: View): Boolean {
val sbn = runCatching { entryForRow(row)?.let(sbnForEntry) }.getOrNull() ?: return false
return ProcessVisibilityPolicyCache.isBlocked(
VisibilityNotificationExtractor.from(sbn),
NotificationSurface.LOCKSCREEN_COLLAPSED,
).also { blocked ->
if (blocked && !blockedObserved) {
blockedObserved = true
Log.i(TAG, "Filtered a OneUI lockscreen notification card")
}
}
}
private fun hideRow(row: View) {
if (row.visibility == View.GONE && row.alpha == 0f) return
hiddenRows.putIfAbsent(row, RowState(row.visibility, row.alpha))
row.visibility = View.GONE
row.alpha = 0f
}
private fun restoreRow(row: View) {
val state = hiddenRows.remove(row) ?: return
row.visibility = state.visibility
row.alpha = state.alpha
setBooleanField(row, "mWillBeGone", false)
setBooleanField(row, "mInShelf", false)
setBooleanField(row, "mTransformingInShelf", false)
findField(row.javaClass, "mViewState")?.get(row)?.let { viewState ->
setBooleanField(viewState, "hidden", false)
setBooleanField(viewState, "gone", false)
}
row.requestLayout()
(row.parent as? View)?.requestLayout()
}
private fun setBooleanField(target: Any, name: String, value: Boolean) {
findField(target.javaClass, name)?.let { field ->
if (field.get(target) is Boolean) field.setBoolean(target, value)
}
}
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 data class RowState(val visibility: Int, val alpha: Float)
private companion object {
const val TAG = "NotificationsMaster"
const val SHADE = 0
const val KEYGUARD = 1
const val CARDS = 0
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"
const val STATE_CONTROLLER = "com.android.systemui.statusbar.StatusBarStateControllerImpl"
const val ONE_UI_LOCKSCREEN_COORDINATOR =
"com.android.systemui.statusbar.notification.collection.coordinator.LockScreenNotiIconCoordinator"
}
}