diff --git a/app/src/main/java/se/ajpanton/notificationsmaster/module/OneUiCardsGridRenderer.kt b/app/src/main/java/se/ajpanton/notificationsmaster/module/OneUiCardsGridRenderer.kt index 9c17666..ba886fa 100644 --- a/app/src/main/java/se/ajpanton/notificationsmaster/module/OneUiCardsGridRenderer.kt +++ b/app/src/main/java/se/ajpanton/notificationsmaster/module/OneUiCardsGridRenderer.kt @@ -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) 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 e701dc2..0f02f6e 100644 --- a/app/src/main/java/se/ajpanton/notificationsmaster/module/OneUiLockscreenCardsBackend.kt +++ b/app/src/main/java/se/ajpanton/notificationsmaster/module/OneUiLockscreenCardsBackend.kt @@ -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() } 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 4403ff9..cad8377 100644 --- a/app/src/main/java/se/ajpanton/notificationsmaster/module/SamsungAodBackend.kt +++ b/app/src/main/java/se/ajpanton/notificationsmaster/module/SamsungAodBackend.kt @@ -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()) private val cardsContainers = Collections.newSetFromMap(WeakHashMap()) private val cardsDozing = Collections.synchronizedMap(WeakHashMap()) + private val cardsReadyAt = Collections.synchronizedMap(WeakHashMap()) + private val cardsLocations = Collections.synchronizedMap(WeakHashMap>()) private val cardsRenderer = OneUiCardsGridRenderer() private val mainHandler = Handler(Looper.getMainLooper()) private val refreshing = ThreadLocal() @@ -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() - @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 { + 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 =