Improve grouped and messaging notification capture

This commit is contained in:
ajp_anton
2026-07-27 12:03:33 +00:00
parent 4d84ff98c7
commit 3dc87b417a
8 changed files with 79 additions and 3 deletions
@@ -24,6 +24,7 @@ import se.ajpanton.notificationlog.settings.TimestampZone
import se.ajpanton.notificationlog.settings.LoggingType
import se.ajpanton.notificationlog.settings.LoggingRuleStore
import se.ajpanton.notificationlog.settings.AppLockStore
import se.ajpanton.notificationlog.settings.CaptureSettingsStore
import se.ajpanton.notificationlog.data.EncryptedNotificationLogStore
import se.ajpanton.notificationlog.data.EncryptedImageStore
import se.ajpanton.notificationlog.capture.NotificationCaptureService
@@ -84,6 +85,11 @@ class SettingsFragment : Fragment(R.layout.fragment_settings) {
}
setupCopyControls()
setupEventMasterToggles()
val captureSettings = CaptureSettingsStore(requireContext())
binding!!.groupSummaries.isChecked = captureSettings.logGroupSummaries
binding!!.groupSummaries.setOnCheckedChangeListener { _, checked ->
captureSettings.logGroupSummaries = checked
}
binding!!.exportLogs.setOnClickListener { confirmExport() }
binding!!.clearLogs.setOnClickListener { confirmClearLogs() }
binding!!.notificationAccess.setOnClickListener { startActivity(Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS)) }
@@ -0,0 +1,7 @@
package se.ajpanton.notificationlog.capture
/** Applies the global, Android-provided group-summary classification without inspecting text. */
object GroupSummaryPolicy {
fun shouldLog(snapshot: NotificationSnapshot, logGroupSummaries: Boolean): Boolean =
logGroupSummaries || !snapshot.isGroupSummary
}
@@ -12,6 +12,7 @@ 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 se.ajpanton.notificationlog.settings.CaptureSettingsStore
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
@@ -20,12 +21,14 @@ class NotificationCaptureService : NotificationListenerService() {
private lateinit var logStore: EncryptedNotificationLogStore
private lateinit var writeExecutor: ExecutorService
private lateinit var ruleStore: LoggingRuleStore
private lateinit var captureSettings: CaptureSettingsStore
private lateinit var imageStore: EncryptedImageStore
override fun onCreate() {
super.onCreate()
logStore = EncryptedNotificationLogStore(this)
ruleStore = LoggingRuleStore(this)
captureSettings = CaptureSettingsStore(this)
imageStore = EncryptedImageStore(this)
writeExecutor = Executors.newSingleThreadExecutor { runnable ->
Thread(runnable, "notification-log-writer")
@@ -76,6 +79,7 @@ class NotificationCaptureService : NotificationListenerService() {
includeContents: Boolean,
previousSnapshot: NotificationSnapshot? = null,
) {
if (!GroupSummaryPolicy.shouldLog(snapshot, captureSettings.logGroupSummaries)) return
if (!NotificationRuleEvaluator.allows(ruleStore.ruleFor(loggingType), snapshot.packageName)) return
val appName = appName(snapshot.packageName)
val retainImage = includeContents && snapshot.imageBytes != null &&
@@ -117,7 +121,7 @@ class NotificationCaptureService : NotificationListenerService() {
ruleStore.ruleFor(LoggingType.IMAGE_CONTENT), snapshot.packageName,
)
return listOfNotNull(text, if (image) "[image]" else null)
.joinToString("")
.joinToString("\n")
.take(MAX_CONTENT_CHARACTERS)
.ifEmpty { null }
}
@@ -15,6 +15,7 @@ data class NotificationSnapshot(
val packageName: String,
val textContents: String?,
val hasImage: Boolean,
val isGroupSummary: Boolean,
val isRoutine: Boolean,
val imageBytes: ByteArray?,
)
@@ -23,18 +24,25 @@ object NotificationContents {
fun extract(notification: Notification): String? {
val parts = linkedSetOf<String>()
val extras = notification.extras ?: Bundle.EMPTY
messagingContents(extras)?.let { return it }
extras.getCharSequence(Notification.EXTRA_TITLE)?.addTo(parts)
extras.getCharSequence(Notification.EXTRA_TEXT)?.addTo(parts)
extras.getCharSequence(Notification.EXTRA_BIG_TEXT)?.addTo(parts)
extras.getCharSequence(Notification.EXTRA_SUB_TEXT)?.addTo(parts)
extras.getCharSequence(Notification.EXTRA_SUMMARY_TEXT)?.addTo(parts)
extras.getCharSequenceArray(Notification.EXTRA_TEXT_LINES)?.forEach { it?.addTo(parts) }
return parts.takeIf { it.isNotEmpty() }?.joinToString("\n")
}
/** MessagingStyle also populates generic title/text fields with an alternate rendering. */
private fun messagingContents(extras: Bundle): String? {
val parts = linkedSetOf<String>()
Notification.MessagingStyle.Message.getMessagesFromBundleArray(
extras.getParcelableArray(Notification.EXTRA_MESSAGES, Bundle::class.java),
).forEach { message ->
listOfNotNull(message.senderPerson?.name, message.text).joinToString(": ").addTo(parts)
}
return parts.takeIf { it.isNotEmpty() }?.joinToString("")
return parts.takeIf { it.isNotEmpty() }?.joinToString("\n")
}
fun snapshot(sbn: StatusBarNotification, context: Context): NotificationSnapshot {
@@ -49,6 +57,7 @@ object NotificationContents {
hasImage = extras?.let {
it.containsKey(Notification.EXTRA_PICTURE) || it.containsKey(Notification.EXTRA_PICTURE_ICON)
} == true,
isGroupSummary = notification.flags and Notification.FLAG_GROUP_SUMMARY != 0,
isRoutine = extras?.getBoolean(Notification.EXTRA_SHOW_CHRONOMETER, false) == true ||
extras?.containsKey(Notification.EXTRA_PROGRESS) == true,
imageBytes = picture?.toPng() ?: pictureIcon?.let { icon -> icon.loadDrawable(context)?.toBitmap()?.toPng() },
@@ -0,0 +1,18 @@
package se.ajpanton.notificationlog.settings
import android.content.Context
import androidx.core.content.edit
/** Global capture choices that are independent of a particular event type. */
class CaptureSettingsStore(context: Context) {
private val preferences = context.applicationContext.getSharedPreferences(FILE_NAME, Context.MODE_PRIVATE)
var logGroupSummaries: Boolean
get() = preferences.getBoolean(LOG_GROUP_SUMMARIES, true)
set(value) = preferences.edit { putBoolean(LOG_GROUP_SUMMARIES, value) }
private companion object {
const val FILE_NAME = "capture-settings"
const val LOG_GROUP_SUMMARIES = "log_group_summaries"
}
}
@@ -21,6 +21,12 @@
android:layout_marginTop="24dp"
android:text="Event logging" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/group_summaries"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Log Android group summaries" />
<LinearLayout
android:id="@+id/event_master_toggles"
android:layout_width="match_parent"