Separate OneUI lockscreen and AOD filtering

This commit is contained in:
ajp_anton
2026-09-03 00:30:37 +00:00
parent b1458b13e3
commit ed5f0c67db
4 changed files with 65 additions and 41 deletions
@@ -2,7 +2,6 @@ package se.ajpanton.notificationsmaster.module
import android.os.Handler
import android.os.Looper
import android.os.Message
import android.service.notification.StatusBarNotification
import android.util.Log
import io.github.libxposed.api.XposedInterface
@@ -15,12 +14,13 @@ import java.util.WeakHashMap
/** Filters OneUI's collapsed lockscreen projection without changing notification rows. */
internal class OneUiLockscreenBackend(private val framework: XposedInterface) : VisibilitySurfaceBackend {
private val coordinators = Collections.newSetFromMap(WeakHashMap<Any, Boolean>())
private val replayingLockscreen = ThreadLocal<Boolean>()
private lateinit var refresh: Method
@Volatile private var blockedObserved = false
override fun install(classLoader: ClassLoader): Boolean = runCatching {
val coordinator = Class.forName(COORDINATOR, false, classLoader)
val managerHandler = Class.forName(MANAGER_HANDLER, false, classLoader)
val iconsController = Class.forName(ICONS_CONTROLLER, false, classLoader)
val info = Class.forName(NOTIFICATION_INFO, false, classLoader)
val sbn = info.getDeclaredField("mSbn").also {
require(it.type == StatusBarNotification::class.java)
@@ -33,44 +33,39 @@ internal class OneUiLockscreenBackend(private val framework: XposedInterface) :
it.name == "onLockScreenNotiStateChanged" &&
it.parameterTypes.isEmpty() && it.returnType == Void.TYPE
} ?: error("OneUI lockscreen refresh method not found")
val handle = managerHandler.declaredMethods.singleOrNull {
it.name == "handleMessage" &&
it.parameterTypes.contentEquals(arrayOf(Message::class.java)) && it.returnType == Void.TYPE
} ?: error("OneUI lockscreen update handler not found")
val updateIcons = iconsController.declaredMethods.singleOrNull {
it.name == "onNotificationInfoUpdated" &&
it.parameterTypes.contentEquals(arrayOf(ArrayList::class.java)) && it.returnType == Void.TYPE
} ?: error("OneUI lockscreen icon update method not found")
attach.isAccessible = true
refresh.isAccessible = true
handle.isAccessible = true
updateIcons.isAccessible = true
framework.hook(attach).intercept { chain ->
synchronized(coordinators) { coordinators += chain.thisObject }
chain.proceed()
}
framework.hook(handle).intercept { chain ->
val message = chain.args[0] as? Message ?: return@intercept chain.proceed()
if (message.what != NOTIFICATION_INFO_UPDATED) return@intercept chain.proceed()
val original = message.obj as? ArrayList<*> ?: return@intercept chain.proceed()
framework.hook(updateIcons).intercept { chain ->
if (replayingLockscreen.get() == true) return@intercept chain.proceed()
val original = chain.args[0] as? ArrayList<*> ?: return@intercept chain.proceed()
if (original.any { it != null && !info.isInstance(it) }) return@intercept chain.proceed()
val filtered = original.filterTo(ArrayList(original.size)) { item ->
val notification = item?.let {
runCatching {
VisibilityNotificationExtractor.from(sbn.get(it) as StatusBarNotification)
}.getOrNull()
val aod = filter(original, sbn, NotificationSurface.AOD)
val lockscreen = filter(original, sbn, NotificationSurface.LOCKSCREEN_COLLAPSED)
// OneUI builds AOD icons and lockscreen state in this same callback. Let
// AOD consume its list, then replay only the lockscreen half.
val result = chain.proceed(arrayOf(aod))
if (lockscreen != aod) {
replayingLockscreen.set(true)
OneUiSurfaceDispatch.suppressAodIconUpdate.set(true)
try {
updateIcons.invoke(chain.thisObject, lockscreen)
} catch (error: ReflectiveOperationException) {
throw error.cause ?: error
} finally {
OneUiSurfaceDispatch.suppressAodIconUpdate.remove()
replayingLockscreen.remove()
}
notification == null || !ProcessVisibilityPolicyCache.isBlocked(
notification,
NotificationSurface.LOCKSCREEN_COLLAPSED,
)
}
if (filtered.size == original.size) return@intercept chain.proceed()
if (!blockedObserved) {
blockedObserved = true
Log.i(TAG, "Filtered a OneUI collapsed-lockscreen notification")
}
message.obj = filtered
try {
chain.proceed()
} finally {
message.obj = original
}
result
}
}.onSuccess {
Log.i(TAG, "Installed OneUI collapsed-lockscreen notification backend")
@@ -89,13 +84,36 @@ internal class OneUiLockscreenBackend(private val framework: XposedInterface) :
}
}
private fun filter(
original: ArrayList<*>,
sbn: java.lang.reflect.Field,
surface: NotificationSurface,
): ArrayList<*> {
val filtered = original.filterTo(ArrayList(original.size)) { item ->
val notification = item?.let {
runCatching {
VisibilityNotificationExtractor.from(sbn.get(it) as StatusBarNotification)
}.getOrNull()
}
notification == null || !ProcessVisibilityPolicyCache.isBlocked(notification, surface)
}
if (filtered.size < original.size && !blockedObserved) {
blockedObserved = true
Log.i(TAG, "Filtered a OneUI ${surface.name.lowercase()} notification")
}
return filtered
}
private companion object {
const val TAG = "NotificationsMaster"
const val NOTIFICATION_INFO_UPDATED = 101
const val COORDINATOR =
"com.android.systemui.statusbar.notification.collection.coordinator.LockScreenNotiIconCoordinator"
const val MANAGER_HANDLER =
"com.android.systemui.statusbar.LockscreenNotificationManager\$LockscreenNotificationMgrHandler"
const val ICONS_CONTROLLER =
"com.android.systemui.statusbar.iconsOnly.LockscreenNotificationIconsOnlyController"
const val NOTIFICATION_INFO = "com.android.systemui.statusbar.LockscreenNotificationInfo"
}
}
internal object OneUiSurfaceDispatch {
val suppressAodIconUpdate = ThreadLocal<Boolean>()
}
@@ -148,7 +148,7 @@ internal class OneUiLockscreenCardsBackend(
calculate.isAccessible = true
framework.hook(calculate).intercept { chain ->
val container = chain.thisObject as? ViewGroup ?: return@intercept chain.proceed()
if (isDozeTransition() && isCardsMode(container)) {
if (isDozeActive() && isCardsMode(container)) {
gridRenderer.suspend(container, true)
}
if (!shouldFilterLockscreenCards(container)) return@intercept chain.proceed()
@@ -209,7 +209,7 @@ internal class OneUiLockscreenCardsBackend(
if (active) {
gridRenderer.refresh(container, true, visibleCardKeys(container), gridConfig())
} else {
gridRenderer.suspend(container, isDozeTransition() && isCardsMode(container))
gridRenderer.suspend(container, isDozeActive() && isCardsMode(container))
}
result
}
@@ -228,6 +228,9 @@ internal class OneUiLockscreenCardsBackend(
statusBarStateKnown = true
}
(findField(controller.javaClass, "mIsDozing")?.get(controller) as? Boolean)?.let { dozing = it }
(findField(controller.javaClass, "mDozeAmount")?.get(controller) as? Number)?.let {
dozeAmount = it.toFloat().coerceIn(0f, 1f)
}
applyTrackedStacks()
}
@@ -237,7 +240,7 @@ internal class OneUiLockscreenCardsBackend(
::isLockscreenCards,
::visibleCardKeys,
gridConfig(),
) { isDozeTransition() && isCardsMode(it) }
) { isDozeActive() && isCardsMode(it) }
}
private fun applyStack(stack: ViewGroup) {
@@ -257,12 +260,14 @@ internal class OneUiLockscreenCardsBackend(
}
private fun isLockscreenCards(view: View) =
shouldFilterLockscreenCards(view) && !isDozeTransition()
shouldFilterLockscreenCards(view) && !isDozeActive()
private fun shouldFilterLockscreenCards(view: View) =
statusBarStateKnown && statusBarState == KEYGUARD && isCardsMode(view)
statusBarStateKnown && statusBarState == KEYGUARD && !isSteadyAod() && isCardsMode(view)
private fun isDozeTransition() = dozing || dozeAmount > 0f
private fun isDozeActive() = dozing || dozeAmount > 0f
private fun isSteadyAod() = dozing && dozeAmount >= 1f
private fun isCardsMode(view: View) = runCatching {
Settings.System.getInt(view.context.contentResolver, LOCKSCREEN_STYLE) == CARDS
@@ -61,6 +61,7 @@ internal class SamsungAodBackend(private val framework: XposedInterface) : Visib
result
}
framework.hook(updateIcons).intercept { chain ->
if (OneUiSurfaceDispatch.suppressAodIconUpdate.get() == true) return@intercept null
val icons = imageViewList(chain.args[0]) ?: return@intercept chain.proceed()
val result = chain.proceed()
notificationIconViews = icons
@@ -43,7 +43,7 @@
android:id="@+id/show_lockscreen"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Collapsed lockscreen" />
android:text="Lockscreen" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/show_unlocked"