Filter OneUI lockscreen notification cards
This commit is contained in:
@@ -25,6 +25,7 @@ class NotificationsMasterModule : XposedModule() {
|
||||
).firstOrNull { it.install(param.classLoader) }
|
||||
systemUiBackends = listOfNotNull(unlocked) + listOf(
|
||||
OneUiLockscreenBackend(this),
|
||||
OneUiLockscreenCardsBackend(this),
|
||||
).filter { it.install(param.classLoader) }
|
||||
ProcessVisibilityPolicyCache.installWhenReady {
|
||||
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