From 3eba02d25bb82c825b00b43e67c76f2314936383 Mon Sep 17 00:00:00 2001 From: ajp_anton Date: Thu, 23 Jul 2026 09:20:00 +0000 Subject: [PATCH] List installed apps in event settings --- .../notificationlog/EventSettingsFragment.kt | 63 +++++++++++++++++++ .../capture/NotificationCaptureService.kt | 3 + .../notificationlog/capture/SeenApps.kt | 10 +++ .../res/layout/fragment_event_settings.xml | 2 +- 4 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/se/ajpanton/notificationlog/capture/SeenApps.kt diff --git a/app/src/main/java/se/ajpanton/notificationlog/EventSettingsFragment.kt b/app/src/main/java/se/ajpanton/notificationlog/EventSettingsFragment.kt index 2f3b311..6204a52 100644 --- a/app/src/main/java/se/ajpanton/notificationlog/EventSettingsFragment.kt +++ b/app/src/main/java/se/ajpanton/notificationlog/EventSettingsFragment.kt @@ -2,12 +2,16 @@ package se.ajpanton.notificationlog import android.os.Bundle import android.view.View +import android.widget.CheckBox +import android.widget.LinearLayout +import android.widget.TextView import androidx.annotation.StringRes import androidx.fragment.app.Fragment import se.ajpanton.notificationlog.databinding.FragmentEventSettingsBinding import se.ajpanton.notificationlog.settings.AppRuleMode import se.ajpanton.notificationlog.settings.LoggingRuleStore import se.ajpanton.notificationlog.settings.LoggingType +import se.ajpanton.notificationlog.capture.SeenApps class EventSettingsFragment : Fragment(R.layout.fragment_event_settings) { private var binding: FragmentEventSettingsBinding? = null @@ -47,6 +51,7 @@ class EventSettingsFragment : Fragment(R.layout.fragment_event_settings) { binding!!.onlySeenToggle.setOnCheckedChangeListener { _, checked -> save { copy(onlySeenApps = checked) } } binding!!.seenFirstToggle.setOnCheckedChangeListener { _, checked -> save { copy(seenAppsFirst = checked) } } binding!!.routineUpdatesToggle.setOnCheckedChangeListener { _, checked -> save { copy(ignoreRoutineUpdates = checked) } } + reloadAppList() } private fun save(change: se.ajpanton.notificationlog.settings.LoggingRule.() -> se.ajpanton.notificationlog.settings.LoggingRule) { @@ -60,6 +65,64 @@ class EventSettingsFragment : Fragment(R.layout.fragment_event_settings) { binding!!.onlySeenToggle.text = if (rule.onlySeenApps) "Show only seen apps" else "Show all apps" } + private fun reloadAppList() { + val container = binding!!.appList + container.removeAllViews() + val rule = store.ruleFor(type) + val seen = SeenApps.snapshot() + val apps = requireContext().packageManager.getInstalledApplications(0) + .map { info -> + AppRow( + label = requireContext().packageManager.getApplicationLabel(info).toString(), + packageName = info.packageName, + seen = info.packageName in seen, + selected = info.packageName in rule.selectedPackages, + ) + } + .sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.label }) + val shown = if (rule.onlySeenApps) apps.filter { it.seen || it.selected } else apps + val seenRows = shown.filter { it.seen } + val otherRows = shown.filterNot { it.seen } + if (!rule.onlySeenApps && rule.seenAppsFirst && seenRows.isNotEmpty()) { + seenRows.forEach { container.addView(appView(it)) } + if (otherRows.isNotEmpty()) container.addView(separator()) + otherRows.forEach { container.addView(appView(it)) } + } else if (rule.onlySeenApps && seenRows.isNotEmpty() && otherRows.isNotEmpty()) { + seenRows.forEach { container.addView(appView(it)) } + container.addView(separator()) + otherRows.forEach { container.addView(appView(it)) } + } else { + shown.forEach { container.addView(appView(it)) } + } + } + + private fun appView(row: AppRow): View = LinearLayout(requireContext()).apply { + orientation = LinearLayout.HORIZONTAL + val checkbox = CheckBox(context).apply { isChecked = row.selected } + checkbox.setOnCheckedChangeListener { _, checked -> + val current = store.ruleFor(type) + val packages = current.selectedPackages.toMutableSet().apply { + if (checked) add(row.packageName) else remove(row.packageName) + } + store.save(type, current.copy(selectedPackages = packages)) + // Deliberately do not reload: an unchecked unseen row must remain reachable. + } + addView(checkbox) + addView(LinearLayout(context).apply { + orientation = LinearLayout.VERTICAL + layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f) + addView(TextView(context).apply { text = row.label; textSize = 16f; setTypeface(typeface, 1) }) + addView(TextView(context).apply { text = row.packageName; textSize = 12f }) + }) + } + + private fun separator() = View(requireContext()).apply { + layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 1).apply { setMargins(0, 12, 0, 12) } + setBackgroundColor(0x33000000) + } + + private data class AppRow(val label: String, val packageName: String, val seen: Boolean, val selected: Boolean) + companion object { private const val ARG_TITLE = "title" private const val ARG_TYPE = "type" diff --git a/app/src/main/java/se/ajpanton/notificationlog/capture/NotificationCaptureService.kt b/app/src/main/java/se/ajpanton/notificationlog/capture/NotificationCaptureService.kt index 8cf17e5..17b8093 100644 --- a/app/src/main/java/se/ajpanton/notificationlog/capture/NotificationCaptureService.kt +++ b/app/src/main/java/se/ajpanton/notificationlog/capture/NotificationCaptureService.kt @@ -33,6 +33,7 @@ class NotificationCaptureService : NotificationListenerService() { super.onListenerConnected() getActiveNotifications()?.forEach { sbn -> val snapshot = NotificationContents.snapshot(sbn) + SeenApps.markSeen(snapshot.packageName) activeNotifications[snapshot.key] = snapshot record(snapshot, NotificationAction.ALREADY_ACTIVE, LoggingType.APPEARING, includeContents = true) } @@ -40,6 +41,7 @@ class NotificationCaptureService : NotificationListenerService() { override fun onNotificationPosted(sbn: StatusBarNotification) { val snapshot = NotificationContents.snapshot(sbn) + SeenApps.markSeen(snapshot.packageName) val previous = activeNotifications.put(snapshot.key, snapshot) when { previous == null -> record(snapshot, NotificationAction.APPEARED, LoggingType.APPEARING, includeContents = true) @@ -54,6 +56,7 @@ class NotificationCaptureService : NotificationListenerService() { reason: Int, ) { val snapshot = activeNotifications.remove(sbn.key) ?: NotificationContents.snapshot(sbn) + SeenApps.markSeen(snapshot.packageName) record(snapshot, actionForRemoval(reason), LoggingType.DISAPPEARING, includeContents = false) } diff --git a/app/src/main/java/se/ajpanton/notificationlog/capture/SeenApps.kt b/app/src/main/java/se/ajpanton/notificationlog/capture/SeenApps.kt new file mode 100644 index 0000000..d377b69 --- /dev/null +++ b/app/src/main/java/se/ajpanton/notificationlog/capture/SeenApps.kt @@ -0,0 +1,10 @@ +package se.ajpanton.notificationlog.capture + +import java.util.concurrent.ConcurrentHashMap + +/** Process/boot-lifetime record of packages seen by the listener. */ +object SeenApps { + private val packages = ConcurrentHashMap.newKeySet() + fun markSeen(packageName: String) { packages += packageName } + fun snapshot(): Set = packages.toSet() +} diff --git a/app/src/main/res/layout/fragment_event_settings.xml b/app/src/main/res/layout/fragment_event_settings.xml index c50f638..00fe94a 100644 --- a/app/src/main/res/layout/fragment_event_settings.xml +++ b/app/src/main/res/layout/fragment_event_settings.xml @@ -7,6 +7,6 @@ - +