From 55057767c13e8aea339844bdd49557747c0c6af6 Mon Sep 17 00:00:00 2001 From: ajp_anton Date: Mon, 27 Jul 2026 13:27:08 +0000 Subject: [PATCH] Clarify seen and checked app filtering --- .../notificationlog/EventSettingsFragment.kt | 26 ++++++-- .../settings/AppListOrdering.kt | 12 +++- .../settings/AppListOrderingTest.kt | 60 +++++++++++++++++-- 3 files changed, 87 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/se/ajpanton/notificationlog/EventSettingsFragment.kt b/app/src/main/java/se/ajpanton/notificationlog/EventSettingsFragment.kt index d5c76ae..50a57ab 100644 --- a/app/src/main/java/se/ajpanton/notificationlog/EventSettingsFragment.kt +++ b/app/src/main/java/se/ajpanton/notificationlog/EventSettingsFragment.kt @@ -70,7 +70,6 @@ class FilterAppsFragment : Fragment(R.layout.fragment_event_settings) { binding!!.appRuleToggle.isChecked = rule.mode == AppRuleMode.WHITELIST binding!!.onlySeenToggle.isChecked = rule.onlySeenApps binding!!.seenFirstToggle.isChecked = rule.seenAppsFirst - binding!!.seenFirstToggle.isEnabled = !rule.onlySeenApps updateRuleLabels(rule) binding!!.appRuleToggle.setOnCheckedChangeListener { _, checked -> save { copy(mode = if (checked) AppRuleMode.WHITELIST else AppRuleMode.BLACKLIST) } @@ -87,7 +86,7 @@ class FilterAppsFragment : Fragment(R.layout.fragment_event_settings) { private fun updateRuleLabels(rule: AppFilterSettings) { binding!!.appRuleToggle.text = modeLabel(rule.mode) - binding!!.onlySeenToggle.text = "Show only seen apps" + binding!!.onlySeenToggle.text = "Show only seen and checked apps" binding!!.seenFirstToggle.text = "Show seen apps first" } @@ -121,7 +120,7 @@ class FilterAppsFragment : Fragment(R.layout.fragment_event_settings) { selected = info.packageName in rule.selectedPackages, ) } - val items = AppListOrdering.items(apps, rule.onlySeenApps, rule.seenAppsFirst) + val items = AppListOrdering.items(apps, rule.onlySeenApps, rule.seenAppsFirst, rule.mode) activity?.runOnUiThread { if (binding === currentBinding && generation == appLoadGeneration) { appAdapter.submit(items) @@ -151,7 +150,11 @@ class FilterAppsFragment : Fragment(R.layout.fragment_event_settings) { notifyDataSetChanged() } - override fun getItemViewType(position: Int): Int = if (items[position] is AppListItem.App) APP else SEPARATOR + override fun getItemViewType(position: Int): Int = when (items[position]) { + is AppListItem.App -> APP + is AppListItem.SectionTitle -> SECTION_TITLE + AppListItem.Separator -> SEPARATOR + } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = if (viewType == APP) { val row = LinearLayout(parent.context).apply { @@ -170,6 +173,15 @@ class FilterAppsFragment : Fragment(R.layout.fragment_event_settings) { row.addView(checkbox) row.addView(textColumn) AppHolder(row, checkbox, label, packageName) + } else if (viewType == SECTION_TITLE) { + SectionTitleHolder(TextView(parent.context).apply { + layoutParams = RecyclerView.LayoutParams( + RecyclerView.LayoutParams.MATCH_PARENT, + RecyclerView.LayoutParams.WRAP_CONTENT, + ).apply { setMargins(0, dp(parent, 8), 0, dp(parent, 4)) } + setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 12f) + setTypeface(typeface, android.graphics.Typeface.BOLD) + }) } else { val divider = View(parent.context).apply { layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, dp(parent, 1)).apply { @@ -197,6 +209,9 @@ class FilterAppsFragment : Fragment(R.layout.fragment_event_settings) { } } } + if (holder is SectionTitleHolder && item is AppListItem.SectionTitle) { + holder.text.text = item.value + } } override fun getItemCount(): Int = items.size @@ -210,9 +225,12 @@ class FilterAppsFragment : Fragment(R.layout.fragment_event_settings) { private class SeparatorHolder(view: View) : RecyclerView.ViewHolder(view) + private class SectionTitleHolder(val text: TextView) : RecyclerView.ViewHolder(text) + private companion object { const val APP = 0 const val SEPARATOR = 1 + const val SECTION_TITLE = 2 fun dp(parent: ViewGroup, value: Int): Int = (value * parent.resources.displayMetrics.density).toInt() } } diff --git a/app/src/main/java/se/ajpanton/notificationlog/settings/AppListOrdering.kt b/app/src/main/java/se/ajpanton/notificationlog/settings/AppListOrdering.kt index 1e00aea..14167d5 100644 --- a/app/src/main/java/se/ajpanton/notificationlog/settings/AppListOrdering.kt +++ b/app/src/main/java/se/ajpanton/notificationlog/settings/AppListOrdering.kt @@ -3,25 +3,35 @@ package se.ajpanton.notificationlog.settings data class ListedApp(val label: String, val packageName: String, val seen: Boolean, val selected: Boolean) sealed interface AppListItem { data class App(val value: ListedApp) : AppListItem + data class SectionTitle(val value: String) : AppListItem data object Separator : AppListItem } object AppListOrdering { - fun items(allApps: List, onlySeen: Boolean, seenAppsFirst: Boolean): List { + fun items( + allApps: List, + onlySeen: Boolean, + seenAppsFirst: Boolean, + mode: AppRuleMode, + ): List { val apps = allApps.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.label }) val shown = if (onlySeen) apps.filter { it.seen || it.selected } else apps val seen = shown.filter { it.seen } val selectedUnseen = shown.filter { !it.seen } if (!seenAppsFirst || seen.isEmpty()) return shown.map(AppListItem::App) return buildList { + if (!onlySeen) add(AppListItem.SectionTitle("Seen apps")) + if (onlySeen && selectedUnseen.isNotEmpty()) add(AppListItem.SectionTitle("Seen apps")) seen.forEach { add(AppListItem.App(it)) } if (onlySeen) { if (selectedUnseen.isNotEmpty()) { add(AppListItem.Separator) + add(AppListItem.SectionTitle(if (mode == AppRuleMode.BLACKLIST) "Blacklisted apps" else "Whitelisted apps")) selectedUnseen.forEach { add(AppListItem.App(it)) } } } else { add(AppListItem.Separator) + add(AppListItem.SectionTitle("All apps")) // The full alphabetical list intentionally repeats seen apps after its quick-access group. shown.forEach { add(AppListItem.App(it)) } } diff --git a/app/src/test/java/se/ajpanton/notificationlog/settings/AppListOrderingTest.kt b/app/src/test/java/se/ajpanton/notificationlog/settings/AppListOrderingTest.kt index 0fb845f..01d7307 100644 --- a/app/src/test/java/se/ajpanton/notificationlog/settings/AppListOrderingTest.kt +++ b/app/src/test/java/se/ajpanton/notificationlog/settings/AppListOrderingTest.kt @@ -6,13 +6,61 @@ import org.junit.Test class AppListOrderingTest { private fun app(label: String, seen: Boolean = false, selected: Boolean = false) = ListedApp(label, "pkg.$label", seen, selected) - @Test fun `seen apps are repeated before the full list when requested`() { - val items = AppListOrdering.items(listOf(app("Zulu"), app("Beta", seen = true), app("Alpha", seen = true)), false, true) - assertEquals(listOf("Alpha", "Beta", "|", "Alpha", "Beta", "Zulu"), items.map { if (it is AppListItem.App) it.value.label else "|" }) + @Test fun `seen apps are titled and repeated before the full list when requested`() { + val items = AppListOrdering.items( + listOf(app("Zulu"), app("Beta", seen = true), app("Alpha", seen = true)), + onlySeen = false, + seenAppsFirst = true, + mode = AppRuleMode.BLACKLIST, + ) + assertEquals(listOf("[Seen apps]", "Alpha", "Beta", "|", "[All apps]", "Alpha", "Beta", "Zulu"), items.labels()) } - @Test fun `selected unseen app remains after seen apps in only-seen mode`() { - val items = AppListOrdering.items(listOf(app("Seen", seen = true), app("Chosen", selected = true), app("Hidden")), true, true) - assertEquals(listOf("Seen", "|", "Chosen"), items.map { if (it is AppListItem.App) it.value.label else "|" }) + @Test fun `selected unseen apps are titled after seen apps in only-seen mode`() { + val items = AppListOrdering.items( + listOf(app("Seen", seen = true), app("Chosen", selected = true), app("Hidden")), + onlySeen = true, + seenAppsFirst = true, + mode = AppRuleMode.WHITELIST, + ) + assertEquals(listOf("[Seen apps]", "Seen", "|", "[Whitelisted apps]", "Chosen"), items.labels()) + } + + @Test fun `only seen and checked without grouping is one alphabetical list`() { + val items = AppListOrdering.items( + listOf(app("Zulu", seen = true), app("Beta", selected = true), app("Hidden")), + onlySeen = true, + seenAppsFirst = false, + mode = AppRuleMode.BLACKLIST, + ) + assertEquals(listOf("Beta", "Zulu"), items.labels()) + } + + @Test fun `only seen apps omit section titles without unseen checked apps`() { + val items = AppListOrdering.items( + listOf(app("Zulu", seen = true), app("Alpha", seen = true), app("Hidden")), + onlySeen = true, + seenAppsFirst = true, + mode = AppRuleMode.BLACKLIST, + ) + assertEquals(listOf("Alpha", "Zulu"), items.labels()) + } + + @Test fun `all apps without grouping is one alphabetical list`() { + val items = AppListOrdering.items( + listOf(app("Zulu", seen = true), app("Alpha"), app("Beta", selected = true)), + onlySeen = false, + seenAppsFirst = false, + mode = AppRuleMode.BLACKLIST, + ) + assertEquals(listOf("Alpha", "Beta", "Zulu"), items.labels()) + } + + private fun List.labels() = map { + when (it) { + is AppListItem.App -> it.value.label + is AppListItem.SectionTitle -> "[${it.value}]" + AppListItem.Separator -> "|" + } } }