Filter native lockscreen and AOD surfaces
This commit is contained in:
+1
-1
@@ -16,7 +16,7 @@ class VisibilityModuleFixtureTest {
|
||||
requireFixtureMode()
|
||||
store.save(VisibilityPolicy(apps = listOf(AppVisibilityPolicy(
|
||||
packageName = HELPER_PACKAGE,
|
||||
blockedSurfaces = setOf(NotificationSurface.UNLOCKED_STATUSBAR),
|
||||
blockedSurfaces = NotificationSurface.entries.toSet(),
|
||||
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.util.Collections
|
||||
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. */
|
||||
internal class Aosp34UnlockedIconBackend(private val framework: XposedInterface) : UnlockedIconBackend {
|
||||
/** API 34 uses distinct mapper IDs for its unlocked and AOD icon containers. */
|
||||
internal class Aosp34IconBackend(private val framework: XposedInterface) : VisibilitySurfaceBackend {
|
||||
private val controllers = Collections.newSetFromMap(WeakHashMap<Any, Boolean>())
|
||||
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) {
|
||||
if (Build.VERSION.SDK_INT != 34) return
|
||||
runCatching {
|
||||
override fun install(classLoader: ClassLoader): Boolean {
|
||||
if (Build.VERSION.SDK_INT != 34) return false
|
||||
return runCatching {
|
||||
val controller = Class.forName(CONTROLLER, false, classLoader)
|
||||
val mapper = Class.forName(ICON_MAPPER, 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 {
|
||||
it.name == "updateStatusBarIcons" && it.parameterTypes.isEmpty() && it.returnType == Void.TYPE
|
||||
} ?: error("Unlocked icon refresh method not found")
|
||||
updateAodNotificationIcons = controller.declaredMethods.singleOrNull {
|
||||
it.name == "updateAodNotificationIcons" && it.parameterTypes.isEmpty() && it.returnType == Void.TYPE
|
||||
}
|
||||
apply.isAccessible = true
|
||||
updateStatusBarIcons.isAccessible = true
|
||||
framework.hook(updateStatusBarIcons).intercept { chain ->
|
||||
synchronized(controllers) { controllers += chain.thisObject }
|
||||
chain.proceed()
|
||||
updateAodNotificationIcons?.isAccessible = true
|
||||
listOfNotNull(updateStatusBarIcons, updateAodNotificationIcons).forEach { refresh ->
|
||||
framework.hook(refresh).intercept { chain ->
|
||||
synchronized(controllers) { controllers += chain.thisObject }
|
||||
chain.proceed()
|
||||
}
|
||||
}
|
||||
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 {
|
||||
VisibilityNotificationExtractor.from(sbn.get(chain.args[0]) as StatusBarNotification)
|
||||
}.getOrNull() ?: return@intercept chain.proceed()
|
||||
if (!ProcessVisibilityPolicyCache.isBlocked(
|
||||
notification,
|
||||
NotificationSurface.UNLOCKED_STATUSBAR,
|
||||
surface,
|
||||
)) return@intercept chain.proceed()
|
||||
if (!blockedObserved) {
|
||||
blockedObserved = true
|
||||
Log.i(TAG, "Filtered an API 34 unlocked notification icon")
|
||||
if (blockedSurfaces.add(surface)) {
|
||||
Log.i(TAG, "Filtered an API 34 ${surface.name} notification icon")
|
||||
}
|
||||
null
|
||||
}
|
||||
@@ -62,22 +75,25 @@ internal class Aosp34UnlockedIconBackend(private val framework: XposedInterface)
|
||||
Log.i(TAG, "Installed AOSP API 34 unlocked notification-icon backend")
|
||||
}.onFailure {
|
||||
Log.w(TAG, "AOSP API 34 unlocked icon shape is unsupported; leaving icons unchanged", it)
|
||||
}
|
||||
}.isSuccess
|
||||
}
|
||||
|
||||
override fun onPolicyChanged() {
|
||||
blockedObserved = false
|
||||
blockedSurfaces.clear()
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
val current = synchronized(controllers) { controllers.toList() }
|
||||
current.forEach { controller ->
|
||||
runCatching { updateStatusBarIcons.invoke(controller) }
|
||||
.onFailure { Log.w(TAG, "Could not refresh API 34 unlocked icons", it) }
|
||||
runCatching {
|
||||
updateStatusBarIcons.invoke(controller)
|
||||
updateAodNotificationIcons?.invoke(controller)
|
||||
}.onFailure { Log.w(TAG, "Could not refresh API 34 notification icons", it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val TAG = "NotificationsMaster"
|
||||
const val AOD_CLASS_ID = 0
|
||||
const val STATUS_BAR_CLASS_ID = 1
|
||||
const val CONTROLLER = "com.android.systemui.statusbar.phone.NotificationIconAreaController"
|
||||
const val ICON_MAPPER = "$CONTROLLER\$\$ExternalSyntheticLambda2"
|
||||
+59
-22
@@ -12,17 +12,18 @@ import java.lang.reflect.Proxy
|
||||
import java.util.LinkedHashSet
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
/** Filters only the modern status-bar icon flow; the notification/shade model is untouched. */
|
||||
internal class AospModernUnlockedIconBackend(private val framework: XposedInterface) : UnlockedIconBackend {
|
||||
/** Filters the modern unlocked and AOD icon flows; the notification/shade model is untouched. */
|
||||
internal class AospModernIconBackend(private val framework: XposedInterface) : VisibilitySurfaceBackend {
|
||||
private val metadata = ConcurrentHashMap<String, VisibilityNotification>()
|
||||
private val activeKeys = ConcurrentHashMap<NotificationSurface, Set<String>>()
|
||||
@Volatile private var policyChanges: Any? = null
|
||||
private var policyRevision = 0L
|
||||
@Volatile private var lastFilterSignature = -1L
|
||||
private val lastFilterSignatures = ConcurrentHashMap<NotificationSurface, Long>()
|
||||
@Volatile private var captureFailureLogged = false
|
||||
|
||||
override fun install(classLoader: ClassLoader) {
|
||||
if (Build.VERSION.SDK_INT !in 35..36) return
|
||||
runCatching {
|
||||
override fun install(classLoader: ClassLoader): Boolean {
|
||||
if (Build.VERSION.SDK_INT !in 35..36) return false
|
||||
return runCatching {
|
||||
val entryClass = Class.forName(NOTIFICATION_ENTRY, false, classLoader)
|
||||
val modelClass = Class.forName(ACTIVE_NOTIFICATION_MODEL, false, classLoader)
|
||||
val modelAccesses = buildList {
|
||||
@@ -39,11 +40,12 @@ internal class AospModernUnlockedIconBackend(private val framework: XposedInterf
|
||||
installMetadataCapture(classLoader, entryClass, modelClass)
|
||||
installFlowRefresh(classLoader)
|
||||
installStatusBarFilter(classLoader, modelAccesses)
|
||||
installAodFilter(classLoader, modelAccesses)
|
||||
}.onSuccess {
|
||||
Log.i(TAG, "Installed AOSP API ${Build.VERSION.SDK_INT} unlocked notification-icon backend")
|
||||
}.onFailure {
|
||||
Log.w(TAG, "AOSP API ${Build.VERSION.SDK_INT} unlocked icon shape is unsupported; leaving icons unchanged", it)
|
||||
}
|
||||
}.isSuccess
|
||||
}
|
||||
|
||||
@Synchronized override fun onPolicyChanged() {
|
||||
@@ -142,6 +144,9 @@ internal class AospModernUnlockedIconBackend(private val framework: XposedInterf
|
||||
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
|
||||
val constructor = interactor.declaredConstructors.singleOrNull {
|
||||
it.parameterTypes.map { type -> type.name } == listOf(
|
||||
@@ -164,17 +169,24 @@ internal class AospModernUnlockedIconBackend(private val framework: XposedInterf
|
||||
framework.hook(target).intercept { chain ->
|
||||
val result = chain.proceed()
|
||||
runCatching {
|
||||
val upstream = 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)
|
||||
field.set(chain.thisObject, refreshed(field.get(chain.thisObject)))
|
||||
}.onFailure { Log.w(TAG, "Could not attach visibility refresh flow; leaving icons unchanged", it) }
|
||||
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(
|
||||
@@ -189,19 +201,42 @@ internal class AospModernUnlockedIconBackend(private val framework: XposedInterf
|
||||
emit.isAccessible = true
|
||||
framework.hook(emit).intercept { chain ->
|
||||
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]))
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
if (entries.any { model -> model != null && modelAccesses.none { it.type.isInstance(model) } }) return value
|
||||
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 }
|
||||
}.toSet()
|
||||
metadata.keys.retainAll(activeKeys)
|
||||
activeKeys[surface] = surfaceKeys
|
||||
metadata.keys.retainAll(activeKeys.values.flatten().toSet())
|
||||
var changed = false
|
||||
var missingMetadata = 0
|
||||
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
|
||||
val notification = captured ?: VisibilityNotification(packageName, key)
|
||||
val blocked = (!hasExceptions || captured != null) &&
|
||||
ProcessVisibilityPolicyCache.isBlocked(notification, NotificationSurface.UNLOCKED_STATUSBAR)
|
||||
ProcessVisibilityPolicyCache.isBlocked(notification, surface)
|
||||
changed = changed || blocked
|
||||
!blocked
|
||||
}
|
||||
val hidden = entries.size - filtered.size
|
||||
val signature = ((entries.size * 31L + hidden) * 31L + missingMetadata)
|
||||
if (signature != lastFilterSignature) {
|
||||
lastFilterSignature = signature
|
||||
if (lastFilterSignatures.put(surface, signature) != signature) {
|
||||
Log.i(
|
||||
TAG,
|
||||
"Unlocked notification icons: input=${entries.size}, hidden=$hidden, " +
|
||||
"${surface.name} notification icons: input=${entries.size}, hidden=$hidden, " +
|
||||
"missingMetadata=$missingMetadata",
|
||||
)
|
||||
}
|
||||
@@ -262,6 +296,9 @@ internal class AospModernUnlockedIconBackend(private val framework: XposedInterf
|
||||
"com.android.systemui.statusbar.notification.icon.domain.interactor.StatusBarNotificationIconsInteractor"
|
||||
const val STATUS_BAR_VIEW_MODEL =
|
||||
"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 =
|
||||
"com.android.systemui.statusbar.notification.icon.ui.viewmodel." +
|
||||
"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.
|
||||
*/
|
||||
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) {
|
||||
if (param.packageName !in VisibilityPolicySync.TARGET_PACKAGES) return
|
||||
if (param.packageName == SYSTEM_UI_PACKAGE) {
|
||||
if (systemUiBackend != null) return
|
||||
val backend = if (android.os.Build.VERSION.SDK_INT == 34) {
|
||||
Aosp34UnlockedIconBackend(this)
|
||||
} else AospModernUnlockedIconBackend(this)
|
||||
backend.install(param.classLoader)
|
||||
systemUiBackend = backend
|
||||
ProcessVisibilityPolicyCache.installWhenReady(backend::onPolicyChanged)
|
||||
} else ProcessVisibilityPolicyCache.installWhenReady()
|
||||
if (systemUiBackends != null) return
|
||||
val unlocked = listOf(
|
||||
OneUiUnlockedIconBackend(this),
|
||||
Aosp34IconBackend(this),
|
||||
AospModernIconBackend(this),
|
||||
).firstOrNull { it.install(param.classLoader) }
|
||||
systemUiBackends = listOfNotNull(unlocked) + listOf(
|
||||
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) {
|
||||
|
||||
@@ -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