Add per-app event logging overrides

This commit is contained in:
ajp_anton
2026-07-27 13:45:32 +00:00
parent 55057767c1
commit 1e4315d3dd
10 changed files with 229 additions and 24 deletions
@@ -1,6 +1,7 @@
package se.ajpanton.notificationlog package se.ajpanton.notificationlog
import android.os.Bundle import android.os.Bundle
import android.content.res.ColorStateList
import android.text.SpannableString import android.text.SpannableString
import android.text.Spanned import android.text.Spanned
import android.text.style.StyleSpan import android.text.style.StyleSpan
@@ -9,12 +10,16 @@ import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.CheckBox import android.widget.CheckBox
import android.widget.ImageButton
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.switchmaterial.SwitchMaterial
import se.ajpanton.notificationlog.capture.SeenApps import se.ajpanton.notificationlog.capture.SeenApps
import se.ajpanton.notificationlog.databinding.FragmentEventSettingsBinding import se.ajpanton.notificationlog.databinding.FragmentEventSettingsBinding
import se.ajpanton.notificationlog.settings.AppListItem import se.ajpanton.notificationlog.settings.AppListItem
@@ -23,10 +28,14 @@ import se.ajpanton.notificationlog.settings.AppRuleMode
import se.ajpanton.notificationlog.settings.ListedApp import se.ajpanton.notificationlog.settings.ListedApp
import se.ajpanton.notificationlog.settings.AppFilterSettings import se.ajpanton.notificationlog.settings.AppFilterSettings
import se.ajpanton.notificationlog.settings.AppFilterSettingsStore import se.ajpanton.notificationlog.settings.AppFilterSettingsStore
import se.ajpanton.notificationlog.settings.LoggingRuleStore
import se.ajpanton.notificationlog.settings.LoggingType
import se.ajpanton.notificationlog.settings.PerAppEventSettingsStore
class FilterAppsFragment : Fragment(R.layout.fragment_event_settings) { class FilterAppsFragment : Fragment(R.layout.fragment_event_settings) {
private var binding: FragmentEventSettingsBinding? = null private var binding: FragmentEventSettingsBinding? = null
private lateinit var store: AppFilterSettingsStore private lateinit var store: AppFilterSettingsStore
private lateinit var perAppEventSettings: PerAppEventSettingsStore
private lateinit var appAdapter: AppListAdapter private lateinit var appAdapter: AppListAdapter
private var appLoadGeneration = 0 private var appLoadGeneration = 0
@@ -34,7 +43,8 @@ class FilterAppsFragment : Fragment(R.layout.fragment_event_settings) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
binding = FragmentEventSettingsBinding.bind(view) binding = FragmentEventSettingsBinding.bind(view)
store = AppFilterSettingsStore(requireContext()) store = AppFilterSettingsStore(requireContext())
appAdapter = AppListAdapter(::setPackageSelected) perAppEventSettings = PerAppEventSettingsStore(requireContext())
appAdapter = AppListAdapter(::setPackageSelected, ::showPerAppEventSettings)
binding!!.appList.layoutManager = LinearLayoutManager(requireContext()) binding!!.appList.layoutManager = LinearLayoutManager(requireContext())
binding!!.appList.adapter = appAdapter binding!!.appList.adapter = appAdapter
binding!!.appListRefresh.setOnRefreshListener(::reloadAppList) binding!!.appListRefresh.setOnRefreshListener(::reloadAppList)
@@ -118,9 +128,10 @@ class FilterAppsFragment : Fragment(R.layout.fragment_event_settings) {
packageName = info.packageName, packageName = info.packageName,
seen = info.packageName in seen, seen = info.packageName in seen,
selected = info.packageName in rule.selectedPackages, selected = info.packageName in rule.selectedPackages,
hasEventOverride = perAppEventSettings.hasOverride(info.packageName),
) )
} }
val items = AppListOrdering.items(apps, rule.onlySeenApps, rule.seenAppsFirst, rule.mode) val items = AppListOrdering.items(apps, rule.onlySeenApps, rule.seenAppsFirst)
activity?.runOnUiThread { activity?.runOnUiThread {
if (binding === currentBinding && generation == appLoadGeneration) { if (binding === currentBinding && generation == appLoadGeneration) {
appAdapter.submit(items) appAdapter.submit(items)
@@ -140,8 +151,69 @@ class FilterAppsFragment : Fragment(R.layout.fragment_event_settings) {
// Do not reload: an unchecked unseen row remains visible until the next requested refresh. // Do not reload: an unchecked unseen row remains visible until the next requested refresh.
} }
private fun showPerAppEventSettings(app: ListedApp) {
val eventTypes = listOf(LoggingType.APPEARING, LoggingType.DISAPPEARING, LoggingType.EDITS)
val rules = LoggingRuleStore(requireContext())
val content = LinearLayout(requireContext()).apply {
orientation = LinearLayout.VERTICAL
setPadding(dp(24), 0, dp(24), 0)
}
val useGlobal = SwitchMaterial(requireContext()).apply { text = "Use global" }
val eventToggles = eventTypes.associateWith { type ->
SwitchMaterial(requireContext()).apply {
text = type.label()
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT,
).apply { marginStart = dp(24) }
}.also(content::addView)
}
content.addView(useGlobal, 0)
fun globalEvents() = eventTypes.associateWith { rules.ruleFor(it).enabled }
fun refresh() {
val useGlobalSetting = perAppEventSettings.usesGlobal(app.packageName)
useGlobal.setOnCheckedChangeListener(null)
useGlobal.isChecked = useGlobalSetting
eventToggles.forEach { (type, toggle) ->
toggle.setOnCheckedChangeListener(null)
toggle.isEnabled = !useGlobalSetting
toggle.isChecked = perAppEventSettings.isEnabled(
app.packageName,
type,
globalEvents().getValue(type),
)
toggle.setOnCheckedChangeListener { _, enabled ->
perAppEventSettings.setEnabled(app.packageName, type, enabled)
}
}
useGlobal.setOnCheckedChangeListener { _, enabled ->
if (enabled) perAppEventSettings.useGlobal(app.packageName)
else perAppEventSettings.startUsingOverride(app.packageName, globalEvents())
appAdapter.updateEventOverride(app.packageName, !enabled)
refresh()
}
}
refresh()
MaterialAlertDialogBuilder(requireContext())
.setTitle("When logging, only log")
.setView(content)
.setPositiveButton("Close", null)
.show()
}
private fun LoggingType.label() = when (this) {
LoggingType.APPEARING -> "Appearing"
LoggingType.DISAPPEARING -> "Disappearing"
LoggingType.EDITS -> "Edits"
else -> error("Only event types are shown in the per-app dialog")
}
private fun dp(value: Int): Int = (value * resources.displayMetrics.density).toInt()
private class AppListAdapter( private class AppListAdapter(
private val onSelectedChanged: (String, Boolean) -> Unit, private val onSelectedChanged: (String, Boolean) -> Unit,
private val onEventFilterClicked: (ListedApp) -> Unit,
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { ) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private var items: List<AppListItem> = emptyList() private var items: List<AppListItem> = emptyList()
@@ -162,6 +234,12 @@ class FilterAppsFragment : Fragment(R.layout.fragment_event_settings) {
setPadding(0, dp(parent, 4), 0, dp(parent, 4)) setPadding(0, dp(parent, 4), 0, dp(parent, 4))
} }
val checkbox = CheckBox(parent.context) val checkbox = CheckBox(parent.context)
val filter = ImageButton(parent.context).apply {
setImageResource(R.drawable.ic_filter_list)
contentDescription = "Edit logging events"
layoutParams = LinearLayout.LayoutParams(dp(parent, 48), dp(parent, 48))
setPadding(dp(parent, 12), dp(parent, 12), dp(parent, 12), dp(parent, 12))
}
val textColumn = LinearLayout(parent.context).apply { val textColumn = LinearLayout(parent.context).apply {
orientation = LinearLayout.VERTICAL orientation = LinearLayout.VERTICAL
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f) layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
@@ -171,8 +249,9 @@ class FilterAppsFragment : Fragment(R.layout.fragment_event_settings) {
textColumn.addView(label) textColumn.addView(label)
textColumn.addView(packageName) textColumn.addView(packageName)
row.addView(checkbox) row.addView(checkbox)
row.addView(filter)
row.addView(textColumn) row.addView(textColumn)
AppHolder(row, checkbox, label, packageName) AppHolder(row, checkbox, filter, label, packageName)
} else if (viewType == SECTION_TITLE) { } else if (viewType == SECTION_TITLE) {
SectionTitleHolder(TextView(parent.context).apply { SectionTitleHolder(TextView(parent.context).apply {
layoutParams = RecyclerView.LayoutParams( layoutParams = RecyclerView.LayoutParams(
@@ -208,6 +287,15 @@ class FilterAppsFragment : Fragment(R.layout.fragment_event_settings) {
} else current } else current
} }
} }
holder.filter.contentDescription = "Edit logging events for ${app.label}"
holder.filter.imageTintList = ColorStateList.valueOf(
MaterialColors.getColor(
holder.filter,
if (app.hasEventOverride) com.google.android.material.R.attr.colorPrimary
else com.google.android.material.R.attr.colorOnSurface,
),
)
holder.filter.setOnClickListener { onEventFilterClicked(app) }
} }
if (holder is SectionTitleHolder && item is AppListItem.SectionTitle) { if (holder is SectionTitleHolder && item is AppListItem.SectionTitle) {
holder.text.text = item.value holder.text.text = item.value
@@ -216,9 +304,21 @@ class FilterAppsFragment : Fragment(R.layout.fragment_event_settings) {
override fun getItemCount(): Int = items.size override fun getItemCount(): Int = items.size
fun updateEventOverride(packageName: String, hasEventOverride: Boolean) {
items = items.map { item ->
if (item is AppListItem.App && item.value.packageName == packageName) {
AppListItem.App(item.value.copy(hasEventOverride = hasEventOverride))
} else {
item
}
}
notifyDataSetChanged()
}
private class AppHolder( private class AppHolder(
view: View, view: View,
val checkbox: CheckBox, val checkbox: CheckBox,
val filter: ImageButton,
val label: TextView, val label: TextView,
val packageName: TextView, val packageName: TextView,
) : RecyclerView.ViewHolder(view) ) : RecyclerView.ViewHolder(view)
@@ -14,6 +14,7 @@ import se.ajpanton.notificationlog.settings.LoggingType
import se.ajpanton.notificationlog.settings.NotificationRuleEvaluator import se.ajpanton.notificationlog.settings.NotificationRuleEvaluator
import se.ajpanton.notificationlog.settings.CaptureSettingsStore import se.ajpanton.notificationlog.settings.CaptureSettingsStore
import se.ajpanton.notificationlog.settings.AppFilterSettingsStore import se.ajpanton.notificationlog.settings.AppFilterSettingsStore
import se.ajpanton.notificationlog.settings.PerAppEventSettingsStore
import java.util.concurrent.ExecutorService import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors import java.util.concurrent.Executors
@@ -24,6 +25,7 @@ class NotificationCaptureService : NotificationListenerService() {
private lateinit var ruleStore: LoggingRuleStore private lateinit var ruleStore: LoggingRuleStore
private lateinit var captureSettings: CaptureSettingsStore private lateinit var captureSettings: CaptureSettingsStore
private lateinit var appFilterStore: AppFilterSettingsStore private lateinit var appFilterStore: AppFilterSettingsStore
private lateinit var perAppEventSettings: PerAppEventSettingsStore
private lateinit var imageStore: EncryptedImageStore private lateinit var imageStore: EncryptedImageStore
override fun onCreate() { override fun onCreate() {
@@ -32,6 +34,7 @@ class NotificationCaptureService : NotificationListenerService() {
ruleStore = LoggingRuleStore(this) ruleStore = LoggingRuleStore(this)
captureSettings = CaptureSettingsStore(this) captureSettings = CaptureSettingsStore(this)
appFilterStore = AppFilterSettingsStore(this) appFilterStore = AppFilterSettingsStore(this)
perAppEventSettings = PerAppEventSettingsStore(this)
imageStore = EncryptedImageStore(this) imageStore = EncryptedImageStore(this)
writeExecutor = Executors.newSingleThreadExecutor { runnable -> writeExecutor = Executors.newSingleThreadExecutor { runnable ->
Thread(runnable, "notification-log-writer") Thread(runnable, "notification-log-writer")
@@ -126,8 +129,15 @@ class NotificationCaptureService : NotificationListenerService() {
.ifEmpty { null } .ifEmpty { null }
} }
private fun allows(type: LoggingType, packageName: String): Boolean = private fun allows(type: LoggingType, packageName: String): Boolean {
ruleStore.ruleFor(type).enabled && NotificationRuleEvaluator.allows(appFilterStore.load(), packageName) val globalEnabled = ruleStore.ruleFor(type).enabled
val eventEnabled = if (type in EVENT_TYPES) {
perAppEventSettings.isEnabled(packageName, type, globalEnabled)
} else {
globalEnabled
}
return eventEnabled && NotificationRuleEvaluator.allows(appFilterStore.load(), packageName)
}
private fun appName(packageName: String): String = try { private fun appName(packageName: String): String = try {
val applicationInfo = packageManager.getApplicationInfo(packageName, 0) val applicationInfo = packageManager.getApplicationInfo(packageName, 0)
@@ -167,5 +177,6 @@ class NotificationCaptureService : NotificationListenerService() {
private companion object { private companion object {
const val TAG = "NotificationCapture" const val TAG = "NotificationCapture"
const val MAX_CONTENT_CHARACTERS = 16_000 const val MAX_CONTENT_CHARACTERS = 16_000
val EVENT_TYPES = setOf(LoggingType.APPEARING, LoggingType.DISAPPEARING, LoggingType.EDITS)
} }
} }
@@ -1,6 +1,14 @@
package se.ajpanton.notificationlog.settings 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,
val hasEventOverride: Boolean = false,
) {
val edited: Boolean get() = selected || hasEventOverride
}
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 class SectionTitle(val value: String) : AppListItem
@@ -12,22 +20,21 @@ object AppListOrdering {
allApps: List<ListedApp>, allApps: List<ListedApp>,
onlySeen: Boolean, onlySeen: Boolean,
seenAppsFirst: Boolean, seenAppsFirst: Boolean,
mode: AppRuleMode,
): List<AppListItem> { ): 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.edited } else apps
val seen = shown.filter { it.seen } val seen = shown.filter { it.seen }
val selectedUnseen = shown.filter { !it.seen } val editedUnseen = 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) add(AppListItem.SectionTitle("Seen apps"))
if (onlySeen && selectedUnseen.isNotEmpty()) add(AppListItem.SectionTitle("Seen apps")) if (onlySeen && editedUnseen.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 (editedUnseen.isNotEmpty()) {
add(AppListItem.Separator) add(AppListItem.Separator)
add(AppListItem.SectionTitle(if (mode == AppRuleMode.BLACKLIST) "Blacklisted apps" else "Whitelisted apps")) add(AppListItem.SectionTitle("Edited apps"))
selectedUnseen.forEach { add(AppListItem.App(it)) } editedUnseen.forEach { add(AppListItem.App(it)) }
} }
} else { } else {
add(AppListItem.Separator) add(AppListItem.Separator)
@@ -28,7 +28,11 @@ class LoggingRuleStore(context: Context) {
private fun key(type: LoggingType, suffix: String) = "${type.name.lowercase()}.$suffix" private fun key(type: LoggingType, suffix: String) = "${type.name.lowercase()}.$suffix"
private fun updateListenerComponent() { private fun updateListenerComponent() {
NotificationListenerComponentController.update(appContext, LoggingType.entries.map(::ruleFor)) NotificationListenerComponentController.update(
appContext,
LoggingType.entries.map(::ruleFor),
PerAppEventSettingsStore(appContext).hasEnabledEventOverride(),
)
} }
private companion object { private companion object {
@@ -13,11 +13,11 @@ import se.ajpanton.notificationlog.capture.NotificationCaptureService
* is therefore both the no-work battery mode and the no-start-on-boot mode. * is therefore both the no-work battery mode and the no-start-on-boot mode.
*/ */
internal object NotificationListenerComponentController { internal object NotificationListenerComponentController {
fun update(context: Context, rules: Collection<LoggingRule>) { fun update(context: Context, rules: Collection<LoggingRule>, hasEnabledEventOverride: Boolean = false) {
val applicationContext = context.applicationContext val applicationContext = context.applicationContext
val component = ComponentName(applicationContext, NotificationCaptureService::class.java) val component = ComponentName(applicationContext, NotificationCaptureService::class.java)
val packageManager = applicationContext.packageManager val packageManager = applicationContext.packageManager
val desired = if (NotificationListenerPolicy.shouldRun(rules)) { val desired = if (NotificationListenerPolicy.shouldRun(rules, hasEnabledEventOverride)) {
PackageManager.COMPONENT_ENABLED_STATE_ENABLED PackageManager.COMPONENT_ENABLED_STATE_ENABLED
} else { } else {
PackageManager.COMPONENT_ENABLED_STATE_DISABLED PackageManager.COMPONENT_ENABLED_STATE_DISABLED
@@ -2,5 +2,6 @@ package se.ajpanton.notificationlog.settings
/** The listener is useful only when at least one event type is enabled. */ /** The listener is useful only when at least one event type is enabled. */
object NotificationListenerPolicy { object NotificationListenerPolicy {
fun shouldRun(rules: Collection<LoggingRule>): Boolean = rules.any { it.enabled } fun shouldRun(rules: Collection<LoggingRule>, hasEnabledEventOverride: Boolean = false): Boolean =
rules.any { it.enabled } || hasEnabledEventOverride
} }
@@ -0,0 +1,55 @@
package se.ajpanton.notificationlog.settings
import android.content.Context
import androidx.core.content.edit
/** Event logging choices that replace the global event switches for one app. */
class PerAppEventSettingsStore(context: Context) {
private val appContext = context.applicationContext
private val preferences = appContext.getSharedPreferences(FILE_NAME, Context.MODE_PRIVATE)
fun usesGlobal(packageName: String): Boolean = !preferences.contains(key(packageName))
fun isEnabled(packageName: String, type: LoggingType, globalEnabled: Boolean): Boolean =
preferences.getStringSet(key(packageName), null)?.let { type.name in it } ?: globalEnabled
fun useGlobal(packageName: String) {
preferences.edit { remove(key(packageName)) }
updateListenerComponent()
}
fun startUsingOverride(packageName: String, globalEvents: Map<LoggingType, Boolean>) {
saveEvents(packageName, globalEvents.filterValues { it }.keys)
}
fun setEnabled(packageName: String, type: LoggingType, enabled: Boolean) {
val events = preferences.getStringSet(key(packageName), emptySet()).orEmpty().toMutableSet()
if (enabled) events.add(type.name) else events.remove(type.name)
preferences.edit { putStringSet(key(packageName), events) }
updateListenerComponent()
}
fun hasOverride(packageName: String): Boolean = !usesGlobal(packageName)
fun hasEnabledEventOverride(): Boolean = preferences.all
.filterKeys { it.startsWith(KEY_PREFIX) }
.values
.any { value -> (value as? Set<*>)?.isNotEmpty() == true }
private fun saveEvents(packageName: String, enabledEvents: Set<LoggingType>) {
preferences.edit { putStringSet(key(packageName), enabledEvents.mapTo(mutableSetOf()) { it.name }) }
updateListenerComponent()
}
private fun updateListenerComponent() {
val rules = LoggingType.entries.map { LoggingRuleStore(appContext).ruleFor(it) }
NotificationListenerComponentController.update(appContext, rules, hasEnabledEventOverride())
}
private fun key(packageName: String) = "$KEY_PREFIX$packageName"
private companion object {
const val FILE_NAME = "per-app-event-settings"
const val KEY_PREFIX = "events:"
}
}
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M3,5h18v2H3zM6,11h12v2H6zM10,17h4v2h-4z" />
</vector>
@@ -4,14 +4,18 @@ import org.junit.Assert.assertEquals
import org.junit.Test 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,
hasEventOverride: Boolean = false,
) = ListedApp(label, "pkg.$label", seen, selected, hasEventOverride)
@Test fun `seen apps are titled and 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( val items = AppListOrdering.items(
listOf(app("Zulu"), app("Beta", seen = true), app("Alpha", seen = true)), listOf(app("Zulu"), app("Beta", seen = true), app("Alpha", seen = true)),
onlySeen = false, onlySeen = false,
seenAppsFirst = true, seenAppsFirst = true,
mode = AppRuleMode.BLACKLIST,
) )
assertEquals(listOf("[Seen apps]", "Alpha", "Beta", "|", "[All apps]", "Alpha", "Beta", "Zulu"), items.labels()) assertEquals(listOf("[Seen apps]", "Alpha", "Beta", "|", "[All apps]", "Alpha", "Beta", "Zulu"), items.labels())
} }
@@ -21,9 +25,8 @@ class AppListOrderingTest {
listOf(app("Seen", seen = true), app("Chosen", selected = true), app("Hidden")), listOf(app("Seen", seen = true), app("Chosen", selected = true), app("Hidden")),
onlySeen = true, onlySeen = true,
seenAppsFirst = true, seenAppsFirst = true,
mode = AppRuleMode.WHITELIST,
) )
assertEquals(listOf("[Seen apps]", "Seen", "|", "[Whitelisted apps]", "Chosen"), items.labels()) assertEquals(listOf("[Seen apps]", "Seen", "|", "[Edited apps]", "Chosen"), items.labels())
} }
@Test fun `only seen and checked without grouping is one alphabetical list`() { @Test fun `only seen and checked without grouping is one alphabetical list`() {
@@ -31,17 +34,24 @@ class AppListOrderingTest {
listOf(app("Zulu", seen = true), app("Beta", selected = true), app("Hidden")), listOf(app("Zulu", seen = true), app("Beta", selected = true), app("Hidden")),
onlySeen = true, onlySeen = true,
seenAppsFirst = false, seenAppsFirst = false,
mode = AppRuleMode.BLACKLIST,
) )
assertEquals(listOf("Beta", "Zulu"), items.labels()) assertEquals(listOf("Beta", "Zulu"), items.labels())
} }
@Test fun `unseen app with an event override is included as edited`() {
val items = AppListOrdering.items(
listOf(app("Seen", seen = true), app("Configured", hasEventOverride = true), app("Hidden")),
onlySeen = true,
seenAppsFirst = true,
)
assertEquals(listOf("[Seen apps]", "Seen", "|", "[Edited apps]", "Configured"), items.labels())
}
@Test fun `only seen apps omit section titles without unseen checked apps`() { @Test fun `only seen apps omit section titles without unseen checked apps`() {
val items = AppListOrdering.items( val items = AppListOrdering.items(
listOf(app("Zulu", seen = true), app("Alpha", seen = true), app("Hidden")), listOf(app("Zulu", seen = true), app("Alpha", seen = true), app("Hidden")),
onlySeen = true, onlySeen = true,
seenAppsFirst = true, seenAppsFirst = true,
mode = AppRuleMode.BLACKLIST,
) )
assertEquals(listOf("Alpha", "Zulu"), items.labels()) assertEquals(listOf("Alpha", "Zulu"), items.labels())
} }
@@ -51,7 +61,6 @@ class AppListOrderingTest {
listOf(app("Zulu", seen = true), app("Alpha"), app("Beta", selected = true)), listOf(app("Zulu", seen = true), app("Alpha"), app("Beta", selected = true)),
onlySeen = false, onlySeen = false,
seenAppsFirst = false, seenAppsFirst = false,
mode = AppRuleMode.BLACKLIST,
) )
assertEquals(listOf("Alpha", "Beta", "Zulu"), items.labels()) assertEquals(listOf("Alpha", "Beta", "Zulu"), items.labels())
} }
@@ -12,4 +12,13 @@ class NotificationListenerPolicyTest {
@Test fun `listener stays off when every logging type is disabled`() { @Test fun `listener stays off when every logging type is disabled`() {
assertFalse(NotificationListenerPolicy.shouldRun(LoggingType.entries.map { LoggingRule(enabled = false) })) assertFalse(NotificationListenerPolicy.shouldRun(LoggingType.entries.map { LoggingRule(enabled = false) }))
} }
@Test fun `listener runs for an enabled per-app event override`() {
assertTrue(
NotificationListenerPolicy.shouldRun(
LoggingType.entries.map { LoggingRule(enabled = false) },
hasEnabledEventOverride = true,
),
)
}
} }