From ed5f0c67dbf62ba4c75d241f7162981ae000c83f Mon Sep 17 00:00:00 2001 From: ajp_anton Date: Wed, 2 Sep 2026 22:51:47 +0000 Subject: [PATCH] Separate OneUI lockscreen and AOD filtering --- .../module/OneUiLockscreenBackend.kt | 86 +++++++++++-------- .../module/OneUiLockscreenCardsBackend.kt | 17 ++-- .../module/SamsungAodBackend.kt | 1 + .../res/layout/fragment_app_visibility.xml | 2 +- 4 files changed, 65 insertions(+), 41 deletions(-) diff --git a/app/src/main/java/se/ajpanton/notificationsmaster/module/OneUiLockscreenBackend.kt b/app/src/main/java/se/ajpanton/notificationsmaster/module/OneUiLockscreenBackend.kt index 17ee98b..956f35f 100644 --- a/app/src/main/java/se/ajpanton/notificationsmaster/module/OneUiLockscreenBackend.kt +++ b/app/src/main/java/se/ajpanton/notificationsmaster/module/OneUiLockscreenBackend.kt @@ -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()) + private val replayingLockscreen = ThreadLocal() 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() +} 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 040c67c..4b4e840 100644 --- a/app/src/main/java/se/ajpanton/notificationsmaster/module/OneUiLockscreenCardsBackend.kt +++ b/app/src/main/java/se/ajpanton/notificationsmaster/module/OneUiLockscreenCardsBackend.kt @@ -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 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 9e8dc65..33ab3f0 100644 --- a/app/src/main/java/se/ajpanton/notificationsmaster/module/SamsungAodBackend.kt +++ b/app/src/main/java/se/ajpanton/notificationsmaster/module/SamsungAodBackend.kt @@ -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 diff --git a/app/src/main/res/layout/fragment_app_visibility.xml b/app/src/main/res/layout/fragment_app_visibility.xml index 0a5a6aa..cc5ecaf 100644 --- a/app/src/main/res/layout/fragment_app_visibility.xml +++ b/app/src/main/res/layout/fragment_app_visibility.xml @@ -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" />