Apply per-event notification logging rules

This commit is contained in:
ajp_anton
2026-07-23 09:13:57 +00:00
parent 6d77a43c55
commit d5aee8495f
6 changed files with 135 additions and 13 deletions
@@ -8,6 +8,9 @@ import android.util.Log
import se.ajpanton.notificationlog.data.EncryptedNotificationLogStore
import se.ajpanton.notificationlog.model.NotificationAction
import se.ajpanton.notificationlog.model.NotificationLogEntry
import se.ajpanton.notificationlog.settings.LoggingRuleStore
import se.ajpanton.notificationlog.settings.LoggingType
import se.ajpanton.notificationlog.settings.NotificationRuleEvaluator
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
@@ -15,10 +18,12 @@ class NotificationCaptureService : NotificationListenerService() {
private val activeNotifications = mutableMapOf<String, NotificationSnapshot>()
private lateinit var logStore: EncryptedNotificationLogStore
private lateinit var writeExecutor: ExecutorService
private lateinit var ruleStore: LoggingRuleStore
override fun onCreate() {
super.onCreate()
logStore = EncryptedNotificationLogStore(this)
ruleStore = LoggingRuleStore(this)
writeExecutor = Executors.newSingleThreadExecutor { runnable ->
Thread(runnable, "notification-log-writer")
}
@@ -29,7 +34,7 @@ class NotificationCaptureService : NotificationListenerService() {
getActiveNotifications()?.forEach { sbn ->
val snapshot = NotificationContents.snapshot(sbn)
activeNotifications[snapshot.key] = snapshot
record(snapshot, NotificationAction.ALREADY_ACTIVE, includeContents = true)
record(snapshot, NotificationAction.ALREADY_ACTIVE, LoggingType.APPEARING, includeContents = true)
}
}
@@ -37,8 +42,9 @@ class NotificationCaptureService : NotificationListenerService() {
val snapshot = NotificationContents.snapshot(sbn)
val previous = activeNotifications.put(snapshot.key, snapshot)
when {
previous == null -> record(snapshot, NotificationAction.APPEARED, includeContents = true)
previous.contents != snapshot.contents -> record(snapshot, NotificationAction.EDITED, includeContents = true)
previous == null -> record(snapshot, NotificationAction.APPEARED, LoggingType.APPEARING, includeContents = true)
previous.textContents != snapshot.textContents || previous.hasImage != snapshot.hasImage ->
record(snapshot, NotificationAction.EDITED, LoggingType.EDITS, includeContents = true)
}
}
@@ -48,7 +54,7 @@ class NotificationCaptureService : NotificationListenerService() {
reason: Int,
) {
val snapshot = activeNotifications.remove(sbn.key) ?: NotificationContents.snapshot(sbn)
record(snapshot, actionForRemoval(reason), includeContents = false)
record(snapshot, actionForRemoval(reason), LoggingType.DISAPPEARING, includeContents = false)
}
override fun onDestroy() {
@@ -56,14 +62,20 @@ class NotificationCaptureService : NotificationListenerService() {
super.onDestroy()
}
private fun record(snapshot: NotificationSnapshot, action: NotificationAction, includeContents: Boolean) {
private fun record(
snapshot: NotificationSnapshot,
action: NotificationAction,
loggingType: LoggingType,
includeContents: Boolean,
) {
if (!NotificationRuleEvaluator.allows(ruleStore.ruleFor(loggingType), snapshot.packageName)) return
val appName = appName(snapshot.packageName)
val entry = NotificationLogEntry(
recordedAtEpochMillis = System.currentTimeMillis(),
packageName = snapshot.packageName,
appName = appName,
action = action,
contents = if (includeContents) snapshot.contents else null,
contents = if (includeContents) visibleContents(snapshot) else null,
)
writeExecutor.execute {
try {
@@ -74,6 +86,16 @@ class NotificationCaptureService : NotificationListenerService() {
}
}
private fun visibleContents(snapshot: NotificationSnapshot): String? {
val text = snapshot.textContents?.takeIf {
NotificationRuleEvaluator.allows(ruleStore.ruleFor(LoggingType.TEXT_CONTENT), snapshot.packageName)
}
val image = snapshot.hasImage && NotificationRuleEvaluator.allows(
ruleStore.ruleFor(LoggingType.IMAGE_CONTENT), snapshot.packageName,
)
return listOfNotNull(text, if (image) "[image]" else null).joinToString("").ifEmpty { null }
}
private fun appName(packageName: String): String = try {
val applicationInfo = packageManager.getApplicationInfo(packageName, 0)
packageManager.getApplicationLabel(applicationInfo).toString()
@@ -7,7 +7,8 @@ import android.service.notification.StatusBarNotification
data class NotificationSnapshot(
val key: String,
val packageName: String,
val contents: String?,
val textContents: String?,
val hasImage: Boolean,
)
object NotificationContents {
@@ -25,18 +26,16 @@ object NotificationContents {
).forEach { message ->
listOfNotNull(message.senderPerson?.name, message.text).joinToString(": ").addTo(parts)
}
if (extras.containsKey(Notification.EXTRA_PICTURE) ||
extras.containsKey(Notification.EXTRA_PICTURE_ICON)
) {
"[image]".addTo(parts)
}
return parts.takeIf { it.isNotEmpty() }?.joinToString("")
}
fun snapshot(sbn: StatusBarNotification) = NotificationSnapshot(
key = sbn.key,
packageName = sbn.packageName,
contents = extract(sbn.notification),
textContents = extract(sbn.notification),
hasImage = sbn.notification.extras?.let {
it.containsKey(Notification.EXTRA_PICTURE) || it.containsKey(Notification.EXTRA_PICTURE_ICON)
} == true,
)
private fun CharSequence.addTo(parts: MutableSet<String>) {