Filter native lockscreen and AOD surfaces
This commit is contained in:
+1
-1
@@ -16,7 +16,7 @@ class VisibilityModuleFixtureTest {
|
|||||||
requireFixtureMode()
|
requireFixtureMode()
|
||||||
store.save(VisibilityPolicy(apps = listOf(AppVisibilityPolicy(
|
store.save(VisibilityPolicy(apps = listOf(AppVisibilityPolicy(
|
||||||
packageName = HELPER_PACKAGE,
|
packageName = HELPER_PACKAGE,
|
||||||
blockedSurfaces = setOf(NotificationSurface.UNLOCKED_STATUSBAR),
|
blockedSurfaces = NotificationSurface.entries.toSet(),
|
||||||
exceptions = listOf(VisibilityExceptionRule("Edited message", emptySet())),
|
exceptions = listOf(VisibilityExceptionRule("Edited message", emptySet())),
|
||||||
))))
|
))))
|
||||||
}
|
}
|
||||||
|
|||||||
+34
-18
@@ -10,16 +10,18 @@ import se.ajpanton.notificationsmaster.visibility.NotificationSurface
|
|||||||
import java.lang.reflect.Method
|
import java.lang.reflect.Method
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
import java.util.WeakHashMap
|
import java.util.WeakHashMap
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
/** API 34's class-id 1 mapper is used exclusively by the unlocked status-bar container. */
|
/** API 34 uses distinct mapper IDs for its unlocked and AOD icon containers. */
|
||||||
internal class Aosp34UnlockedIconBackend(private val framework: XposedInterface) : UnlockedIconBackend {
|
internal class Aosp34IconBackend(private val framework: XposedInterface) : VisibilitySurfaceBackend {
|
||||||
private val controllers = Collections.newSetFromMap(WeakHashMap<Any, Boolean>())
|
private val controllers = Collections.newSetFromMap(WeakHashMap<Any, Boolean>())
|
||||||
private lateinit var updateStatusBarIcons: Method
|
private lateinit var updateStatusBarIcons: Method
|
||||||
@Volatile private var blockedObserved = false
|
private var updateAodNotificationIcons: Method? = null
|
||||||
|
private val blockedSurfaces = ConcurrentHashMap.newKeySet<NotificationSurface>()
|
||||||
|
|
||||||
override fun install(classLoader: ClassLoader) {
|
override fun install(classLoader: ClassLoader): Boolean {
|
||||||
if (Build.VERSION.SDK_INT != 34) return
|
if (Build.VERSION.SDK_INT != 34) return false
|
||||||
runCatching {
|
return runCatching {
|
||||||
val controller = Class.forName(CONTROLLER, false, classLoader)
|
val controller = Class.forName(CONTROLLER, false, classLoader)
|
||||||
val mapper = Class.forName(ICON_MAPPER, false, classLoader)
|
val mapper = Class.forName(ICON_MAPPER, false, classLoader)
|
||||||
val entry = Class.forName(NOTIFICATION_ENTRY, false, classLoader)
|
val entry = Class.forName(NOTIFICATION_ENTRY, false, classLoader)
|
||||||
@@ -37,24 +39,35 @@ internal class Aosp34UnlockedIconBackend(private val framework: XposedInterface)
|
|||||||
updateStatusBarIcons = controller.declaredMethods.singleOrNull {
|
updateStatusBarIcons = controller.declaredMethods.singleOrNull {
|
||||||
it.name == "updateStatusBarIcons" && it.parameterTypes.isEmpty() && it.returnType == Void.TYPE
|
it.name == "updateStatusBarIcons" && it.parameterTypes.isEmpty() && it.returnType == Void.TYPE
|
||||||
} ?: error("Unlocked icon refresh method not found")
|
} ?: error("Unlocked icon refresh method not found")
|
||||||
|
updateAodNotificationIcons = controller.declaredMethods.singleOrNull {
|
||||||
|
it.name == "updateAodNotificationIcons" && it.parameterTypes.isEmpty() && it.returnType == Void.TYPE
|
||||||
|
}
|
||||||
apply.isAccessible = true
|
apply.isAccessible = true
|
||||||
updateStatusBarIcons.isAccessible = true
|
updateStatusBarIcons.isAccessible = true
|
||||||
framework.hook(updateStatusBarIcons).intercept { chain ->
|
updateAodNotificationIcons?.isAccessible = true
|
||||||
synchronized(controllers) { controllers += chain.thisObject }
|
listOfNotNull(updateStatusBarIcons, updateAodNotificationIcons).forEach { refresh ->
|
||||||
chain.proceed()
|
framework.hook(refresh).intercept { chain ->
|
||||||
|
synchronized(controllers) { controllers += chain.thisObject }
|
||||||
|
chain.proceed()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
framework.hook(apply).intercept { chain ->
|
framework.hook(apply).intercept { chain ->
|
||||||
if (classId.getInt(chain.thisObject) != STATUS_BAR_CLASS_ID) return@intercept chain.proceed()
|
val surface = when (classId.getInt(chain.thisObject)) {
|
||||||
|
AOD_CLASS_ID -> if (updateAodNotificationIcons != null) {
|
||||||
|
NotificationSurface.AOD
|
||||||
|
} else return@intercept chain.proceed()
|
||||||
|
STATUS_BAR_CLASS_ID -> NotificationSurface.UNLOCKED_STATUSBAR
|
||||||
|
else -> return@intercept chain.proceed()
|
||||||
|
}
|
||||||
val notification = runCatching {
|
val notification = runCatching {
|
||||||
VisibilityNotificationExtractor.from(sbn.get(chain.args[0]) as StatusBarNotification)
|
VisibilityNotificationExtractor.from(sbn.get(chain.args[0]) as StatusBarNotification)
|
||||||
}.getOrNull() ?: return@intercept chain.proceed()
|
}.getOrNull() ?: return@intercept chain.proceed()
|
||||||
if (!ProcessVisibilityPolicyCache.isBlocked(
|
if (!ProcessVisibilityPolicyCache.isBlocked(
|
||||||
notification,
|
notification,
|
||||||
NotificationSurface.UNLOCKED_STATUSBAR,
|
surface,
|
||||||
)) return@intercept chain.proceed()
|
)) return@intercept chain.proceed()
|
||||||
if (!blockedObserved) {
|
if (blockedSurfaces.add(surface)) {
|
||||||
blockedObserved = true
|
Log.i(TAG, "Filtered an API 34 ${surface.name} notification icon")
|
||||||
Log.i(TAG, "Filtered an API 34 unlocked notification icon")
|
|
||||||
}
|
}
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
@@ -62,22 +75,25 @@ internal class Aosp34UnlockedIconBackend(private val framework: XposedInterface)
|
|||||||
Log.i(TAG, "Installed AOSP API 34 unlocked notification-icon backend")
|
Log.i(TAG, "Installed AOSP API 34 unlocked notification-icon backend")
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
Log.w(TAG, "AOSP API 34 unlocked icon shape is unsupported; leaving icons unchanged", it)
|
Log.w(TAG, "AOSP API 34 unlocked icon shape is unsupported; leaving icons unchanged", it)
|
||||||
}
|
}.isSuccess
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPolicyChanged() {
|
override fun onPolicyChanged() {
|
||||||
blockedObserved = false
|
blockedSurfaces.clear()
|
||||||
Handler(Looper.getMainLooper()).post {
|
Handler(Looper.getMainLooper()).post {
|
||||||
val current = synchronized(controllers) { controllers.toList() }
|
val current = synchronized(controllers) { controllers.toList() }
|
||||||
current.forEach { controller ->
|
current.forEach { controller ->
|
||||||
runCatching { updateStatusBarIcons.invoke(controller) }
|
runCatching {
|
||||||
.onFailure { Log.w(TAG, "Could not refresh API 34 unlocked icons", it) }
|
updateStatusBarIcons.invoke(controller)
|
||||||
|
updateAodNotificationIcons?.invoke(controller)
|
||||||
|
}.onFailure { Log.w(TAG, "Could not refresh API 34 notification icons", it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
const val TAG = "NotificationsMaster"
|
const val TAG = "NotificationsMaster"
|
||||||
|
const val AOD_CLASS_ID = 0
|
||||||
const val STATUS_BAR_CLASS_ID = 1
|
const val STATUS_BAR_CLASS_ID = 1
|
||||||
const val CONTROLLER = "com.android.systemui.statusbar.phone.NotificationIconAreaController"
|
const val CONTROLLER = "com.android.systemui.statusbar.phone.NotificationIconAreaController"
|
||||||
const val ICON_MAPPER = "$CONTROLLER\$\$ExternalSyntheticLambda2"
|
const val ICON_MAPPER = "$CONTROLLER\$\$ExternalSyntheticLambda2"
|
||||||
+59
-22
@@ -12,17 +12,18 @@ import java.lang.reflect.Proxy
|
|||||||
import java.util.LinkedHashSet
|
import java.util.LinkedHashSet
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
/** Filters only the modern status-bar icon flow; the notification/shade model is untouched. */
|
/** Filters the modern unlocked and AOD icon flows; the notification/shade model is untouched. */
|
||||||
internal class AospModernUnlockedIconBackend(private val framework: XposedInterface) : UnlockedIconBackend {
|
internal class AospModernIconBackend(private val framework: XposedInterface) : VisibilitySurfaceBackend {
|
||||||
private val metadata = ConcurrentHashMap<String, VisibilityNotification>()
|
private val metadata = ConcurrentHashMap<String, VisibilityNotification>()
|
||||||
|
private val activeKeys = ConcurrentHashMap<NotificationSurface, Set<String>>()
|
||||||
@Volatile private var policyChanges: Any? = null
|
@Volatile private var policyChanges: Any? = null
|
||||||
private var policyRevision = 0L
|
private var policyRevision = 0L
|
||||||
@Volatile private var lastFilterSignature = -1L
|
private val lastFilterSignatures = ConcurrentHashMap<NotificationSurface, Long>()
|
||||||
@Volatile private var captureFailureLogged = false
|
@Volatile private var captureFailureLogged = false
|
||||||
|
|
||||||
override fun install(classLoader: ClassLoader) {
|
override fun install(classLoader: ClassLoader): Boolean {
|
||||||
if (Build.VERSION.SDK_INT !in 35..36) return
|
if (Build.VERSION.SDK_INT !in 35..36) return false
|
||||||
runCatching {
|
return runCatching {
|
||||||
val entryClass = Class.forName(NOTIFICATION_ENTRY, false, classLoader)
|
val entryClass = Class.forName(NOTIFICATION_ENTRY, false, classLoader)
|
||||||
val modelClass = Class.forName(ACTIVE_NOTIFICATION_MODEL, false, classLoader)
|
val modelClass = Class.forName(ACTIVE_NOTIFICATION_MODEL, false, classLoader)
|
||||||
val modelAccesses = buildList {
|
val modelAccesses = buildList {
|
||||||
@@ -39,11 +40,12 @@ internal class AospModernUnlockedIconBackend(private val framework: XposedInterf
|
|||||||
installMetadataCapture(classLoader, entryClass, modelClass)
|
installMetadataCapture(classLoader, entryClass, modelClass)
|
||||||
installFlowRefresh(classLoader)
|
installFlowRefresh(classLoader)
|
||||||
installStatusBarFilter(classLoader, modelAccesses)
|
installStatusBarFilter(classLoader, modelAccesses)
|
||||||
|
installAodFilter(classLoader, modelAccesses)
|
||||||
}.onSuccess {
|
}.onSuccess {
|
||||||
Log.i(TAG, "Installed AOSP API ${Build.VERSION.SDK_INT} unlocked notification-icon backend")
|
Log.i(TAG, "Installed AOSP API ${Build.VERSION.SDK_INT} unlocked notification-icon backend")
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
Log.w(TAG, "AOSP API ${Build.VERSION.SDK_INT} unlocked icon shape is unsupported; leaving icons unchanged", it)
|
Log.w(TAG, "AOSP API ${Build.VERSION.SDK_INT} unlocked icon shape is unsupported; leaving icons unchanged", it)
|
||||||
}
|
}.isSuccess
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized override fun onPolicyChanged() {
|
@Synchronized override fun onPolicyChanged() {
|
||||||
@@ -142,6 +144,9 @@ internal class AospModernUnlockedIconBackend(private val framework: XposedInterf
|
|||||||
else -> error("Unexpected Function3 method ${method.name}")
|
else -> error("Unexpected Function3 method ${method.name}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fun refreshed(upstream: Any?) = combineMethod?.invoke(null, upstream, signal, keepUpstreamValue)
|
||||||
|
?: transformMethod?.let { (method, transform) -> method.invoke(null, upstream, signal, transform) }
|
||||||
|
?: combineConstructor!!.newInstance(upstream, signal, keepUpstreamValue)
|
||||||
policyChanges = signal
|
policyChanges = signal
|
||||||
val constructor = interactor.declaredConstructors.singleOrNull {
|
val constructor = interactor.declaredConstructors.singleOrNull {
|
||||||
it.parameterTypes.map { type -> type.name } == listOf(
|
it.parameterTypes.map { type -> type.name } == listOf(
|
||||||
@@ -164,17 +169,24 @@ internal class AospModernUnlockedIconBackend(private val framework: XposedInterf
|
|||||||
framework.hook(target).intercept { chain ->
|
framework.hook(target).intercept { chain ->
|
||||||
val result = chain.proceed()
|
val result = chain.proceed()
|
||||||
runCatching {
|
runCatching {
|
||||||
val upstream = field.get(chain.thisObject)
|
field.set(chain.thisObject, refreshed(field.get(chain.thisObject)))
|
||||||
val combined = combineMethod?.invoke(null, upstream, signal, keepUpstreamValue)
|
|
||||||
?: transformMethod?.let { (method, transform) ->
|
|
||||||
method.invoke(null, upstream, signal, transform)
|
|
||||||
}
|
|
||||||
?: combineConstructor!!.newInstance(upstream, signal, keepUpstreamValue)
|
|
||||||
field.set(chain.thisObject, combined)
|
|
||||||
}.onFailure { Log.w(TAG, "Could not attach visibility refresh flow; leaving icons unchanged", it) }
|
}.onFailure { Log.w(TAG, "Could not attach visibility refresh flow; leaving icons unchanged", it) }
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
runCatching {
|
||||||
|
val aod = Class.forName(AOD_VIEW_MODEL, false, classLoader)
|
||||||
|
val icons = aod.requiredField("icons", null).also { require(flowClass.isAssignableFrom(it.type)) }
|
||||||
|
require(aod.declaredConstructors.isNotEmpty()) { "AOD view model has no constructors" }
|
||||||
|
aod.declaredConstructors.forEach { constructor ->
|
||||||
|
constructor.isAccessible = true
|
||||||
|
framework.hook(constructor).intercept { chain ->
|
||||||
|
val result = chain.proceed()
|
||||||
|
icons.set(chain.thisObject, refreshed(icons.get(chain.thisObject)))
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.onFailure { Log.i(TAG, "AOD icon refresh flow is unavailable") }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun installStatusBarFilter(
|
private fun installStatusBarFilter(
|
||||||
@@ -189,19 +201,42 @@ internal class AospModernUnlockedIconBackend(private val framework: XposedInterf
|
|||||||
emit.isAccessible = true
|
emit.isAccessible = true
|
||||||
framework.hook(emit).intercept { chain ->
|
framework.hook(emit).intercept { chain ->
|
||||||
val original = chain.args[0]
|
val original = chain.args[0]
|
||||||
val filtered = filter(original, modelAccesses)
|
val filtered = filter(original, modelAccesses, NotificationSurface.UNLOCKED_STATUSBAR)
|
||||||
if (filtered === original) chain.proceed() else chain.proceed(arrayOf(filtered, chain.args[1]))
|
if (filtered === original) chain.proceed() else chain.proceed(arrayOf(filtered, chain.args[1]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun filter(value: Any?, modelAccesses: List<ModelAccess>): Any? {
|
private fun installAodFilter(classLoader: ClassLoader, modelAccesses: List<ModelAccess>) {
|
||||||
|
runCatching {
|
||||||
|
val collector = Class.forName(AOD_MAP_COLLECTOR, false, classLoader)
|
||||||
|
val emit = collector.declaredMethods.singleOrNull(::isEmitMethod)
|
||||||
|
?: error("AOD icon collector method not found")
|
||||||
|
emit.isAccessible = true
|
||||||
|
framework.hook(emit).intercept { chain ->
|
||||||
|
val original = chain.args[0]
|
||||||
|
val filtered = filter(original, modelAccesses, NotificationSurface.AOD)
|
||||||
|
if (filtered === original) chain.proceed() else chain.proceed(arrayOf(filtered, chain.args[1]))
|
||||||
|
}
|
||||||
|
}.onSuccess {
|
||||||
|
Log.i(TAG, "Installed AOSP AOD notification-icon backend")
|
||||||
|
}.onFailure {
|
||||||
|
Log.i(TAG, "AOSP AOD notification-icon boundary is unavailable")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun filter(
|
||||||
|
value: Any?,
|
||||||
|
modelAccesses: List<ModelAccess>,
|
||||||
|
surface: NotificationSurface,
|
||||||
|
): Any? {
|
||||||
val entries = value as? Set<*> ?: return value
|
val entries = value as? Set<*> ?: return value
|
||||||
if (entries.any { model -> model != null && modelAccesses.none { it.type.isInstance(model) } }) return value
|
if (entries.any { model -> model != null && modelAccesses.none { it.type.isInstance(model) } }) return value
|
||||||
fun access(model: Any) = modelAccesses.first { it.type.isInstance(model) }
|
fun access(model: Any) = modelAccesses.first { it.type.isInstance(model) }
|
||||||
val activeKeys = entries.mapNotNull { model ->
|
val surfaceKeys = entries.mapNotNull { model ->
|
||||||
model?.let { access(it).key.get(it) as? String }
|
model?.let { access(it).key.get(it) as? String }
|
||||||
}.toSet()
|
}.toSet()
|
||||||
metadata.keys.retainAll(activeKeys)
|
activeKeys[surface] = surfaceKeys
|
||||||
|
metadata.keys.retainAll(activeKeys.values.flatten().toSet())
|
||||||
var changed = false
|
var changed = false
|
||||||
var missingMetadata = 0
|
var missingMetadata = 0
|
||||||
val filtered = entries.filterTo(LinkedHashSet(entries.size)) { model ->
|
val filtered = entries.filterTo(LinkedHashSet(entries.size)) { model ->
|
||||||
@@ -217,17 +252,16 @@ internal class AospModernUnlockedIconBackend(private val framework: XposedInterf
|
|||||||
if (hasExceptions && captured == null) missingMetadata += 1
|
if (hasExceptions && captured == null) missingMetadata += 1
|
||||||
val notification = captured ?: VisibilityNotification(packageName, key)
|
val notification = captured ?: VisibilityNotification(packageName, key)
|
||||||
val blocked = (!hasExceptions || captured != null) &&
|
val blocked = (!hasExceptions || captured != null) &&
|
||||||
ProcessVisibilityPolicyCache.isBlocked(notification, NotificationSurface.UNLOCKED_STATUSBAR)
|
ProcessVisibilityPolicyCache.isBlocked(notification, surface)
|
||||||
changed = changed || blocked
|
changed = changed || blocked
|
||||||
!blocked
|
!blocked
|
||||||
}
|
}
|
||||||
val hidden = entries.size - filtered.size
|
val hidden = entries.size - filtered.size
|
||||||
val signature = ((entries.size * 31L + hidden) * 31L + missingMetadata)
|
val signature = ((entries.size * 31L + hidden) * 31L + missingMetadata)
|
||||||
if (signature != lastFilterSignature) {
|
if (lastFilterSignatures.put(surface, signature) != signature) {
|
||||||
lastFilterSignature = signature
|
|
||||||
Log.i(
|
Log.i(
|
||||||
TAG,
|
TAG,
|
||||||
"Unlocked notification icons: input=${entries.size}, hidden=$hidden, " +
|
"${surface.name} notification icons: input=${entries.size}, hidden=$hidden, " +
|
||||||
"missingMetadata=$missingMetadata",
|
"missingMetadata=$missingMetadata",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -262,6 +296,9 @@ internal class AospModernUnlockedIconBackend(private val framework: XposedInterf
|
|||||||
"com.android.systemui.statusbar.notification.icon.domain.interactor.StatusBarNotificationIconsInteractor"
|
"com.android.systemui.statusbar.notification.icon.domain.interactor.StatusBarNotificationIconsInteractor"
|
||||||
const val STATUS_BAR_VIEW_MODEL =
|
const val STATUS_BAR_VIEW_MODEL =
|
||||||
"com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerStatusBarViewModel"
|
"com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerStatusBarViewModel"
|
||||||
|
const val AOD_VIEW_MODEL =
|
||||||
|
"com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerAlwaysOnDisplayViewModel"
|
||||||
|
const val AOD_MAP_COLLECTOR = "$AOD_VIEW_MODEL\$special\$\$inlined\$map\$1\$2"
|
||||||
const val STATUS_BAR_MAP_COLLECTOR_PREFIX =
|
const val STATUS_BAR_MAP_COLLECTOR_PREFIX =
|
||||||
"com.android.systemui.statusbar.notification.icon.ui.viewmodel." +
|
"com.android.systemui.statusbar.notification.icon.ui.viewmodel." +
|
||||||
"NotificationIconContainerStatusBarViewModel\$special\$\$inlined\$map\$"
|
"NotificationIconContainerStatusBarViewModel\$special\$\$inlined\$map\$"
|
||||||
+21
-9
@@ -11,19 +11,31 @@ import io.github.libxposed.api.XposedModuleInterface
|
|||||||
* in a compatible framework and grants it a system-server scope.
|
* in a compatible framework and grants it a system-server scope.
|
||||||
*/
|
*/
|
||||||
class NotificationsMasterModule : XposedModule() {
|
class NotificationsMasterModule : XposedModule() {
|
||||||
private var systemUiBackend: UnlockedIconBackend? = null
|
private var systemUiBackends: List<VisibilitySurfaceBackend>? = null
|
||||||
|
private var aodBackends: List<VisibilitySurfaceBackend>? = null
|
||||||
|
|
||||||
override fun onPackageReady(param: XposedModuleInterface.PackageReadyParam) {
|
override fun onPackageReady(param: XposedModuleInterface.PackageReadyParam) {
|
||||||
if (param.packageName !in VisibilityPolicySync.TARGET_PACKAGES) return
|
if (param.packageName !in VisibilityPolicySync.TARGET_PACKAGES) return
|
||||||
if (param.packageName == SYSTEM_UI_PACKAGE) {
|
if (param.packageName == SYSTEM_UI_PACKAGE) {
|
||||||
if (systemUiBackend != null) return
|
if (systemUiBackends != null) return
|
||||||
val backend = if (android.os.Build.VERSION.SDK_INT == 34) {
|
val unlocked = listOf(
|
||||||
Aosp34UnlockedIconBackend(this)
|
OneUiUnlockedIconBackend(this),
|
||||||
} else AospModernUnlockedIconBackend(this)
|
Aosp34IconBackend(this),
|
||||||
backend.install(param.classLoader)
|
AospModernIconBackend(this),
|
||||||
systemUiBackend = backend
|
).firstOrNull { it.install(param.classLoader) }
|
||||||
ProcessVisibilityPolicyCache.installWhenReady(backend::onPolicyChanged)
|
systemUiBackends = listOfNotNull(unlocked) + listOf(
|
||||||
} else ProcessVisibilityPolicyCache.installWhenReady()
|
OneUiLockscreenBackend(this),
|
||||||
|
).filter { it.install(param.classLoader) }
|
||||||
|
ProcessVisibilityPolicyCache.installWhenReady {
|
||||||
|
systemUiBackends.orEmpty().forEach(VisibilitySurfaceBackend::onPolicyChanged)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (aodBackends != null) return
|
||||||
|
aodBackends = listOf(SamsungAodBackend(this)).filter { it.install(param.classLoader) }
|
||||||
|
ProcessVisibilityPolicyCache.installWhenReady {
|
||||||
|
aodBackends.orEmpty().forEach(VisibilitySurfaceBackend::onPolicyChanged)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSystemServerStarting(param: XposedModuleInterface.SystemServerStartingParam) {
|
override fun onSystemServerStarting(param: XposedModuleInterface.SystemServerStartingParam) {
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package se.ajpanton.notificationsmaster.module
|
||||||
|
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.os.Message
|
||||||
|
import android.service.notification.StatusBarNotification
|
||||||
|
import android.util.Log
|
||||||
|
import io.github.libxposed.api.XposedInterface
|
||||||
|
import se.ajpanton.notificationsmaster.visibility.NotificationSurface
|
||||||
|
import java.lang.reflect.Method
|
||||||
|
import java.util.ArrayList
|
||||||
|
import java.util.Collections
|
||||||
|
import java.util.WeakHashMap
|
||||||
|
|
||||||
|
/** Filters OneUI's collapsed lockscreen projection without changing notification rows. */
|
||||||
|
internal class OneUiLockscreenBackend(private val framework: XposedInterface) : VisibilitySurfaceBackend {
|
||||||
|
private val coordinators = Collections.newSetFromMap(WeakHashMap<Any, Boolean>())
|
||||||
|
private lateinit var refresh: Method
|
||||||
|
@Volatile private var blockedObserved = false
|
||||||
|
|
||||||
|
override fun install(classLoader: ClassLoader): Boolean = runCatching {
|
||||||
|
val coordinator = Class.forName(COORDINATOR, false, classLoader)
|
||||||
|
val managerHandler = Class.forName(MANAGER_HANDLER, false, classLoader)
|
||||||
|
val info = Class.forName(NOTIFICATION_INFO, false, classLoader)
|
||||||
|
val sbn = info.getDeclaredField("mSbn").also {
|
||||||
|
require(it.type == StatusBarNotification::class.java)
|
||||||
|
it.isAccessible = true
|
||||||
|
}
|
||||||
|
val attach = coordinator.declaredMethods.singleOrNull {
|
||||||
|
it.name == "attach" && it.parameterTypes.size == 1 && it.returnType == Void.TYPE
|
||||||
|
} ?: error("OneUI lockscreen coordinator attach method not found")
|
||||||
|
refresh = coordinator.declaredMethods.singleOrNull {
|
||||||
|
it.name == "onLockScreenNotiStateChanged" &&
|
||||||
|
it.parameterTypes.isEmpty() && it.returnType == Void.TYPE
|
||||||
|
} ?: error("OneUI lockscreen refresh method not found")
|
||||||
|
val handle = managerHandler.declaredMethods.singleOrNull {
|
||||||
|
it.name == "handleMessage" &&
|
||||||
|
it.parameterTypes.contentEquals(arrayOf(Message::class.java)) && it.returnType == Void.TYPE
|
||||||
|
} ?: error("OneUI lockscreen update handler not found")
|
||||||
|
attach.isAccessible = true
|
||||||
|
refresh.isAccessible = true
|
||||||
|
handle.isAccessible = true
|
||||||
|
framework.hook(attach).intercept { chain ->
|
||||||
|
synchronized(coordinators) { coordinators += chain.thisObject }
|
||||||
|
chain.proceed()
|
||||||
|
}
|
||||||
|
framework.hook(handle).intercept { chain ->
|
||||||
|
val message = chain.args[0] as? Message ?: return@intercept chain.proceed()
|
||||||
|
if (message.what != NOTIFICATION_INFO_UPDATED) return@intercept chain.proceed()
|
||||||
|
val original = message.obj as? ArrayList<*> ?: return@intercept chain.proceed()
|
||||||
|
if (original.any { it != null && !info.isInstance(it) }) return@intercept chain.proceed()
|
||||||
|
val filtered = original.filterTo(ArrayList(original.size)) { item ->
|
||||||
|
val notification = item?.let {
|
||||||
|
runCatching {
|
||||||
|
VisibilityNotificationExtractor.from(sbn.get(it) as StatusBarNotification)
|
||||||
|
}.getOrNull()
|
||||||
|
}
|
||||||
|
notification == null || !ProcessVisibilityPolicyCache.isBlocked(
|
||||||
|
notification,
|
||||||
|
NotificationSurface.LOCKSCREEN_COLLAPSED,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (filtered.size == original.size) return@intercept chain.proceed()
|
||||||
|
if (!blockedObserved) {
|
||||||
|
blockedObserved = true
|
||||||
|
Log.i(TAG, "Filtered a OneUI collapsed-lockscreen notification")
|
||||||
|
}
|
||||||
|
message.obj = filtered
|
||||||
|
try {
|
||||||
|
chain.proceed()
|
||||||
|
} finally {
|
||||||
|
message.obj = original
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.onSuccess {
|
||||||
|
Log.i(TAG, "Installed OneUI collapsed-lockscreen notification backend")
|
||||||
|
}.onFailure {
|
||||||
|
Log.d(TAG, "OneUI collapsed-lockscreen boundary is unavailable")
|
||||||
|
}.isSuccess
|
||||||
|
|
||||||
|
override fun onPolicyChanged() {
|
||||||
|
blockedObserved = false
|
||||||
|
Handler(Looper.getMainLooper()).post {
|
||||||
|
val current = synchronized(coordinators) { coordinators.toList() }
|
||||||
|
current.forEach { coordinator ->
|
||||||
|
runCatching { refresh.invoke(coordinator) }
|
||||||
|
.onFailure { Log.w(TAG, "Could not refresh OneUI collapsed-lockscreen notifications", it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val TAG = "NotificationsMaster"
|
||||||
|
const val NOTIFICATION_INFO_UPDATED = 101
|
||||||
|
const val COORDINATOR =
|
||||||
|
"com.android.systemui.statusbar.notification.collection.coordinator.LockScreenNotiIconCoordinator"
|
||||||
|
const val MANAGER_HANDLER =
|
||||||
|
"com.android.systemui.statusbar.LockscreenNotificationManager\$LockscreenNotificationMgrHandler"
|
||||||
|
const val NOTIFICATION_INFO = "com.android.systemui.statusbar.LockscreenNotificationInfo"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
package se.ajpanton.notificationsmaster.module
|
||||||
|
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.service.notification.StatusBarNotification
|
||||||
|
import android.util.Log
|
||||||
|
import io.github.libxposed.api.XposedInterface
|
||||||
|
import se.ajpanton.notificationsmaster.visibility.NotificationSurface
|
||||||
|
import java.lang.reflect.Method
|
||||||
|
import java.util.Collections
|
||||||
|
import java.util.WeakHashMap
|
||||||
|
import java.util.function.Function
|
||||||
|
|
||||||
|
/** Filters only OneUI's native unlocked status-bar container. */
|
||||||
|
internal class OneUiUnlockedIconBackend(private val framework: XposedInterface) : VisibilitySurfaceBackend {
|
||||||
|
private val controllers = Collections.newSetFromMap(WeakHashMap<Any, Boolean>())
|
||||||
|
private lateinit var updateStatusBarIcons: Method
|
||||||
|
@Volatile private var blockedObserved = false
|
||||||
|
|
||||||
|
override fun install(classLoader: ClassLoader): Boolean = runCatching {
|
||||||
|
val controller = Class.forName(CONTROLLER, false, classLoader)
|
||||||
|
val container = Class.forName(ICON_CONTAINER, false, classLoader)
|
||||||
|
val entry = Class.forName(NOTIFICATION_ENTRY, false, classLoader)
|
||||||
|
val notificationIcons = controller.getDeclaredField("mNotificationIcons").also {
|
||||||
|
require(it.type == container)
|
||||||
|
it.isAccessible = true
|
||||||
|
}
|
||||||
|
val sbn = entry.getDeclaredField("mSbn").also {
|
||||||
|
require(it.type == StatusBarNotification::class.java)
|
||||||
|
it.isAccessible = true
|
||||||
|
}
|
||||||
|
val update = controller.declaredMethods.singleOrNull {
|
||||||
|
it.name == "updateIconsForLayout" &&
|
||||||
|
it.returnType == Void.TYPE &&
|
||||||
|
it.parameterTypes.contentEquals(
|
||||||
|
arrayOf(
|
||||||
|
Function::class.java,
|
||||||
|
container,
|
||||||
|
Boolean::class.javaPrimitiveType,
|
||||||
|
Boolean::class.javaPrimitiveType,
|
||||||
|
Boolean::class.javaPrimitiveType,
|
||||||
|
Boolean::class.javaPrimitiveType,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} ?: error("OneUI icon update method not found")
|
||||||
|
updateStatusBarIcons = controller.declaredMethods.singleOrNull {
|
||||||
|
it.name == "updateStatusBarIcons" && it.parameterTypes.isEmpty() && it.returnType == Void.TYPE
|
||||||
|
} ?: error("OneUI status-bar icon refresh method not found")
|
||||||
|
update.isAccessible = true
|
||||||
|
updateStatusBarIcons.isAccessible = true
|
||||||
|
framework.hook(updateStatusBarIcons).intercept { chain ->
|
||||||
|
synchronized(controllers) { controllers += chain.thisObject }
|
||||||
|
chain.proceed()
|
||||||
|
}
|
||||||
|
framework.hook(update).intercept { chain ->
|
||||||
|
if (chain.args[1] !== notificationIcons.get(chain.thisObject)) return@intercept chain.proceed()
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
val original = chain.args[0] as? Function<Any, Any?> ?: return@intercept chain.proceed()
|
||||||
|
val wrapped = Function<Any, Any?> { item ->
|
||||||
|
val notification = runCatching {
|
||||||
|
VisibilityNotificationExtractor.from(sbn.get(item) as StatusBarNotification)
|
||||||
|
}.getOrNull()
|
||||||
|
if (notification == null || !ProcessVisibilityPolicyCache.isBlocked(
|
||||||
|
notification,
|
||||||
|
NotificationSurface.UNLOCKED_STATUSBAR,
|
||||||
|
)) {
|
||||||
|
original.apply(item)
|
||||||
|
} else {
|
||||||
|
if (!blockedObserved) {
|
||||||
|
blockedObserved = true
|
||||||
|
Log.i(TAG, "Filtered a OneUI unlocked notification icon")
|
||||||
|
}
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val args = chain.args.toTypedArray()
|
||||||
|
args[0] = wrapped
|
||||||
|
chain.proceed(args)
|
||||||
|
}
|
||||||
|
}.onSuccess {
|
||||||
|
Log.i(TAG, "Installed OneUI unlocked notification-icon backend")
|
||||||
|
}.onFailure {
|
||||||
|
Log.d(TAG, "OneUI unlocked icon boundary is unavailable")
|
||||||
|
}.isSuccess
|
||||||
|
|
||||||
|
override fun onPolicyChanged() {
|
||||||
|
blockedObserved = false
|
||||||
|
Handler(Looper.getMainLooper()).post {
|
||||||
|
val current = synchronized(controllers) { controllers.toList() }
|
||||||
|
current.forEach { controller ->
|
||||||
|
runCatching { updateStatusBarIcons.invoke(controller) }
|
||||||
|
.onFailure { Log.w(TAG, "Could not refresh OneUI unlocked icons", it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val TAG = "NotificationsMaster"
|
||||||
|
const val CONTROLLER = "com.android.systemui.statusbar.phone.LegacyNotificationIconAreaControllerImpl"
|
||||||
|
const val ICON_CONTAINER = "com.android.systemui.statusbar.phone.NotificationIconContainer"
|
||||||
|
const val NOTIFICATION_ENTRY = "com.android.systemui.statusbar.notification.collection.NotificationEntry"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
package se.ajpanton.notificationsmaster.module
|
||||||
|
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.service.notification.StatusBarNotification
|
||||||
|
import android.util.Log
|
||||||
|
import io.github.libxposed.api.XposedInterface
|
||||||
|
import se.ajpanton.notificationsmaster.visibility.NotificationSurface
|
||||||
|
import java.lang.reflect.Method
|
||||||
|
import java.util.Collections
|
||||||
|
import java.util.WeakHashMap
|
||||||
|
|
||||||
|
/** Filters the notification lists handed to Samsung's AOD renderer. */
|
||||||
|
internal class SamsungAodBackend(private val framework: XposedInterface) : VisibilitySurfaceBackend {
|
||||||
|
private val managers = Collections.synchronizedMap(WeakHashMap<Any, State>())
|
||||||
|
private val refreshing = ThreadLocal<Boolean>()
|
||||||
|
private lateinit var updateActive: Method
|
||||||
|
private lateinit var updateVisible: Method
|
||||||
|
@Volatile private var blockedObserved = false
|
||||||
|
|
||||||
|
override fun install(classLoader: ClassLoader): Boolean = runCatching {
|
||||||
|
val manager = Class.forName(MANAGER, false, classLoader)
|
||||||
|
updateActive = manager.exactMethod("updateActiveNotifications", List::class.java)
|
||||||
|
updateVisible = manager.exactMethod(
|
||||||
|
"updateVisibleNotifications",
|
||||||
|
List::class.java,
|
||||||
|
List::class.java,
|
||||||
|
Int::class.javaPrimitiveType!!,
|
||||||
|
)
|
||||||
|
framework.hook(updateActive).intercept { chain ->
|
||||||
|
val raw = notificationList(chain.args[0]) ?: return@intercept chain.proceed()
|
||||||
|
if (refreshing.get() != true) state(chain.thisObject).active = raw
|
||||||
|
proceedFiltered(chain, 0, raw)
|
||||||
|
}
|
||||||
|
framework.hook(updateVisible).intercept { chain ->
|
||||||
|
val first = notificationList(chain.args[0]) ?: return@intercept chain.proceed()
|
||||||
|
val second = notificationList(chain.args[1]) ?: return@intercept chain.proceed()
|
||||||
|
if (refreshing.get() != true) {
|
||||||
|
state(chain.thisObject).visible = VisibleState(first, second, chain.args[2] as Int)
|
||||||
|
}
|
||||||
|
val filteredFirst = filter(first)
|
||||||
|
val filteredSecond = filter(second)
|
||||||
|
if (filteredFirst === first && filteredSecond === second) chain.proceed() else {
|
||||||
|
chain.proceed(arrayOf(filteredFirst, filteredSecond, chain.args[2]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.onSuccess {
|
||||||
|
Log.i(TAG, "Installed Samsung AOD notification backend")
|
||||||
|
}.onFailure {
|
||||||
|
Log.d(TAG, "Samsung AOD notification boundary is unavailable")
|
||||||
|
}.isSuccess
|
||||||
|
|
||||||
|
override fun onPolicyChanged() {
|
||||||
|
blockedObserved = false
|
||||||
|
Handler(Looper.getMainLooper()).post {
|
||||||
|
val current = synchronized(managers) { managers.entries.map { it.key to it.value.copy() } }
|
||||||
|
refreshing.set(true)
|
||||||
|
try {
|
||||||
|
current.forEach { (manager, state) ->
|
||||||
|
state.active?.let { updateActive.invoke(manager, filter(it)) }
|
||||||
|
state.visible?.let {
|
||||||
|
updateVisible.invoke(manager, filter(it.first), filter(it.second), it.type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error: ReflectiveOperationException) {
|
||||||
|
Log.w(TAG, "Could not refresh Samsung AOD notifications", error)
|
||||||
|
} finally {
|
||||||
|
refreshing.remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun proceedFiltered(
|
||||||
|
chain: XposedInterface.Chain,
|
||||||
|
argument: Int,
|
||||||
|
raw: List<StatusBarNotification>,
|
||||||
|
): Any? {
|
||||||
|
val filtered = filter(raw)
|
||||||
|
if (filtered === raw) return chain.proceed()
|
||||||
|
val args = chain.args.toTypedArray()
|
||||||
|
args[argument] = filtered
|
||||||
|
return chain.proceed(args)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun filter(raw: List<StatusBarNotification>): List<StatusBarNotification> {
|
||||||
|
val filtered = raw.filterNot { sbn ->
|
||||||
|
runCatching {
|
||||||
|
ProcessVisibilityPolicyCache.isBlocked(
|
||||||
|
VisibilityNotificationExtractor.from(sbn),
|
||||||
|
NotificationSurface.AOD,
|
||||||
|
)
|
||||||
|
}.getOrDefault(false)
|
||||||
|
}
|
||||||
|
if (filtered.size == raw.size) return raw
|
||||||
|
if (!blockedObserved) {
|
||||||
|
blockedObserved = true
|
||||||
|
Log.i(TAG, "Filtered a Samsung AOD notification")
|
||||||
|
}
|
||||||
|
return filtered
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun notificationList(value: Any?): List<StatusBarNotification>? {
|
||||||
|
val list = value as? List<*> ?: return null
|
||||||
|
if (list.any { it != null && it !is StatusBarNotification }) return null
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
return ArrayList(list as List<StatusBarNotification>)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun state(manager: Any) = synchronized(managers) { managers.getOrPut(manager, ::State) }
|
||||||
|
|
||||||
|
private fun Class<*>.exactMethod(name: String, vararg parameters: Class<*>) =
|
||||||
|
getDeclaredMethod(name, *parameters).also { it.isAccessible = true }
|
||||||
|
|
||||||
|
private data class State(
|
||||||
|
var active: List<StatusBarNotification>? = null,
|
||||||
|
var visible: VisibleState? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
private data class VisibleState(
|
||||||
|
val first: List<StatusBarNotification>,
|
||||||
|
val second: List<StatusBarNotification>,
|
||||||
|
val type: Int,
|
||||||
|
)
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val TAG = "NotificationsMaster"
|
||||||
|
const val MANAGER = "com.samsung.android.uniform.plugins.notification.AODNotificationManager"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
package se.ajpanton.notificationsmaster.module
|
|
||||||
|
|
||||||
internal interface UnlockedIconBackend {
|
|
||||||
fun install(classLoader: ClassLoader)
|
|
||||||
fun onPolicyChanged()
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package se.ajpanton.notificationsmaster.module
|
||||||
|
|
||||||
|
internal interface VisibilitySurfaceBackend {
|
||||||
|
fun install(classLoader: ClassLoader): Boolean
|
||||||
|
fun onPolicyChanged()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user