Keep debug notifications independent

This commit is contained in:
ajp_anton
2026-08-30 01:40:05 +00:00
parent 738ae25f1d
commit e814a1aab3
3 changed files with 125 additions and 1 deletions
@@ -151,7 +151,9 @@ object DebugNotifications {
private fun cancelAbove(context: Context, manager: NotificationManager, desired: Int) { private fun cancelAbove(context: Context, manager: NotificationManager, desired: Int) {
manager.activeNotifications.filter { it.packageName == context.packageName }.forEach { sbn -> manager.activeNotifications.filter { it.packageName == context.packageName }.forEach { sbn ->
val number = sbn.id - ID_BASE val number = sbn.id - ID_BASE
if (number in 1..MAX_COUNT && number > desired) cancel(manager, sbn) val autoSummary = !isDebug(sbn.notification) &&
sbn.notification.flags and Notification.FLAG_GROUP_SUMMARY != 0
if (autoSummary || number in 1..MAX_COUNT && number > desired) cancel(manager, sbn)
} }
} }
@@ -0,0 +1,119 @@
package se.ajpanton.notificationsmaster.module
import android.app.Notification
import android.service.notification.StatusBarNotification
import android.util.Log
import io.github.libxposed.api.XposedInterface
import java.lang.reflect.Method
/** Prevents Android from collapsing this app's marked layout-test notifications. */
internal class DebugNotificationAutoGroupBridge(private val framework: XposedInterface) {
fun install(classLoader: ClassLoader) {
val groupHelper = findClass(GROUP_HELPER, classLoader) ?: return unavailable()
val recordClass = findClass(NOTIFICATION_RECORD, classLoader) ?: return unavailable()
val notificationManager = findClass(NOTIFICATION_MANAGER, classLoader) ?: return unavailable()
val blockGrouping = XposedInterface.Hooker { chain ->
if (!isDebugRecord(chain.args.firstOrNull())) return@Hooker chain.proceed()
if ((chain.executable as? Method)?.returnType == Boolean::class.javaPrimitiveType) false else null
}
GROUP_METHODS.forEach { hookMethods(groupHelper, it, blockGrouping) }
hookMethods(recordClass, "setOverrideGroupKey") { chain ->
if (isDebugRecord(chain.thisObject)) null else chain.proceed()
}
val dropSummary = XposedInterface.Hooker { chain ->
val info = notificationInfo(chain.args.toTypedArray())
if (info?.packageName == APP_PACKAGE && !info.isDebug && info.isGroupSummary) null
else chain.proceed()
}
hookMethods(notificationManager, "enqueueNotificationInternal", dropSummary)
hookMethods(notificationManager, "enqueueNotificationInternalLocked", dropSummary)
Log.i(TAG, "Installed debug-notification auto-group guard")
}
private fun hookMethods(type: Class<*>, name: String, hooker: XposedInterface.Hooker) {
type.declaredMethods.filter { it.name == name }.forEach { method ->
method.isAccessible = true
framework.hook(method).intercept(hooker)
}
}
private fun isDebugRecord(record: Any?): Boolean {
val sbn = invoke(record, "getSbn") as? StatusBarNotification
val packageName = sbn?.packageName ?: invoke(record, "getPackageName") as? String
val notification = sbn?.notification ?: invoke(record, "getNotification") as? Notification
return packageName == APP_PACKAGE && notification?.extras?.getBoolean(DEBUG_EXTRA, false) == true
}
private fun notificationInfo(args: Array<Any?>): NotificationInfo? {
var packageName: String? = null
var notification: Notification? = null
args.forEach { arg ->
when (arg) {
is StatusBarNotification -> {
packageName = arg.packageName
notification = arg.notification
}
is Notification -> notification = arg
is String -> if (packageName == null && arg == APP_PACKAGE) packageName = arg
else -> if (arg?.javaClass?.name?.contains("NotificationRecord") == true) {
val sbn = invoke(arg, "getSbn") as? StatusBarNotification
packageName = sbn?.packageName ?: invoke(arg, "getPackageName") as? String ?: packageName
notification = sbn?.notification ?: invoke(arg, "getNotification") as? Notification ?: notification
}
}
}
val value = notification ?: return null
return NotificationInfo(
packageName ?: return null,
value.extras?.getBoolean(DEBUG_EXTRA, false) == true,
value.flags and Notification.FLAG_GROUP_SUMMARY != 0,
)
}
private fun invoke(instance: Any?, name: String): Any? {
var type = instance?.javaClass
while (type != null) {
val method = runCatching { type.getDeclaredMethod(name) }.getOrNull()
if (method != null) return runCatching {
method.isAccessible = true
method.invoke(instance)
}.getOrNull()
type = type.superclass
}
return null
}
private fun findClass(name: String, classLoader: ClassLoader) =
runCatching { Class.forName(name, false, classLoader) }.getOrNull()
private fun unavailable() {
Log.w(TAG, "Debug-notification auto-group boundary is unavailable")
}
private data class NotificationInfo(
val packageName: String,
val isDebug: Boolean,
val isGroupSummary: Boolean,
)
private companion object {
const val TAG = "NotificationsMaster"
const val APP_PACKAGE = "se.ajpanton.notificationsmaster"
const val DEBUG_EXTRA = "se.ajpanton.notificationsmaster.DEBUG_NOTIFICATION"
const val GROUP_HELPER = "com.android.server.notification.GroupHelper"
const val NOTIFICATION_RECORD = "com.android.server.notification.NotificationRecord"
const val NOTIFICATION_MANAGER = "com.android.server.notification.NotificationManagerService"
val GROUP_METHODS = listOf(
"onNotificationPosted",
"onNotificationRemoved",
"maybeGroup",
"maybeUngroup",
"autoGroup",
"autogroup",
"maybeAutoGroup",
"shouldAutoGroup",
"isEligibleForAutoGroup",
)
}
}
@@ -43,6 +43,9 @@ class NotificationsMasterModule : XposedModule() {
override fun onSystemServerStarting(param: XposedModuleInterface.SystemServerStartingParam) { override fun onSystemServerStarting(param: XposedModuleInterface.SystemServerStartingParam) {
SystemAlertPolicyCache.installWhenReady() SystemAlertPolicyCache.installWhenReady()
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
installBridge("debug notification auto-group") {
DebugNotificationAutoGroupBridge(this).install(param.classLoader)
}
installBridge("notification attention") { installBridge("notification attention") {
NotificationAttentionBridge(this, ::diagnoseAttentionHelperEvent).install(param.classLoader) NotificationAttentionBridge(this, ::diagnoseAttentionHelperEvent).install(param.classLoader)
} }