Clarify seen and checked app filtering

This commit is contained in:
ajp_anton
2026-07-27 13:27:08 +00:00
parent cad73e33a0
commit 55057767c1
3 changed files with 87 additions and 11 deletions
@@ -70,7 +70,6 @@ class FilterAppsFragment : Fragment(R.layout.fragment_event_settings) {
binding!!.appRuleToggle.isChecked = rule.mode == AppRuleMode.WHITELIST binding!!.appRuleToggle.isChecked = rule.mode == AppRuleMode.WHITELIST
binding!!.onlySeenToggle.isChecked = rule.onlySeenApps binding!!.onlySeenToggle.isChecked = rule.onlySeenApps
binding!!.seenFirstToggle.isChecked = rule.seenAppsFirst binding!!.seenFirstToggle.isChecked = rule.seenAppsFirst
binding!!.seenFirstToggle.isEnabled = !rule.onlySeenApps
updateRuleLabels(rule) updateRuleLabels(rule)
binding!!.appRuleToggle.setOnCheckedChangeListener { _, checked -> binding!!.appRuleToggle.setOnCheckedChangeListener { _, checked ->
save { copy(mode = if (checked) AppRuleMode.WHITELIST else AppRuleMode.BLACKLIST) } 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) { private fun updateRuleLabels(rule: AppFilterSettings) {
binding!!.appRuleToggle.text = modeLabel(rule.mode) 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" 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, 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 { activity?.runOnUiThread {
if (binding === currentBinding && generation == appLoadGeneration) { if (binding === currentBinding && generation == appLoadGeneration) {
appAdapter.submit(items) appAdapter.submit(items)
@@ -151,7 +150,11 @@ class FilterAppsFragment : Fragment(R.layout.fragment_event_settings) {
notifyDataSetChanged() 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) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = if (viewType == APP) {
val row = LinearLayout(parent.context).apply { val row = LinearLayout(parent.context).apply {
@@ -170,6 +173,15 @@ class FilterAppsFragment : Fragment(R.layout.fragment_event_settings) {
row.addView(checkbox) row.addView(checkbox)
row.addView(textColumn) row.addView(textColumn)
AppHolder(row, checkbox, label, packageName) 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 { } else {
val divider = View(parent.context).apply { val divider = View(parent.context).apply {
layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, dp(parent, 1)).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 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 SeparatorHolder(view: View) : RecyclerView.ViewHolder(view)
private class SectionTitleHolder(val text: TextView) : RecyclerView.ViewHolder(text)
private companion object { private companion object {
const val APP = 0 const val APP = 0
const val SEPARATOR = 1 const val SEPARATOR = 1
const val SECTION_TITLE = 2
fun dp(parent: ViewGroup, value: Int): Int = (value * parent.resources.displayMetrics.density).toInt() fun dp(parent: ViewGroup, value: Int): Int = (value * parent.resources.displayMetrics.density).toInt()
} }
} }
@@ -3,25 +3,35 @@ package se.ajpanton.notificationlog.settings
data class ListedApp(val label: String, val packageName: String, val seen: Boolean, val selected: Boolean) data class ListedApp(val label: String, val packageName: String, val seen: Boolean, val selected: Boolean)
sealed interface AppListItem { sealed interface AppListItem {
data class App(val value: ListedApp) : AppListItem data class App(val value: ListedApp) : AppListItem
data class SectionTitle(val value: String) : AppListItem
data object Separator : AppListItem data object Separator : AppListItem
} }
object AppListOrdering { object AppListOrdering {
fun items(allApps: List<ListedApp>, onlySeen: Boolean, seenAppsFirst: Boolean): List<AppListItem> { fun items(
allApps: List<ListedApp>,
onlySeen: Boolean,
seenAppsFirst: Boolean,
mode: AppRuleMode,
): List<AppListItem> {
val apps = allApps.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.label }) val apps = allApps.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.label })
val shown = if (onlySeen) apps.filter { it.seen || it.selected } else apps val shown = if (onlySeen) apps.filter { it.seen || it.selected } else apps
val seen = shown.filter { it.seen } val seen = shown.filter { it.seen }
val selectedUnseen = shown.filter { !it.seen } val selectedUnseen = shown.filter { !it.seen }
if (!seenAppsFirst || seen.isEmpty()) return shown.map(AppListItem::App) if (!seenAppsFirst || seen.isEmpty()) return shown.map(AppListItem::App)
return buildList { return buildList {
if (!onlySeen) add(AppListItem.SectionTitle("Seen apps"))
if (onlySeen && selectedUnseen.isNotEmpty()) add(AppListItem.SectionTitle("Seen apps"))
seen.forEach { add(AppListItem.App(it)) } seen.forEach { add(AppListItem.App(it)) }
if (onlySeen) { if (onlySeen) {
if (selectedUnseen.isNotEmpty()) { if (selectedUnseen.isNotEmpty()) {
add(AppListItem.Separator) add(AppListItem.Separator)
add(AppListItem.SectionTitle(if (mode == AppRuleMode.BLACKLIST) "Blacklisted apps" else "Whitelisted apps"))
selectedUnseen.forEach { add(AppListItem.App(it)) } selectedUnseen.forEach { add(AppListItem.App(it)) }
} }
} else { } else {
add(AppListItem.Separator) add(AppListItem.Separator)
add(AppListItem.SectionTitle("All apps"))
// The full alphabetical list intentionally repeats seen apps after its quick-access group. // The full alphabetical list intentionally repeats seen apps after its quick-access group.
shown.forEach { add(AppListItem.App(it)) } shown.forEach { add(AppListItem.App(it)) }
} }
@@ -6,13 +6,61 @@ import org.junit.Test
class AppListOrderingTest { class AppListOrderingTest {
private fun app(label: String, seen: Boolean = false, selected: Boolean = false) = ListedApp(label, "pkg.$label", seen, selected) 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`() { @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)), false, true) val items = AppListOrdering.items(
assertEquals(listOf("Alpha", "Beta", "|", "Alpha", "Beta", "Zulu"), items.map { if (it is AppListItem.App) it.value.label else "|" }) 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`() { @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")), true, true) val items = AppListOrdering.items(
assertEquals(listOf("Seen", "|", "Chosen"), items.map { if (it is AppListItem.App) it.value.label else "|" }) 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<AppListItem>.labels() = map {
when (it) {
is AppListItem.App -> it.value.label
is AppListItem.SectionTitle -> "[${it.value}]"
AppListItem.Separator -> "|"
}
} }
} }