Filter AOSP lockscreen notifications
This commit is contained in:
@@ -0,0 +1,319 @@
|
||||
package se.ajpanton.notificationsmaster.module
|
||||
|
||||
import android.os.Build
|
||||
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 rows only in AOSP's collapsed lockscreen notification stack. */
|
||||
internal class AospLockscreenBackend(
|
||||
private val framework: XposedInterface,
|
||||
) : VisibilitySurfaceBackend {
|
||||
private val stacks = Collections.newSetFromMap(WeakHashMap<ViewGroup, Boolean>())
|
||||
private val shelves = Collections.newSetFromMap(WeakHashMap<ViewGroup, Boolean>())
|
||||
private val hiddenRows = WeakHashMap<View, RowState>()
|
||||
private val groupOrders = WeakHashMap<View, List<View>>()
|
||||
private lateinit var onKeyguard: Method
|
||||
private lateinit var entryForRow: (View) -> Any?
|
||||
private lateinit var sbnForEntry: (Any) -> StatusBarNotification?
|
||||
private lateinit var attachedChildrenForRow: (View) -> List<View>
|
||||
private lateinit var reorderChildren: (View, List<View>) -> Unit
|
||||
private lateinit var setGroupCount: (View, Int) -> Unit
|
||||
private lateinit var calculateShelfIcons: Method
|
||||
private lateinit var shelfNotification: Field
|
||||
private lateinit var shelfIconStates: Field
|
||||
private lateinit var shelfClampedAmount: Field
|
||||
private lateinit var shelfAppearAmount: Field
|
||||
private lateinit var shelfAlpha: Field
|
||||
private lateinit var shelfHidden: Field
|
||||
private lateinit var shelfVisibleState: Field
|
||||
@Volatile private var blockedObserved = false
|
||||
|
||||
override fun install(classLoader: ClassLoader): Boolean {
|
||||
if (Build.MANUFACTURER.equals("samsung", ignoreCase = true)) return false
|
||||
return runCatching {
|
||||
val stackClass = Class.forName(STACK, false, classLoader)
|
||||
val rowClass = Class.forName(ROW, false, classLoader)
|
||||
onKeyguard = stackClass.declaredMethods.singleOrNull {
|
||||
it.name == "onKeyguard" && it.parameterCount == 0 &&
|
||||
it.returnType == Boolean::class.javaPrimitiveType
|
||||
}?.also { it.isAccessible = true }
|
||||
?: error("AOSP lockscreen state method not found")
|
||||
prepareNotificationAccess(rowClass)
|
||||
installShelfFilter(classLoader)
|
||||
|
||||
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 ->
|
||||
track(stack)
|
||||
(chain.args.firstOrNull() as? View)?.let { applyRow(stack, it) }
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
stackClass.declaredMethods.filter { it.name == "onViewRemoved" }.forEach { method ->
|
||||
method.isAccessible = true
|
||||
framework.hook(method).intercept { chain ->
|
||||
(chain.args.firstOrNull() as? View)?.let(::restoreTree)
|
||||
chain.proceed()
|
||||
}
|
||||
}
|
||||
stackClass.declaredMethods.filter { it.name == "onMeasure" }.forEach { method ->
|
||||
method.isAccessible = true
|
||||
framework.hook(method).intercept { chain ->
|
||||
(chain.thisObject as? ViewGroup)?.let {
|
||||
track(it)
|
||||
applyStack(it)
|
||||
}
|
||||
chain.proceed()
|
||||
}
|
||||
}
|
||||
stackClass.declaredMethods.filter { it.name == "setStatusBarState" }.forEach { method ->
|
||||
method.isAccessible = true
|
||||
framework.hook(method).intercept { chain ->
|
||||
val result = chain.proceed()
|
||||
(chain.thisObject as? ViewGroup)?.let {
|
||||
track(it)
|
||||
applyStack(it)
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
}.onSuccess {
|
||||
Log.i(TAG, "Installed AOSP collapsed-lockscreen notification backend")
|
||||
}.onFailure {
|
||||
Log.w(TAG, "AOSP lockscreen shape is unsupported; leaving rows unchanged", it)
|
||||
}.isSuccess
|
||||
}
|
||||
|
||||
override fun onPolicyChanged() {
|
||||
blockedObserved = false
|
||||
synchronized(stacks) { stacks.toList() }.forEach(::applyStack)
|
||||
synchronized(shelves) { shelves.toList() }.forEach { shelf ->
|
||||
runCatching { calculateShelfIcons.invoke(shelf) }
|
||||
.onFailure { Log.w(TAG, "Could not refresh AOSP lockscreen shelf", it) }
|
||||
}
|
||||
}
|
||||
|
||||
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) { "AOSP 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) { "AOSP notification entry SBN is unavailable" }
|
||||
val accessor = accessor(sbnMethod, sbnField)
|
||||
sbnForEntry = { accessor(it) as? StatusBarNotification }
|
||||
val attachedChildren = rowClass.methods.firstOrNull {
|
||||
it.name == "getAttachedChildren" && it.parameterCount == 0
|
||||
}?.also { it.isAccessible = true }
|
||||
attachedChildrenForRow = { row ->
|
||||
(attachedChildren?.invoke(row) as? List<*>)?.filterIsInstance<View>().orEmpty()
|
||||
}
|
||||
reorderChildren = { row, ordered ->
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
(attachedChildren?.invoke(row) as? MutableList<Any?>)?.let {
|
||||
it.clear()
|
||||
it.addAll(ordered)
|
||||
}
|
||||
}
|
||||
val childrenContainer = requireField(rowClass, "mChildrenContainer")
|
||||
val untruncatedCount = requireField(childrenContainer.type, "mUntruncatedChildCount")
|
||||
setGroupCount = { row, count ->
|
||||
childrenContainer.get(row)?.let { untruncatedCount.setInt(it, count) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun installShelfFilter(classLoader: ClassLoader) {
|
||||
val iconContainer = Class.forName(ICON_CONTAINER, false, classLoader)
|
||||
val iconView = Class.forName(STATUS_BAR_ICON_VIEW, false, classLoader)
|
||||
val iconState = Class.forName(ICON_STATE, false, classLoader)
|
||||
calculateShelfIcons = iconContainer.declaredMethods.singleOrNull {
|
||||
it.name == "calculateIconXTranslations" && it.parameterCount == 0
|
||||
}?.also { it.isAccessible = true } ?: error("AOSP shelf icon layout method not found")
|
||||
shelfNotification = requireField(iconView, "mNotification")
|
||||
shelfIconStates = requireField(iconContainer, "mIconStates")
|
||||
shelfClampedAmount = requireField(iconState, "clampedAppearAmount")
|
||||
shelfAppearAmount = requireField(iconState, "iconAppearAmount")
|
||||
shelfAlpha = requireField(iconState, "mAlpha")
|
||||
shelfHidden = requireField(iconState, "hidden")
|
||||
shelfVisibleState = requireField(iconState, "visibleState")
|
||||
|
||||
framework.hook(calculateShelfIcons).intercept { chain ->
|
||||
val container = chain.thisObject as? ViewGroup
|
||||
?: return@intercept chain.proceed()
|
||||
if (container.parent?.javaClass?.name != SHELF) return@intercept chain.proceed()
|
||||
synchronized(shelves) { shelves += container }
|
||||
if (!isCollapsedLockscreen(container)) return@intercept chain.proceed()
|
||||
val children = List(container.childCount, container::getChildAt)
|
||||
val states = shelfIconStates.get(container) as? Map<*, *>
|
||||
?: return@intercept chain.proceed()
|
||||
val blocked = children.filter(::isBlockedShelfIcon)
|
||||
blocked.forEach { states[it]?.let(::hideShelfIconState) }
|
||||
children.filterNot(blocked::contains).forEach(container::bringChildToFront)
|
||||
val result = try {
|
||||
chain.proceed()
|
||||
} finally {
|
||||
children.forEach(container::bringChildToFront)
|
||||
}
|
||||
blocked.forEach { states[it]?.let(::hideShelfIconState) }
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
private fun track(stack: ViewGroup) {
|
||||
synchronized(stacks) { stacks += stack }
|
||||
}
|
||||
|
||||
private fun applyStack(stack: ViewGroup) {
|
||||
val active = isCollapsedLockscreen(stack)
|
||||
repeat(stack.childCount) { applyRow(stack, stack.getChildAt(it), active) }
|
||||
stack.requestLayout()
|
||||
}
|
||||
|
||||
private fun applyRow(stack: ViewGroup, row: View, active: Boolean? = null) {
|
||||
if (row.javaClass.name != ROW) return
|
||||
val filter = active ?: isCollapsedLockscreen(stack)
|
||||
if (!filter) restoreGroupOrder(row)
|
||||
val children = attachedChildrenForRow(row)
|
||||
val parentBlocked = filter && isBlocked(row)
|
||||
var blockedChildren = children.map { it to (parentBlocked && isBlocked(it)) }
|
||||
if (parentBlocked && children.isNotEmpty()) {
|
||||
groupOrders.putIfAbsent(row, children)
|
||||
blockedChildren = blockedChildren.partition { !it.second }.let { it.first + it.second }
|
||||
reorderChildren(row, blockedChildren.map { it.first })
|
||||
setGroupCount(row, blockedChildren.count { !it.second })
|
||||
}
|
||||
blockedChildren.forEach { (child, blocked) ->
|
||||
if (blocked) hideRow(child) else restoreRow(child)
|
||||
}
|
||||
if (parentBlocked && blockedChildren.all { it.second }) hideRow(row) else restoreRow(row)
|
||||
}
|
||||
|
||||
private fun isBlocked(row: View): Boolean {
|
||||
val sbn = runCatching { entryForRow(row)?.let(sbnForEntry) }.getOrNull() ?: return false
|
||||
return isBlocked(sbn)
|
||||
}
|
||||
|
||||
private fun isBlockedShelfIcon(icon: View): Boolean {
|
||||
if (icon.javaClass.name != STATUS_BAR_ICON_VIEW) return false
|
||||
val sbn = runCatching { shelfNotification.get(icon) as? StatusBarNotification }.getOrNull()
|
||||
?: return false
|
||||
val row = rowForKey(icon, sbn.key)
|
||||
return row?.let { isBlocked(it) && attachedChildrenForRow(it).none { child -> !isBlocked(child) } }
|
||||
?: isBlocked(sbn)
|
||||
}
|
||||
|
||||
private fun rowForKey(view: View, key: String): View? {
|
||||
val root = view.rootView
|
||||
return synchronized(stacks) { stacks.toList() }
|
||||
.asSequence()
|
||||
.filter { it.rootView === root }
|
||||
.flatMap { stack -> (0 until stack.childCount).asSequence().map(stack::getChildAt) }
|
||||
.firstOrNull { row ->
|
||||
row.javaClass.name == ROW &&
|
||||
runCatching { entryForRow(row)?.let(sbnForEntry)?.key == key }.getOrDefault(false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isBlocked(sbn: StatusBarNotification): Boolean =
|
||||
ProcessVisibilityPolicyCache.isBlocked(
|
||||
VisibilityNotificationExtractor.from(sbn),
|
||||
NotificationSurface.LOCKSCREEN_COLLAPSED,
|
||||
).also { blocked ->
|
||||
if (blocked && !blockedObserved) {
|
||||
blockedObserved = true
|
||||
Log.i(TAG, "Filtered an AOSP collapsed-lockscreen notification")
|
||||
}
|
||||
}
|
||||
|
||||
private fun isCollapsedLockscreen(view: View): Boolean {
|
||||
val root = view.rootView
|
||||
val stack = synchronized(stacks) { stacks.firstOrNull { it.rootView === root } } ?: return false
|
||||
return runCatching { onKeyguard.invoke(stack) as Boolean }.getOrDefault(false)
|
||||
}
|
||||
|
||||
private fun hideShelfIconState(state: Any) {
|
||||
shelfClampedAmount.setFloat(state, 0f)
|
||||
shelfAppearAmount.setFloat(state, 0f)
|
||||
shelfAlpha.setFloat(state, 0f)
|
||||
shelfHidden.setBoolean(state, true)
|
||||
shelfVisibleState.setInt(state, ICON_HIDDEN)
|
||||
}
|
||||
|
||||
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
|
||||
row.requestLayout()
|
||||
(row.parent as? View)?.requestLayout()
|
||||
}
|
||||
|
||||
private fun restoreTree(row: View) {
|
||||
restoreGroupOrder(row)
|
||||
if (row.javaClass.name == ROW) attachedChildrenForRow(row).forEach(::restoreRow)
|
||||
restoreRow(row)
|
||||
}
|
||||
|
||||
private fun restoreGroupOrder(row: View) {
|
||||
val saved = groupOrders.remove(row) ?: return
|
||||
val current = attachedChildrenForRow(row)
|
||||
reorderChildren(row, saved.filter(current::contains) + current.filterNot(saved::contains))
|
||||
setGroupCount(row, current.size)
|
||||
}
|
||||
|
||||
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 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 fun requireField(type: Class<*>, name: String) =
|
||||
findField(type, name) ?: error("AOSP $name field is unavailable")
|
||||
|
||||
private data class RowState(val visibility: Int, val alpha: Float)
|
||||
|
||||
private companion object {
|
||||
const val TAG = "NotificationsMaster"
|
||||
const val STACK = "com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout"
|
||||
const val ROW = "com.android.systemui.statusbar.notification.row.ExpandableNotificationRow"
|
||||
const val SHELF = "com.android.systemui.statusbar.NotificationShelf"
|
||||
const val ICON_CONTAINER = "com.android.systemui.statusbar.phone.NotificationIconContainer"
|
||||
const val ICON_STATE = "$ICON_CONTAINER\$IconState"
|
||||
const val STATUS_BAR_ICON_VIEW = "com.android.systemui.statusbar.StatusBarIconView"
|
||||
const val ICON_HIDDEN = 2
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ class NotificationsMasterModule : XposedModule() {
|
||||
AospModernIconBackend(this),
|
||||
).firstOrNull { it.install(param.classLoader) }
|
||||
systemUiBackends = listOfNotNull(unlocked) + listOf(
|
||||
AospLockscreenBackend(this),
|
||||
OneUiLockscreenBackend(this),
|
||||
OneUiLockscreenCardsBackend(this),
|
||||
SamsungAodPluginBackend(this),
|
||||
|
||||
Reference in New Issue
Block a user