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) {
val state = states.remove(container) ?: return
restoreNative(state)
@@ -84,6 +84,10 @@ internal class OneUiLockscreenCardsBackend(
}.forEach { method ->
method.isAccessible = true
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()
updateState(chain.thisObject)
result
@@ -206,6 +210,7 @@ internal class OneUiLockscreenCardsBackend(
statusBarStateKnown = true
}
(findField(controller.javaClass, "mIsDozing")?.get(controller) as? Boolean)?.let { dozing = it }
if (dozing || statusBarState != KEYGUARD) gridRenderer.deactivateAll()
applyTrackedStacks()
}
@@ -3,6 +3,7 @@ package se.ajpanton.notificationsmaster.module
import android.graphics.drawable.Icon
import android.os.Handler
import android.os.Looper
import android.os.SystemClock
import android.provider.Settings
import android.service.notification.StatusBarNotification
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 cardsContainers = Collections.newSetFromMap(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 mainHandler = Handler(Looper.getMainLooper())
private val refreshing = ThreadLocal<Boolean>()
@@ -26,7 +29,7 @@ internal class SamsungAodBackend(private val framework: XposedInterface) : Visib
private lateinit var updateVisible: Method
@Volatile private var blockedObserved = false
@Volatile private var visibleNotifications = emptyList<StatusBarNotification>()
@Volatile private var cardsRefreshPending = false
private var cardsRefreshTask: Runnable? = null
override fun install(classLoader: ClassLoader): Boolean = runCatching {
val manager = Class.forName(MANAGER, false, classLoader)
@@ -102,11 +105,21 @@ internal class SamsungAodBackend(private val framework: XposedInterface) : Visib
}.forEach { method ->
method.isAccessible = true
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()
(chain.thisObject as? ViewGroup)?.let { view ->
trackCardsContainer(view)
cardsDozing[view] = chain.args.firstOrNull() == true
refreshCardsGrid()
container?.let { view ->
cardsDozing[view] = dozing
if (dozing) settleCardsGrid(view, DOZE_SETTLE_DELAY) else {
cardsReadyAt.remove(view)
cardsLocations.remove(view)
refreshCardsGrid()
}
}
result
}
@@ -132,54 +145,93 @@ internal class SamsungAodBackend(private val framework: XposedInterface) : Visib
if (!cardsContainers.add(container)) return
}
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) {
cardsDozing[container] = false
cardsReadyAt.remove(container)
cardsLocations.remove(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()
}
private fun refreshCardsGrid() {
if (cardsRefreshPending) return
cardsRefreshPending = true
mainHandler.post {
cardsRefreshPending = false
val notifications = visibleNotifications.distinctBy(StatusBarNotification::getKey)
val config = OneUiCardsGridRenderer.Config(
ProcessVisibilityPolicyCache.aodCards,
showPills = false,
hideShelf = false,
)
synchronized(cardsContainers) { cardsContainers.toList() }.forEach { container ->
val tint = runCatching {
container.javaClass.getMethod("getIconColor").invoke(container) as Int
}.getOrNull()
val sources = notifications.map { sbn ->
val icon = sbn.notification.smallIcon
OneUiCardsGridRenderer.Source(
sbn.key,
view = null,
icon,
sbn.packageName,
tint.takeUnless {
icon.type == Icon.TYPE_BITMAP || icon.type == Icon.TYPE_ADAPTIVE_BITMAP
},
)
}
cardsRenderer.updateSources(container, sources)
cardsRenderer.refresh(
container,
container.isAttachedToWindow && cardsDozing[container] == true && isCardsMode(container),
emptySet(),
config,
cardsRefreshTask?.let(mainHandler::removeCallbacks)
val now = SystemClock.uptimeMillis()
val delay = synchronized(cardsReadyAt) {
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 config = OneUiCardsGridRenderer.Config(
ProcessVisibilityPolicyCache.aodCards,
showPills = false,
hideShelf = false,
)
synchronized(cardsContainers) { cardsContainers.toList() }.forEach { container ->
val tint = runCatching {
container.javaClass.getMethod("getIconColor").invoke(container) as Int
}.getOrNull()
val sources = notifications.map { sbn ->
val icon = sbn.notification.smallIcon
OneUiCardsGridRenderer.Source(
sbn.key,
view = null,
icon,
sbn.packageName,
tint.takeUnless {
icon.type == Icon.TYPE_BITMAP || icon.type == Icon.TYPE_ADAPTIVE_BITMAP
},
)
}
cardsRenderer.updateSources(container, sources)
cardsRenderer.refresh(
container,
container.isAttachedToWindow && cardsDozing[container] == true && isCardsMode(container),
emptySet(),
config,
)
}
}
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 {
Settings.System.getInt(view.context.contentResolver, NOTIFICATION_STYLE) == CARDS
}.getOrDefault(false)
@@ -239,6 +291,8 @@ internal class SamsungAodBackend(private val framework: XposedInterface) : Visib
private companion object {
const val TAG = "NotificationsMaster"
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 MANAGER = "com.samsung.android.uniform.plugins.notification.AODNotificationManager"
const val AOD_ICON_CONTAINER =