Expose alert rule outcomes

This commit is contained in:
ajp_anton
2026-08-18 16:19:29 +00:00
parent bd727eaec0
commit 8b66456d8c
4 changed files with 55 additions and 17 deletions
@@ -46,9 +46,19 @@ object AlertRuleDialog {
}
}.also(form::addView)
}
val profiles = configuration.profiles
val profile = Spinner(context).apply {
adapter = ArrayAdapter(context, android.R.layout.simple_spinner_dropdown_item, configuration.profiles.map { it.name })
existing?.let { rule -> setSelection(configuration.profiles.indexOfFirst { it.id == rule.profileId }.coerceAtLeast(0)) }
adapter = ArrayAdapter(context, android.R.layout.simple_spinner_dropdown_item, profiles.map { it.name })
existing?.let { rule -> setSelection(profiles.indexOfFirst { it.id == rule.profileId }.coerceAtLeast(0)) }
}
val outcomes = listOf(
AlertOutcome.PLAY_PROFILE to "Play profile",
AlertOutcome.SILENCE_ORIGINAL to "Silence original alert",
AlertOutcome.PASS_THROUGH to "Keep original alert",
)
val outcome = Spinner(context).apply {
adapter = ArrayAdapter(context, android.R.layout.simple_spinner_dropdown_item, outcomes.map { it.second })
setSelection(outcomes.indexOfFirst { it.first == existing?.outcome }.coerceAtLeast(0))
}
val post = CheckBox(context).apply { text = "Appearing"; isChecked = existing == null || AlertSource.NOTIFICATION_POST in existing.sources }
val update = CheckBox(context).apply { text = "Edits"; isChecked = existing == null || AlertSource.NOTIFICATION_UPDATE in existing.sources }
@@ -59,11 +69,22 @@ object AlertRuleDialog {
text = "Allow during DND (requires system support)"
isChecked = existing?.allowDuringDnd == true
}
listOf(profile, post, update, pattern, regex, protected, dnd).forEach(form::addView)
listOf(outcome, profile, post, update, pattern, regex, protected, dnd).forEach(form::addView)
form.addView(android.widget.TextView(context).apply {
text = "Android blocks direct custom alerts during Do Not Disturb. This setting is retained for optional system support, but does not bypass Do Not Disturb in the normal app."
textSize = 14f
})
fun updateOutcomeFields() {
val playsProfile = outcomes[outcome.selectedItemPosition].first == AlertOutcome.PLAY_PROFILE
profile.isEnabled = playsProfile
protected.isEnabled = playsProfile
dnd.isEnabled = playsProfile
}
outcome.onItemSelectedListener = object : android.widget.AdapterView.OnItemSelectedListener {
override fun onNothingSelected(parent: android.widget.AdapterView<*>?) = Unit
override fun onItemSelected(parent: android.widget.AdapterView<*>?, view: View?, position: Int, id: Long) = updateOutcomeFields()
}
updateOutcomeFields()
val dialog = MaterialAlertDialogBuilder(context).setTitle(if (existing == null) "Add rule" else "Edit rule")
.setView(form).setNegativeButton("Cancel", null).setPositiveButton("Save", null)
.apply { if (onDelete != null) setNeutralButton("Delete", null) }.show()
@@ -85,13 +106,20 @@ object AlertRuleDialog {
pattern.error = "Invalid regular expression"
return@setOnClickListener
}
val selectedOutcome = outcomes[outcome.selectedItemPosition].first
val profileId = if (selectedOutcome == AlertOutcome.PLAY_PROFILE) {
profiles.getOrNull(profile.selectedItemPosition)?.id ?: run {
android.widget.Toast.makeText(context, "Create a profile first", android.widget.Toast.LENGTH_LONG).show()
return@setOnClickListener
}
} else null
val definition = AlertRuleDefinition(
sources,
matcher,
AlertOutcome.PLAY_PROFILE,
configuration.profiles[profile.selectedItemPosition].id,
protected.isChecked,
dnd.isChecked,
selectedOutcome,
profileId,
protected.isEnabled && protected.isChecked,
dnd.isEnabled && dnd.isChecked,
)
onSave(apps?.let { selectedApps.toList() }, definition)
dialog.dismiss()
@@ -53,10 +53,6 @@ class AppRulesFragment : Fragment(R.layout.fragment_app_rules) {
private fun edit(existing: AlertRule?) {
val configuration = store.load()
if (configuration.profiles.isEmpty()) {
android.widget.Toast.makeText(requireContext(), "Create a profile first", android.widget.Toast.LENGTH_LONG).show()
return
}
AlertRuleDialog.show(requireContext(), configuration, existing,
onSave = { _, definition ->
store.save(if (existing == null) AlertRuleEditor.addForApps(configuration, listOf(packageName), definition) else AlertRuleEditor.updateForApp(configuration, existing.id, definition))
@@ -5,6 +5,8 @@ import android.view.View
import android.widget.Button
import androidx.fragment.app.Fragment
import se.ajpanton.notificationsmaster.alerts.AlertConfigurationStore
import se.ajpanton.notificationsmaster.alerts.AlertOutcome
import se.ajpanton.notificationsmaster.alerts.AlertRuleDefinition
import se.ajpanton.notificationsmaster.alerts.AlertRuleEditor
import se.ajpanton.notificationsmaster.alerts.AlertSource
import se.ajpanton.notificationsmaster.databinding.FragmentRulesBinding
@@ -27,8 +29,8 @@ class RulesFragment : Fragment(R.layout.fragment_rules) {
binding!!.rules.removeAllViews()
AlertRuleEditor.consolidate(configuration.rules).forEach { rule ->
binding!!.rules.addView(Button(requireContext()).apply {
val profile = configuration.profiles.firstOrNull { it.id == rule.definition.profileId }?.name ?: "Missing profile"
text = profile + "\n" + rule.packageNames.joinToString() + "" + rule.definition.sources.joinToString { it.shortName() }
text = rule.definition.outcomeLabel(configuration) + "\n" +
rule.packageNames.joinToString() + "" + rule.definition.sources.joinToString { it.shortName() }
isAllCaps = false
setOnClickListener { showEditor(rule.ruleIds) }
})
@@ -37,10 +39,6 @@ class RulesFragment : Fragment(R.layout.fragment_rules) {
private fun showEditor(ruleIds: Set<String>? = null) {
val configuration = store.load()
if (configuration.profiles.isEmpty()) {
android.widget.Toast.makeText(requireContext(), "Create a profile first", android.widget.Toast.LENGTH_LONG).show()
return
}
val existing = ruleIds?.let { ids -> configuration.rules.first { it.id in ids } }
AlertRuleDialog.show(
requireContext(), configuration, existing, if (existing == null) InstalledApps.all(requireContext()) else null,
@@ -58,4 +56,10 @@ class RulesFragment : Fragment(R.layout.fragment_rules) {
}
private fun AlertSource.shortName() = if (this == AlertSource.NOTIFICATION_POST) "Appearing" else "Edits"
private fun AlertRuleDefinition.outcomeLabel(configuration: se.ajpanton.notificationsmaster.alerts.AlertConfiguration) = when (outcome) {
AlertOutcome.PLAY_PROFILE -> configuration.profiles.firstOrNull { it.id == profileId }?.name ?: "Missing profile"
AlertOutcome.SILENCE_ORIGINAL -> "Silence original alert"
AlertOutcome.PASS_THROUGH -> "Keep original alert"
}
}
@@ -51,6 +51,16 @@ class AlertNotificationDispatcherTest {
assertEquals(listOf("rule", "second"), AlertNotificationDispatcher.dispatch(configuration, event, false).map { it.ruleId })
}
@Test
fun silenceAndPassThroughRulesDoNotQueueCustomPlayback() {
AlertOutcome.entries.filter { it != AlertOutcome.PLAY_PROFILE }.forEach { outcome ->
val rule = rule.copy(outcome = outcome, profileId = null)
val configuration = AlertConfiguration(rules = listOf(rule))
assertTrue(AlertNotificationDispatcher.dispatch(configuration, event, false).isEmpty())
}
}
private fun configuration(enabled: Boolean = true) = AlertConfiguration(
enabled = enabled,
profiles = listOf(profile),