Render OneUI AOD Cards notification grid

This commit is contained in:
ajp_anton
2026-08-29 23:22:11 +00:00
parent 626776886a
commit b7d6bb6d63
@@ -2,8 +2,11 @@ package se.ajpanton.notificationsmaster.module
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.provider.Settings
import android.service.notification.StatusBarNotification import android.service.notification.StatusBarNotification
import android.util.Log import android.util.Log
import android.view.View
import android.view.ViewGroup
import io.github.libxposed.api.XposedInterface import io.github.libxposed.api.XposedInterface
import se.ajpanton.notificationsmaster.visibility.NotificationSurface import se.ajpanton.notificationsmaster.visibility.NotificationSurface
import java.lang.reflect.Method import java.lang.reflect.Method
@@ -13,10 +16,16 @@ import java.util.WeakHashMap
/** Filters the notification lists handed to Samsung's AOD renderer. */ /** Filters the notification lists handed to Samsung's AOD renderer. */
internal class SamsungAodBackend(private val framework: XposedInterface) : VisibilitySurfaceBackend { internal class SamsungAodBackend(private val framework: XposedInterface) : VisibilitySurfaceBackend {
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 cardsDozing = Collections.synchronizedMap(WeakHashMap<ViewGroup, Boolean>())
private val cardsRenderer = OneUiCardsGridRenderer()
private val mainHandler = Handler(Looper.getMainLooper())
private val refreshing = ThreadLocal<Boolean>() private val refreshing = ThreadLocal<Boolean>()
private lateinit var updateActive: Method private lateinit var updateActive: Method
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 cardsRefreshPending = false
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)
@@ -27,6 +36,7 @@ internal class SamsungAodBackend(private val framework: XposedInterface) : Visib
List::class.java, List::class.java,
Int::class.javaPrimitiveType!!, Int::class.javaPrimitiveType!!,
) )
installCardsGrid(classLoader)
framework.hook(updateActive).intercept { chain -> framework.hook(updateActive).intercept { chain ->
val raw = notificationList(chain.args[0]) ?: return@intercept chain.proceed() val raw = notificationList(chain.args[0]) ?: return@intercept chain.proceed()
if (refreshing.get() != true) state(chain.thisObject).active = raw if (refreshing.get() != true) state(chain.thisObject).active = raw
@@ -40,9 +50,12 @@ internal class SamsungAodBackend(private val framework: XposedInterface) : Visib
} }
val filteredFirst = filter(first) val filteredFirst = filter(first)
val filteredSecond = filter(second) val filteredSecond = filter(second)
if (filteredFirst === first && filteredSecond === second) chain.proceed() else { visibleNotifications = filteredSecond
val result = if (filteredFirst === first && filteredSecond === second) chain.proceed() else {
chain.proceed(arrayOf(filteredFirst, filteredSecond, chain.args[2])) chain.proceed(arrayOf(filteredFirst, filteredSecond, chain.args[2]))
} }
refreshCardsGrid()
result
} }
}.onSuccess { }.onSuccess {
Log.i(TAG, "Installed Samsung AOD notification backend") Log.i(TAG, "Installed Samsung AOD notification backend")
@@ -52,7 +65,7 @@ internal class SamsungAodBackend(private val framework: XposedInterface) : Visib
override fun onPolicyChanged() { override fun onPolicyChanged() {
blockedObserved = false blockedObserved = false
Handler(Looper.getMainLooper()).post { mainHandler.post {
val current = synchronized(managers) { managers.entries.map { it.key to it.value.copy() } } val current = synchronized(managers) { managers.entries.map { it.key to it.value.copy() } }
refreshing.set(true) refreshing.set(true)
try { try {
@@ -67,9 +80,104 @@ internal class SamsungAodBackend(private val framework: XposedInterface) : Visib
} finally { } finally {
refreshing.remove() refreshing.remove()
} }
refreshCardsGrid()
} }
} }
private fun installCardsGrid(classLoader: ClassLoader) {
runCatching {
val container = Class.forName(AOD_ICON_CONTAINER, false, classLoader)
container.declaredConstructors.forEach { constructor ->
constructor.isAccessible = true
framework.hook(constructor).intercept { chain ->
val result = chain.proceed()
(chain.thisObject as? ViewGroup)?.let(::trackCardsContainer)
result
}
}
container.declaredMethods.filter {
it.name == "setDozing" && it.parameterCount == 1 &&
it.parameterTypes[0] == Boolean::class.javaPrimitiveType
}.forEach { method ->
method.isAccessible = true
framework.hook(method).intercept { chain ->
val result = chain.proceed()
(chain.thisObject as? ViewGroup)?.let { view ->
cardsDozing[view] = chain.args.firstOrNull() == true
refreshCardsGrid()
}
result
}
}
container.declaredMethods.filter {
(it.name == "setIconColor" || it.name == "setIconSize") && it.parameterCount == 1
}.forEach { method ->
method.isAccessible = true
framework.hook(method).intercept { chain ->
val result = chain.proceed()
refreshCardsGrid()
result
}
}
}.onFailure { error ->
Log.d(TAG, "Samsung AOD Cards grid boundary is unavailable", error)
}
}
private fun trackCardsContainer(container: ViewGroup) {
synchronized(cardsContainers) {
if (!cardsContainers.add(container)) return
}
container.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(view: View) = refreshCardsGrid()
override fun onViewDetachedFromWindow(view: View) {
cardsDozing[container] = false
cardsRenderer.release(container)
}
})
container.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> refreshCardsGrid() }
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 ->
OneUiCardsGridRenderer.Source(
sbn.key,
view = null,
sbn.notification.smallIcon,
sbn.packageName,
tint,
)
}
cardsRenderer.updateSources(container, sources)
cardsRenderer.refresh(
container,
container.isAttachedToWindow && cardsDozing[container] == true && isCardsMode(container),
emptySet(),
config,
)
}
}
}
private fun isCardsMode(view: View) = runCatching {
Settings.System.getInt(view.context.contentResolver, NOTIFICATION_STYLE) == CARDS
}.getOrDefault(false)
private fun proceedFiltered( private fun proceedFiltered(
chain: XposedInterface.Chain, chain: XposedInterface.Chain,
argument: Int, argument: Int,
@@ -124,6 +232,10 @@ 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 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 =
"com.samsung.android.uniform.widget.notification.NotificationIconsOnlyContainer"
} }
} }