Clarify seen and checked app filtering
This commit is contained in:
@@ -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 -> "|"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user