Add consolidated alert rule editing

This commit is contained in:
ajp_anton
2026-08-16 17:18:43 +00:00
parent c209ef24bb
commit 594931e51d
2 changed files with 158 additions and 0 deletions
@@ -0,0 +1,93 @@
package se.ajpanton.notificationsmaster.alerts
import java.util.UUID
/** All editable rule fields apart from app assignment, local order, and identity. */
data class AlertRuleDefinition(
val sources: Set<AlertSource>,
val matcher: AlertTextMatcher = AlertTextMatcher(),
val outcome: AlertOutcome,
val profileId: String? = null,
val playToCompletion: Boolean = false,
val allowDuringDnd: Boolean = false,
val enabled: Boolean = true,
val name: String? = null,
) {
init {
require(sources.isNotEmpty())
require((outcome == AlertOutcome.PLAY_PROFILE) == (profileId != null))
require(name?.isNotBlank() != false)
}
}
data class ConsolidatedAlertRule(
val definition: AlertRuleDefinition,
val ruleIds: Set<String>,
val packageNames: Set<String>,
)
object AlertRuleEditor {
fun consolidate(rules: Iterable<AlertRule>): List<ConsolidatedAlertRule> =
rules.groupBy(AlertRule::groupingKey).values.map { group ->
ConsolidatedAlertRule(
definition = group.first().definition(),
ruleIds = group.mapTo(linkedSetOf()) { it.id },
packageNames = group.mapTo(sortedSetOf()) { it.packageName },
)
}.sortedWith(compareBy<ConsolidatedAlertRule> { it.definition.name ?: "" }.thenBy { it.packageNames.first() })
fun updateConsolidated(
configuration: AlertConfiguration,
ruleIds: Set<String>,
definition: AlertRuleDefinition,
): AlertConfiguration = configuration.copy(
rules = configuration.rules.map { rule ->
if (rule.id in ruleIds) rule.withDefinition(definition) else rule
},
)
fun updateForApp(
configuration: AlertConfiguration,
ruleId: String,
definition: AlertRuleDefinition,
): AlertConfiguration = updateConsolidated(configuration, setOf(ruleId), definition)
fun addForApps(
configuration: AlertConfiguration,
packageNames: Iterable<String>,
definition: AlertRuleDefinition,
newId: () -> String = { UUID.randomUUID().toString() },
): AlertConfiguration {
val added = packageNames.distinct().map { packageName ->
AlertRule(
id = newId(),
packageName = packageName,
order = configuration.rules.filter { it.packageName == packageName }.maxOfOrNull { it.order }?.plus(1) ?: 0,
sources = definition.sources,
matcher = definition.matcher,
outcome = definition.outcome,
profileId = definition.profileId,
playToCompletion = definition.playToCompletion,
allowDuringDnd = definition.allowDuringDnd,
enabled = definition.enabled,
name = definition.name,
)
}
return configuration.copy(rules = configuration.rules + added)
}
private fun AlertRule.definition() = AlertRuleDefinition(
sources, matcher, outcome, profileId, playToCompletion, allowDuringDnd, enabled, name,
)
private fun AlertRule.withDefinition(definition: AlertRuleDefinition) = copy(
sources = definition.sources,
matcher = definition.matcher,
outcome = definition.outcome,
profileId = definition.profileId,
playToCompletion = definition.playToCompletion,
allowDuringDnd = definition.allowDuringDnd,
enabled = definition.enabled,
name = definition.name,
)
}
@@ -0,0 +1,65 @@
package se.ajpanton.notificationsmaster.alerts
import org.junit.Assert.assertEquals
import org.junit.Test
class AlertRuleEditorTest {
private val profile = AlertProfile("profile", "Profile", "content://sound")
private val definition = AlertRuleDefinition(setOf(AlertSource.NOTIFICATION_POST), outcome = AlertOutcome.PLAY_PROFILE, profileId = profile.id)
@Test
fun consolidatesIdenticalRulesAcrossApps() {
val consolidated = AlertRuleEditor.consolidate(listOf(rule("one", "chat.app", 1), rule("two", "mail.app", 4)))
assertEquals(1, consolidated.size)
assertEquals(setOf("one", "two"), consolidated.single().ruleIds)
assertEquals(setOf("chat.app", "mail.app"), consolidated.single().packageNames)
}
@Test
fun globalEditPreservesEveryAppsIdentityAndOrder() {
val original = configuration(rule("one", "chat.app", 1), rule("two", "mail.app", 4))
val edited = AlertRuleEditor.updateConsolidated(
original,
setOf("one", "two"),
definition.copy(name = "All messages", playToCompletion = true),
)
assertEquals(listOf("one", "two"), edited.rules.map { it.id })
assertEquals(listOf("chat.app", "mail.app"), edited.rules.map { it.packageName })
assertEquals(listOf(1, 4), edited.rules.map { it.order })
assertEquals(listOf("All messages", "All messages"), edited.rules.map { it.name })
}
@Test
fun appEditSplitsOnlyThatAppsRule() {
val original = configuration(rule("one", "chat.app", 1), rule("two", "mail.app", 4))
val edited = AlertRuleEditor.updateForApp(
original,
"one",
definition.copy(matcher = AlertTextMatcher(mode = AlertTextMode.CONTAINS, value = "urgent")),
)
assertEquals(2, AlertRuleEditor.consolidate(edited.rules).size)
assertEquals(AlertTextMode.CONTAINS, edited.rules.first { it.id == "one" }.matcher.mode)
assertEquals(AlertTextMode.ANY, edited.rules.first { it.id == "two" }.matcher.mode)
}
@Test
fun newRuleAppendsAfterExistingRulesForEachApp() {
val original = configuration(rule("one", "chat.app", 2))
val ids = iterOf("chat-new", "mail-new")
val edited = AlertRuleEditor.addForApps(original, listOf("chat.app", "mail.app"), definition, ids::next)
assertEquals(listOf(2, 3, 0), edited.rules.map { it.order })
assertEquals(listOf("chat-new", "mail-new"), edited.rules.drop(1).map { it.id })
}
private fun rule(id: String, packageName: String, order: Int) = AlertRule(
id, packageName, order, definition.sources, definition.matcher, definition.outcome, definition.profileId,
)
private fun configuration(vararg rules: AlertRule) = AlertConfiguration(listOf(profile), rules = rules.toList())
private fun iterOf(vararg values: String) = values.iterator()
}