Filter OneUI lockscreen notification cards
This commit is contained in:
@@ -25,6 +25,7 @@ class NotificationsMasterModule : XposedModule() {
|
|||||||
).firstOrNull { it.install(param.classLoader) }
|
).firstOrNull { it.install(param.classLoader) }
|
||||||
systemUiBackends = listOfNotNull(unlocked) + listOf(
|
systemUiBackends = listOfNotNull(unlocked) + listOf(
|
||||||
OneUiLockscreenBackend(this),
|
OneUiLockscreenBackend(this),
|
||||||
|
OneUiLockscreenCardsBackend(this),
|
||||||
).filter { it.install(param.classLoader) }
|
).filter { it.install(param.classLoader) }
|
||||||
ProcessVisibilityPolicyCache.installWhenReady {
|
ProcessVisibilityPolicyCache.installWhenReady {
|
||||||
systemUiBackends.orEmpty().forEach(VisibilitySurfaceBackend::onPolicyChanged)
|
systemUiBackends.orEmpty().forEach(VisibilitySurfaceBackend::onPolicyChanged)
|
||||||
|
|||||||
+214
@@ -0,0 +1,214 @@
|
|||||||
|
package se.ajpanton.notificationsmaster.module
|
||||||
|
|
||||||
|
import android.provider.Settings
|
||||||
|
import android.service.notification.StatusBarNotification
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import io.github.libxposed.api.XposedInterface
|
||||||
|
import se.ajpanton.notificationsmaster.visibility.NotificationSurface
|
||||||
|
import java.lang.reflect.Field
|
||||||
|
import java.lang.reflect.Method
|
||||||
|
import java.util.Collections
|
||||||
|
import java.util.WeakHashMap
|
||||||
|
|
||||||
|
/** Hides blocked OneUI notification rows only on the collapsed cards lockscreen. */
|
||||||
|
internal class OneUiLockscreenCardsBackend(
|
||||||
|
private val framework: XposedInterface,
|
||||||
|
) : VisibilitySurfaceBackend {
|
||||||
|
private val stacks = Collections.newSetFromMap(WeakHashMap<ViewGroup, Boolean>())
|
||||||
|
private val hiddenRows = WeakHashMap<View, RowState>()
|
||||||
|
private lateinit var entryForRow: (View) -> Any?
|
||||||
|
private lateinit var sbnForEntry: (Any) -> StatusBarNotification?
|
||||||
|
@Volatile private var statusBarStateKnown = false
|
||||||
|
@Volatile private var statusBarState = SHADE
|
||||||
|
@Volatile private var dozing = false
|
||||||
|
@Volatile private var blockedObserved = false
|
||||||
|
|
||||||
|
override fun install(classLoader: ClassLoader): Boolean = runCatching {
|
||||||
|
Class.forName(ONE_UI_LOCKSCREEN_COORDINATOR, false, classLoader)
|
||||||
|
val stackClass = Class.forName(STACK, false, classLoader)
|
||||||
|
val rowClass = Class.forName(ROW, false, classLoader)
|
||||||
|
val stateController = Class.forName(STATE_CONTROLLER, false, classLoader)
|
||||||
|
prepareNotificationAccess(rowClass)
|
||||||
|
|
||||||
|
stackClass.declaredMethods.filter { it.name == "onViewAdded" }.forEach { method ->
|
||||||
|
method.isAccessible = true
|
||||||
|
framework.hook(method).intercept { chain ->
|
||||||
|
val result = chain.proceed()
|
||||||
|
(chain.thisObject as? ViewGroup)?.let { stack ->
|
||||||
|
synchronized(stacks) { stacks += stack }
|
||||||
|
(chain.args.firstOrNull() as? View)?.let(::applyRow)
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stackClass.declaredMethods.filter { it.name == "onViewRemoved" }.forEach { method ->
|
||||||
|
method.isAccessible = true
|
||||||
|
framework.hook(method).intercept { chain ->
|
||||||
|
(chain.args.firstOrNull() as? View)?.let(::restoreRow)
|
||||||
|
chain.proceed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stackClass.declaredMethods.filter { it.name == "onMeasure" }.forEach { method ->
|
||||||
|
method.isAccessible = true
|
||||||
|
framework.hook(method).intercept { chain ->
|
||||||
|
(chain.thisObject as? ViewGroup)?.let { stack ->
|
||||||
|
synchronized(stacks) { stacks += stack }
|
||||||
|
applyStack(stack)
|
||||||
|
}
|
||||||
|
chain.proceed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stateController.declaredConstructors.forEach { constructor ->
|
||||||
|
constructor.isAccessible = true
|
||||||
|
framework.hook(constructor).intercept { chain ->
|
||||||
|
val result = chain.proceed()
|
||||||
|
updateState(chain.thisObject)
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stateController.declaredMethods.filter {
|
||||||
|
it.name == "setState" || it.name == "setDozeAmountInternal"
|
||||||
|
}.forEach { method ->
|
||||||
|
method.isAccessible = true
|
||||||
|
framework.hook(method).intercept { chain ->
|
||||||
|
val result = chain.proceed()
|
||||||
|
updateState(chain.thisObject)
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.onSuccess {
|
||||||
|
Log.i(TAG, "Installed OneUI lockscreen-cards notification backend")
|
||||||
|
}.onFailure {
|
||||||
|
Log.d(TAG, "OneUI lockscreen-cards boundary is unavailable", it)
|
||||||
|
}.isSuccess
|
||||||
|
|
||||||
|
override fun onPolicyChanged() {
|
||||||
|
blockedObserved = false
|
||||||
|
applyTrackedStacks()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun prepareNotificationAccess(rowClass: Class<*>) {
|
||||||
|
val entryMethod = rowClass.methods.firstOrNull { it.name == "getEntry" && it.parameterCount == 0 }
|
||||||
|
val entryField = findField(rowClass, "mEntry", "entry")
|
||||||
|
require(entryMethod != null || entryField != null) { "OneUI notification row entry is unavailable" }
|
||||||
|
entryForRow = accessor(entryMethod, entryField)
|
||||||
|
|
||||||
|
val entryClass = entryMethod?.returnType ?: entryField!!.type
|
||||||
|
val sbnMethod = entryClass.methods.firstOrNull { it.name == "getSbn" && it.parameterCount == 0 }
|
||||||
|
val sbnField = findField(entryClass, "mSbn", "sbn", "mStatusBarNotification")
|
||||||
|
require(sbnMethod != null || sbnField != null) { "OneUI notification entry SBN is unavailable" }
|
||||||
|
val accessor = accessor(sbnMethod, sbnField)
|
||||||
|
sbnForEntry = { accessor(it) as? StatusBarNotification }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun accessor(method: Method?, field: Field?): (Any) -> Any? {
|
||||||
|
method?.isAccessible = true
|
||||||
|
field?.isAccessible = true
|
||||||
|
return if (method != null) ({ target -> method.invoke(target) }) else ({ target -> field!!.get(target) })
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateState(controller: Any) {
|
||||||
|
(findField(controller.javaClass, "mState")?.get(controller) as? Int)?.let {
|
||||||
|
statusBarState = it
|
||||||
|
statusBarStateKnown = true
|
||||||
|
}
|
||||||
|
(findField(controller.javaClass, "mIsDozing")?.get(controller) as? Boolean)?.let { dozing = it }
|
||||||
|
applyTrackedStacks()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun applyTrackedStacks() {
|
||||||
|
synchronized(stacks) { stacks.toList() }.forEach(::applyStack)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun applyStack(stack: ViewGroup) {
|
||||||
|
val active = statusBarStateKnown && statusBarState == KEYGUARD && !dozing && isCardsMode(stack)
|
||||||
|
repeat(stack.childCount) { index ->
|
||||||
|
val row = stack.getChildAt(index)
|
||||||
|
if (row.javaClass.name == ROW) {
|
||||||
|
if (active && isBlocked(row)) hideRow(row) else restoreRow(row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun applyRow(row: View) {
|
||||||
|
val stack = row.parent as? ViewGroup ?: return
|
||||||
|
val active = statusBarStateKnown && statusBarState == KEYGUARD && !dozing && isCardsMode(stack)
|
||||||
|
if (active && isBlocked(row)) hideRow(row) else restoreRow(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isCardsMode(view: View) = runCatching {
|
||||||
|
Settings.System.getInt(view.context.contentResolver, LOCKSCREEN_STYLE) == CARDS
|
||||||
|
}.getOrDefault(false)
|
||||||
|
|
||||||
|
private fun isBlocked(row: View): Boolean {
|
||||||
|
val sbn = runCatching { entryForRow(row)?.let(sbnForEntry) }.getOrNull() ?: return false
|
||||||
|
return ProcessVisibilityPolicyCache.isBlocked(
|
||||||
|
VisibilityNotificationExtractor.from(sbn),
|
||||||
|
NotificationSurface.LOCKSCREEN_COLLAPSED,
|
||||||
|
).also { blocked ->
|
||||||
|
if (blocked && !blockedObserved) {
|
||||||
|
blockedObserved = true
|
||||||
|
Log.i(TAG, "Filtered a OneUI lockscreen notification card")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hideRow(row: View) {
|
||||||
|
if (row.visibility == View.GONE && row.alpha == 0f) return
|
||||||
|
hiddenRows.putIfAbsent(row, RowState(row.visibility, row.alpha))
|
||||||
|
row.visibility = View.GONE
|
||||||
|
row.alpha = 0f
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun restoreRow(row: View) {
|
||||||
|
val state = hiddenRows.remove(row) ?: return
|
||||||
|
row.visibility = state.visibility
|
||||||
|
row.alpha = state.alpha
|
||||||
|
setBooleanField(row, "mWillBeGone", false)
|
||||||
|
setBooleanField(row, "mInShelf", false)
|
||||||
|
setBooleanField(row, "mTransformingInShelf", false)
|
||||||
|
findField(row.javaClass, "mViewState")?.get(row)?.let { viewState ->
|
||||||
|
setBooleanField(viewState, "hidden", false)
|
||||||
|
setBooleanField(viewState, "gone", false)
|
||||||
|
}
|
||||||
|
row.requestLayout()
|
||||||
|
(row.parent as? View)?.requestLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setBooleanField(target: Any, name: String, value: Boolean) {
|
||||||
|
findField(target.javaClass, name)?.let { field ->
|
||||||
|
if (field.get(target) is Boolean) field.setBoolean(target, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findField(type: Class<*>, vararg names: String): Field? {
|
||||||
|
var current: Class<*>? = type
|
||||||
|
while (current != null) {
|
||||||
|
names.forEach { name ->
|
||||||
|
runCatching { current.getDeclaredField(name) }.getOrNull()?.let {
|
||||||
|
it.isAccessible = true
|
||||||
|
return it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
current = current.superclass
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class RowState(val visibility: Int, val alpha: Float)
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val TAG = "NotificationsMaster"
|
||||||
|
const val SHADE = 0
|
||||||
|
const val KEYGUARD = 1
|
||||||
|
const val CARDS = 0
|
||||||
|
const val LOCKSCREEN_STYLE = "lockscreen_minimizing_notification"
|
||||||
|
const val STACK = "com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout"
|
||||||
|
const val ROW = "com.android.systemui.statusbar.notification.row.ExpandableNotificationRow"
|
||||||
|
const val STATE_CONTROLLER = "com.android.systemui.statusbar.StatusBarStateControllerImpl"
|
||||||
|
const val ONE_UI_LOCKSCREEN_COORDINATOR =
|
||||||
|
"com.android.systemui.statusbar.notification.collection.coordinator.LockScreenNotiIconCoordinator"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user