Keep debug notifications independent
This commit is contained in:
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+119
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user