fix: stabilize OneUI AOD notification grids
This commit is contained in:
@@ -39,7 +39,7 @@ internal class OneUiCardsGridRenderer {
|
||||
val native = if (config.hideShelf) findNativeSurface(container) else container
|
||||
if (native !== container.rootView) hideNative(state, native)
|
||||
}
|
||||
container.post { render(container, state, state.active) }
|
||||
scheduleRender(container, state)
|
||||
}
|
||||
|
||||
fun refreshAll(
|
||||
@@ -69,6 +69,7 @@ internal class OneUiCardsGridRenderer {
|
||||
fun suspend(container: ViewGroup, suppressNative: Boolean, nativeSurface: View? = null) {
|
||||
val state = state(container)
|
||||
state.active = false
|
||||
cancelRender(container, state)
|
||||
state.host?.visibility = View.GONE
|
||||
if (suppressNative) {
|
||||
val native = nativeSurface
|
||||
@@ -84,6 +85,7 @@ internal class OneUiCardsGridRenderer {
|
||||
restoreNative(state)
|
||||
state.attachListener?.let(container::removeOnAttachStateChangeListener)
|
||||
removePreDrawListener(state)
|
||||
cancelRender(container, state)
|
||||
state.layoutListener?.let { state.root?.removeOnLayoutChangeListener(it) }
|
||||
state.host?.let { host ->
|
||||
clearHost(host)
|
||||
@@ -91,11 +93,24 @@ internal class OneUiCardsGridRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
private fun render(container: ViewGroup, state: State, active: Boolean) {
|
||||
if (!active || !container.isAttachedToWindow) {
|
||||
failOpen(state)
|
||||
return
|
||||
private fun scheduleRender(container: ViewGroup, state: State) {
|
||||
if (state.renderTask != null) return
|
||||
val task = Runnable {
|
||||
state.renderTask = null
|
||||
if (states[container] === state) render(container, state)
|
||||
}
|
||||
state.renderTask = task
|
||||
if (!container.post(task)) state.renderTask = null
|
||||
}
|
||||
|
||||
private fun cancelRender(container: ViewGroup, state: State) {
|
||||
state.renderTask?.let(container::removeCallbacks)
|
||||
state.renderTask = null
|
||||
}
|
||||
|
||||
private fun render(container: ViewGroup, state: State) {
|
||||
if (!state.active) return
|
||||
if (!container.isAttachedToWindow) return failOpen(state)
|
||||
val root = container.rootView as? ViewGroup ?: return failOpen(state)
|
||||
if (state.config.settings.maxRows == 0) {
|
||||
state.host?.let {
|
||||
@@ -128,6 +143,10 @@ internal class OneUiCardsGridRenderer {
|
||||
|
||||
clearHost(host)
|
||||
host.visibility = View.VISIBLE
|
||||
host.alpha = 1f
|
||||
host.translationX = 0f
|
||||
host.translationY = 0f
|
||||
state.anchorLocation = location
|
||||
val rowGap = dp(root, 8)
|
||||
val nativeSurface = if (state.config.hideShelf) findNativeSurface(container) else container
|
||||
if (nativeSurface === root) return failOpen(state)
|
||||
@@ -152,6 +171,7 @@ internal class OneUiCardsGridRenderer {
|
||||
}
|
||||
}
|
||||
hideNative(state, nativeSurface)
|
||||
syncSourceMovement(container, state)
|
||||
val diagnosticSignature = listOf(remaining.size, state.visibleCardKeys.size, rows.visibleSlots,
|
||||
settings.maxRows, settings.maxIconsPerRow, iconSize).hashCode()
|
||||
if (diagnosticSignature != state.lastDiagnosticSignature) {
|
||||
@@ -294,7 +314,12 @@ internal class OneUiCardsGridRenderer {
|
||||
|
||||
private fun failOpen(state: State) {
|
||||
restoreNative(state)
|
||||
state.host?.visibility = View.GONE
|
||||
state.host?.let {
|
||||
it.visibility = View.GONE
|
||||
it.translationX = 0f
|
||||
it.translationY = 0f
|
||||
}
|
||||
state.anchorLocation = null
|
||||
}
|
||||
|
||||
private fun hideNative(state: State, native: View) {
|
||||
@@ -328,6 +353,7 @@ internal class OneUiCardsGridRenderer {
|
||||
if (state.nativeSuppressed) state.nativeSurface?.let { native ->
|
||||
if (native.alpha != 0f) native.alpha = 0f
|
||||
}
|
||||
if (state.active && state.config.followSourceMovement) syncSourceMovement(container, state)
|
||||
true
|
||||
}
|
||||
observer.addOnPreDrawListener(listener)
|
||||
@@ -367,6 +393,32 @@ internal class OneUiCardsGridRenderer {
|
||||
|
||||
private fun dp(view: View, value: Int) = (value * view.resources.displayMetrics.density).toInt()
|
||||
|
||||
private fun syncSourceMovement(container: ViewGroup, state: State) {
|
||||
val root = state.root ?: return
|
||||
val host = state.host ?: return
|
||||
val anchor = state.anchorLocation ?: return
|
||||
container.getLocationOnScreen(state.sourceLocation)
|
||||
root.getLocationOnScreen(state.rootLocation)
|
||||
val x = (state.sourceLocation[0] - state.rootLocation[0] - anchor.first).toFloat()
|
||||
val y = (state.sourceLocation[1] - state.rootLocation[1] - anchor.second).toFloat()
|
||||
val alpha = effectiveAncestorAlpha(container, root)
|
||||
if (host.translationX != x) host.translationX = x
|
||||
if (host.translationY != y) host.translationY = y
|
||||
if (host.alpha != alpha) host.alpha = alpha
|
||||
}
|
||||
|
||||
private fun effectiveAncestorAlpha(source: View, root: View): Float {
|
||||
if (source.visibility != View.VISIBLE) return 0f
|
||||
var alpha = 1f
|
||||
var current = source.parent as? View
|
||||
while (current != null && current !== root) {
|
||||
if (current.visibility != View.VISIBLE) return 0f
|
||||
alpha *= current.alpha
|
||||
current = current.parent as? View
|
||||
}
|
||||
return alpha
|
||||
}
|
||||
|
||||
private fun state(container: ViewGroup) = states.getOrPut(container) {
|
||||
State().also { state ->
|
||||
state.attachListener = object : View.OnAttachStateChangeListener {
|
||||
@@ -389,6 +441,7 @@ internal class OneUiCardsGridRenderer {
|
||||
val settings: CardsGridSettings,
|
||||
val showPills: Boolean,
|
||||
val hideShelf: Boolean,
|
||||
val followSourceMovement: Boolean = false,
|
||||
)
|
||||
|
||||
private class State {
|
||||
@@ -403,6 +456,10 @@ internal class OneUiCardsGridRenderer {
|
||||
var nativeSuppressed = false
|
||||
var preDrawObserver: ViewTreeObserver? = null
|
||||
var preDrawListener: ViewTreeObserver.OnPreDrawListener? = null
|
||||
var renderTask: Runnable? = null
|
||||
var anchorLocation: Pair<Int, Int>? = null
|
||||
val sourceLocation = IntArray(2)
|
||||
val rootLocation = IntArray(2)
|
||||
var lastDiagnosticSignature = 0
|
||||
var config = Config(CardsGridSettings.LOCKSCREEN_DEFAULT, showPills = true, hideShelf = true)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package se.ajpanton.notificationsmaster.module
|
||||
|
||||
import android.graphics.drawable.Icon
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.SystemClock
|
||||
@@ -9,6 +8,7 @@ import android.service.notification.StatusBarNotification
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import io.github.libxposed.api.XposedInterface
|
||||
import se.ajpanton.notificationsmaster.visibility.NotificationSurface
|
||||
import java.lang.reflect.Method
|
||||
@@ -21,14 +21,13 @@ internal class SamsungAodBackend(private val framework: XposedInterface) : Visib
|
||||
private val cardsContainers = Collections.newSetFromMap(WeakHashMap<ViewGroup, Boolean>())
|
||||
private val cardsDozing = Collections.synchronizedMap(WeakHashMap<ViewGroup, Boolean>())
|
||||
private val cardsTransitions = Collections.synchronizedMap(WeakHashMap<ViewGroup, CardsTransition>())
|
||||
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>()
|
||||
private lateinit var updateActive: Method
|
||||
private lateinit var updateVisible: Method
|
||||
@Volatile private var blockedObserved = false
|
||||
@Volatile private var visibleNotifications = emptyList<StatusBarNotification>()
|
||||
@Volatile private var notificationIconViews = emptyList<ImageView>()
|
||||
private var cardsRefreshTask: Runnable? = null
|
||||
|
||||
override fun install(classLoader: ClassLoader): Boolean = runCatching {
|
||||
@@ -40,6 +39,7 @@ internal class SamsungAodBackend(private val framework: XposedInterface) : Visib
|
||||
List::class.java,
|
||||
Int::class.javaPrimitiveType!!,
|
||||
)
|
||||
val updateIcons = manager.exactMethod("updateNotification", ArrayList::class.java)
|
||||
installCardsGrid(classLoader)
|
||||
framework.hook(updateActive).intercept { chain ->
|
||||
val raw = notificationList(chain.args[0]) ?: return@intercept chain.proceed()
|
||||
@@ -54,17 +54,23 @@ internal class SamsungAodBackend(private val framework: XposedInterface) : Visib
|
||||
}
|
||||
val filteredFirst = filter(first)
|
||||
val filteredSecond = filter(second)
|
||||
visibleNotifications = filteredSecond
|
||||
val result = if (filteredFirst === first && filteredSecond === second) chain.proceed() else {
|
||||
chain.proceed(arrayOf(filteredFirst, filteredSecond, chain.args[2]))
|
||||
}
|
||||
refreshCardsGrid()
|
||||
result
|
||||
}
|
||||
framework.hook(updateIcons).intercept { chain ->
|
||||
val icons = imageViewList(chain.args[0]) ?: return@intercept chain.proceed()
|
||||
val result = chain.proceed()
|
||||
notificationIconViews = icons
|
||||
refreshCardsGrid()
|
||||
result
|
||||
}
|
||||
}.onSuccess {
|
||||
Log.i(TAG, "Installed Samsung AOD notification backend")
|
||||
}.onFailure {
|
||||
Log.d(TAG, "Samsung AOD notification boundary is unavailable")
|
||||
}.onFailure { error ->
|
||||
Log.w(TAG, "Samsung AOD notification boundary is unavailable", error)
|
||||
}.isSuccess
|
||||
|
||||
override fun onPolicyChanged() {
|
||||
@@ -118,7 +124,6 @@ internal class SamsungAodBackend(private val framework: XposedInterface) : Visib
|
||||
cardsDozing[view] = dozing
|
||||
if (dozing) beginDozeTransition(view, origin ?: screenLocation(view)) else {
|
||||
cardsTransitions.remove(view)
|
||||
cardsLocations.remove(view)
|
||||
cardsRenderer.suspend(view, isCardsMode(view), view)
|
||||
}
|
||||
}
|
||||
@@ -147,20 +152,17 @@ internal class SamsungAodBackend(private val framework: XposedInterface) : Visib
|
||||
}
|
||||
container.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
|
||||
override fun onViewAttachedToWindow(view: View) {
|
||||
if (cardsDozing[container] == true) beginLayoutSettle(container) else refreshCardsGrid()
|
||||
if (cardsDozing[container] == true) beginAttachedTransition(container) else refreshCardsGrid()
|
||||
}
|
||||
override fun onViewDetachedFromWindow(view: View) {
|
||||
cardsDozing[container] = false
|
||||
cardsTransitions.remove(container)
|
||||
cardsLocations.remove(container)
|
||||
cardsRenderer.release(container)
|
||||
}
|
||||
})
|
||||
container.addOnLayoutChangeListener { _, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom ->
|
||||
if (left != oldLeft || top != oldTop || right != oldRight || bottom != oldBottom) {
|
||||
if (cardsDozing[container] == true) {
|
||||
if (cardsTransitions[container] == null) beginLayoutSettle(container) else refreshCardsGrid()
|
||||
} else refreshCardsGrid()
|
||||
if (right - left != oldRight - oldLeft || bottom - top != oldBottom - oldTop) {
|
||||
refreshCardsGrid()
|
||||
}
|
||||
}
|
||||
refreshCardsGrid()
|
||||
@@ -180,12 +182,11 @@ internal class SamsungAodBackend(private val framework: XposedInterface) : Visib
|
||||
refreshCardsGrid(POSITION_POLL_DELAY)
|
||||
}
|
||||
|
||||
private fun beginLayoutSettle(container: ViewGroup) {
|
||||
private fun beginAttachedTransition(container: ViewGroup) {
|
||||
val now = SystemClock.uptimeMillis()
|
||||
val current = screenLocation(container)
|
||||
val origin = cardsLocations[container] ?: current
|
||||
cardsTransitions[container] = CardsTransition(
|
||||
origin,
|
||||
current,
|
||||
current,
|
||||
moved = true,
|
||||
stableSince = now,
|
||||
@@ -221,7 +222,6 @@ internal class SamsungAodBackend(private val framework: XposedInterface) : Visib
|
||||
val location = screenLocation(container)
|
||||
val transition = cardsTransitions[container]
|
||||
if (transition == null) {
|
||||
cardsLocations[container] = location
|
||||
ready += container
|
||||
return@forEach
|
||||
}
|
||||
@@ -236,7 +236,6 @@ internal class SamsungAodBackend(private val framework: XposedInterface) : Visib
|
||||
now >= transition.deadline
|
||||
) {
|
||||
cardsTransitions.remove(container)
|
||||
cardsLocations[container] = location
|
||||
ready += container
|
||||
} else {
|
||||
pollAgain = true
|
||||
@@ -244,26 +243,18 @@ internal class SamsungAodBackend(private val framework: XposedInterface) : Visib
|
||||
}
|
||||
if (pollAgain) refreshCardsGrid(POSITION_POLL_DELAY)
|
||||
if (ready.isEmpty()) return
|
||||
val notifications = visibleNotifications.distinctBy(StatusBarNotification::getKey)
|
||||
val config = OneUiCardsGridRenderer.Config(
|
||||
ProcessVisibilityPolicyCache.aodCards,
|
||||
showPills = false,
|
||||
hideShelf = false,
|
||||
followSourceMovement = true,
|
||||
)
|
||||
ready.forEach { container ->
|
||||
val tint = runCatching {
|
||||
container.javaClass.getMethod("getIconColor").invoke(container) as Int
|
||||
}.getOrNull()
|
||||
val sources = notifications.map { sbn ->
|
||||
val icon = sbn.notification.smallIcon
|
||||
val sources = notificationIconViews.map { icon ->
|
||||
OneUiCardsGridRenderer.Source(
|
||||
sbn.key,
|
||||
view = null,
|
||||
"aod@${System.identityHashCode(icon)}",
|
||||
icon,
|
||||
sbn.packageName,
|
||||
tint.takeUnless {
|
||||
icon.type == Icon.TYPE_BITMAP || icon.type == Icon.TYPE_ADAPTIVE_BITMAP
|
||||
},
|
||||
smallIcon = null,
|
||||
)
|
||||
}
|
||||
cardsRenderer.updateSources(container, sources)
|
||||
@@ -290,8 +281,8 @@ internal class SamsungAodBackend(private val framework: XposedInterface) : Visib
|
||||
chain: XposedInterface.Chain,
|
||||
argument: Int,
|
||||
raw: List<StatusBarNotification>,
|
||||
filtered: List<StatusBarNotification> = filter(raw),
|
||||
): Any? {
|
||||
val filtered = filter(raw)
|
||||
if (filtered === raw) return chain.proceed()
|
||||
val args = chain.args.toTypedArray()
|
||||
args[argument] = filtered
|
||||
@@ -322,6 +313,13 @@ internal class SamsungAodBackend(private val framework: XposedInterface) : Visib
|
||||
return ArrayList(list as List<StatusBarNotification>)
|
||||
}
|
||||
|
||||
private fun imageViewList(value: Any?): List<ImageView>? {
|
||||
val list = value as? ArrayList<*> ?: return null
|
||||
if (list.any { it !is ImageView }) return null
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return ArrayList(list as List<ImageView>)
|
||||
}
|
||||
|
||||
private fun state(manager: Any) = synchronized(managers) { managers.getOrPut(manager, ::State) }
|
||||
|
||||
private fun Class<*>.exactMethod(name: String, vararg parameters: Class<*>) =
|
||||
|
||||
Reference in New Issue
Block a user