Stabilize OneUI Cards mode transitions

This commit is contained in:
ajp_anton
2026-08-30 01:42:23 +00:00
parent 7331ac8f01
commit 0a0e7c7649
3 changed files with 109 additions and 39 deletions
@@ -46,6 +46,17 @@ internal class OneUiCardsGridRenderer {
} }
} }
fun deactivate(container: ViewGroup) {
states[container]?.let { state ->
state.active = false
failOpen(state)
}
}
fun deactivateAll() {
states.keys.toList().forEach(::deactivate)
}
fun release(container: ViewGroup) { fun release(container: ViewGroup) {
val state = states.remove(container) ?: return val state = states.remove(container) ?: return
restoreNative(state) restoreNative(state)
@@ -84,6 +84,10 @@ internal class OneUiLockscreenCardsBackend(
}.forEach { method -> }.forEach { method ->
method.isAccessible = true method.isAccessible = true
framework.hook(method).intercept { chain -> framework.hook(method).intercept { chain ->
val leavingLockscreen = method.name == "setState" && chain.args.firstOrNull() != KEYGUARD
val enteringDoze = method.name == "setDozeAmountInternal" &&
((chain.args.firstOrNull() as? Number)?.toFloat() ?: 0f) > 0f
if (leavingLockscreen || enteringDoze) gridRenderer.deactivateAll()
val result = chain.proceed() val result = chain.proceed()
updateState(chain.thisObject) updateState(chain.thisObject)
result result
@@ -206,6 +210,7 @@ internal class OneUiLockscreenCardsBackend(
statusBarStateKnown = true statusBarStateKnown = true
} }
(findField(controller.javaClass, "mIsDozing")?.get(controller) as? Boolean)?.let { dozing = it } (findField(controller.javaClass, "mIsDozing")?.get(controller) as? Boolean)?.let { dozing = it }
if (dozing || statusBarState != KEYGUARD) gridRenderer.deactivateAll()
applyTrackedStacks() applyTrackedStacks()
} }
@@ -3,6 +3,7 @@ package se.ajpanton.notificationsmaster.module
import android.graphics.drawable.Icon import android.graphics.drawable.Icon
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.os.SystemClock
import android.provider.Settings import android.provider.Settings
import android.service.notification.StatusBarNotification import android.service.notification.StatusBarNotification
import android.util.Log import android.util.Log
@@ -19,6 +20,8 @@ internal class SamsungAodBackend(private val framework: XposedInterface) : Visib
private val managers = Collections.synchronizedMap(WeakHashMap<Any, State>()) private val managers = Collections.synchronizedMap(WeakHashMap<Any, State>())
private val cardsContainers = Collections.newSetFromMap(WeakHashMap<ViewGroup, Boolean>()) private val cardsContainers = Collections.newSetFromMap(WeakHashMap<ViewGroup, Boolean>())
private val cardsDozing = Collections.synchronizedMap(WeakHashMap<ViewGroup, Boolean>()) private val cardsDozing = Collections.synchronizedMap(WeakHashMap<ViewGroup, Boolean>())
private val cardsReadyAt = Collections.synchronizedMap(WeakHashMap<ViewGroup, Long>())
private val cardsLocations = Collections.synchronizedMap(WeakHashMap<ViewGroup, Pair<Int, Int>>())
private val cardsRenderer = OneUiCardsGridRenderer() private val cardsRenderer = OneUiCardsGridRenderer()
private val mainHandler = Handler(Looper.getMainLooper()) private val mainHandler = Handler(Looper.getMainLooper())
private val refreshing = ThreadLocal<Boolean>() private val refreshing = ThreadLocal<Boolean>()
@@ -26,7 +29,7 @@ internal class SamsungAodBackend(private val framework: XposedInterface) : Visib
private lateinit var updateVisible: Method private lateinit var updateVisible: Method
@Volatile private var blockedObserved = false @Volatile private var blockedObserved = false
@Volatile private var visibleNotifications = emptyList<StatusBarNotification>() @Volatile private var visibleNotifications = emptyList<StatusBarNotification>()
@Volatile private var cardsRefreshPending = false private var cardsRefreshTask: Runnable? = null
override fun install(classLoader: ClassLoader): Boolean = runCatching { override fun install(classLoader: ClassLoader): Boolean = runCatching {
val manager = Class.forName(MANAGER, false, classLoader) val manager = Class.forName(MANAGER, false, classLoader)
@@ -102,12 +105,22 @@ internal class SamsungAodBackend(private val framework: XposedInterface) : Visib
}.forEach { method -> }.forEach { method ->
method.isAccessible = true method.isAccessible = true
framework.hook(method).intercept { chain -> framework.hook(method).intercept { chain ->
val container = chain.thisObject as? ViewGroup
val dozing = chain.args.firstOrNull() == true
container?.let {
trackCardsContainer(it)
cardsDozing[it] = false
cardsRenderer.deactivate(it)
}
val result = chain.proceed() val result = chain.proceed()
(chain.thisObject as? ViewGroup)?.let { view -> container?.let { view ->
trackCardsContainer(view) cardsDozing[view] = dozing
cardsDozing[view] = chain.args.firstOrNull() == true if (dozing) settleCardsGrid(view, DOZE_SETTLE_DELAY) else {
cardsReadyAt.remove(view)
cardsLocations.remove(view)
refreshCardsGrid() refreshCardsGrid()
} }
}
result result
} }
} }
@@ -132,21 +145,55 @@ internal class SamsungAodBackend(private val framework: XposedInterface) : Visib
if (!cardsContainers.add(container)) return if (!cardsContainers.add(container)) return
} }
container.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener { container.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(view: View) = refreshCardsGrid() override fun onViewAttachedToWindow(view: View) {
if (cardsDozing[container] == true) settleCardsGrid(container) else refreshCardsGrid()
}
override fun onViewDetachedFromWindow(view: View) { override fun onViewDetachedFromWindow(view: View) {
cardsDozing[container] = false cardsDozing[container] = false
cardsReadyAt.remove(container)
cardsLocations.remove(container)
cardsRenderer.release(container) cardsRenderer.release(container)
} }
}) })
container.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> refreshCardsGrid() } container.addOnLayoutChangeListener { _, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom ->
if (left != oldLeft || top != oldTop || right != oldRight || bottom != oldBottom) {
if (cardsDozing[container] == true) settleCardsGrid(container) else refreshCardsGrid()
}
}
refreshCardsGrid()
}
private fun settleCardsGrid(container: ViewGroup, delay: Long = LAYOUT_SETTLE_DELAY) {
cardsRenderer.deactivate(container)
cardsReadyAt[container] = SystemClock.uptimeMillis() + delay
refreshCardsGrid() refreshCardsGrid()
} }
private fun refreshCardsGrid() { private fun refreshCardsGrid() {
if (cardsRefreshPending) return cardsRefreshTask?.let(mainHandler::removeCallbacks)
cardsRefreshPending = true val now = SystemClock.uptimeMillis()
mainHandler.post { val delay = synchronized(cardsReadyAt) {
cardsRefreshPending = false cardsReadyAt.entries
.filter { cardsDozing[it.key] == true }
.maxOfOrNull { (it.value - now).coerceAtLeast(0) } ?: 0
}
cardsRefreshTask = Runnable {
cardsRefreshTask = null
renderCardsGrid()
}.also { mainHandler.postDelayed(it, delay) }
}
private fun renderCardsGrid() {
val moving = synchronized(cardsContainers) { cardsContainers.toList() }.filter { container ->
if (cardsDozing[container] != true) return@filter false
val location = screenLocation(container)
val previous = cardsLocations.put(container, location)
previous == null || previous != location
}
if (moving.isNotEmpty()) {
moving.forEach { settleCardsGrid(it) }
return
}
val notifications = visibleNotifications.distinctBy(StatusBarNotification::getKey) val notifications = visibleNotifications.distinctBy(StatusBarNotification::getKey)
val config = OneUiCardsGridRenderer.Config( val config = OneUiCardsGridRenderer.Config(
ProcessVisibilityPolicyCache.aodCards, ProcessVisibilityPolicyCache.aodCards,
@@ -178,6 +225,11 @@ internal class SamsungAodBackend(private val framework: XposedInterface) : Visib
) )
} }
} }
private fun screenLocation(view: View): Pair<Int, Int> {
val location = IntArray(2)
view.getLocationOnScreen(location)
return location[0] to location[1]
} }
private fun isCardsMode(view: View) = runCatching { private fun isCardsMode(view: View) = runCatching {
@@ -239,6 +291,8 @@ internal class SamsungAodBackend(private val framework: XposedInterface) : Visib
private companion object { private companion object {
const val TAG = "NotificationsMaster" const val TAG = "NotificationsMaster"
const val CARDS = 0 const val CARDS = 0
const val LAYOUT_SETTLE_DELAY = 120L
const val DOZE_SETTLE_DELAY = 220L
const val NOTIFICATION_STYLE = "lockscreen_minimizing_notification" const val NOTIFICATION_STYLE = "lockscreen_minimizing_notification"
const val MANAGER = "com.samsung.android.uniform.plugins.notification.AODNotificationManager" const val MANAGER = "com.samsung.android.uniform.plugins.notification.AODNotificationManager"
const val AOD_ICON_CONTAINER = const val AOD_ICON_CONTAINER =