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) }.also(form::addView)
} }
val profiles = configuration.profiles
val profile = Spinner(context).apply { val profile = Spinner(context).apply {
adapter = ArrayAdapter(context, android.R.layout.simple_spinner_dropdown_item, configuration.profiles.map { it.name }) adapter = ArrayAdapter(context, android.R.layout.simple_spinner_dropdown_item, profiles.map { it.name })
existing?.let { rule -> setSelection(configuration.profiles.indexOfFirst { it.id == rule.profileId }.coerceAtLeast(0)) } 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 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 } 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)" text = "Allow during DND (requires system support)"
isChecked = existing?.allowDuringDnd == true 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 { 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." 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 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") val dialog = MaterialAlertDialogBuilder(context).setTitle(if (existing == null) "Add rule" else "Edit rule")
.setView(form).setNegativeButton("Cancel", null).setPositiveButton("Save", null) .setView(form).setNegativeButton("Cancel", null).setPositiveButton("Save", null)
.apply { if (onDelete != null) setNeutralButton("Delete", null) }.show() .apply { if (onDelete != null) setNeutralButton("Delete", null) }.show()
@@ -85,13 +106,20 @@ object AlertRuleDialog {
pattern.error = "Invalid regular expression" pattern.error = "Invalid regular expression"
return@setOnClickListener 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( val definition = AlertRuleDefinition(
sources, sources,
matcher, matcher,
AlertOutcome.PLAY_PROFILE, selectedOutcome,
configuration.profiles[profile.selectedItemPosition].id, profileId,
protected.isChecked, protected.isEnabled && protected.isChecked,
dnd.isChecked, dnd.isEnabled && dnd.isChecked,
) )
onSave(apps?.let { selectedApps.toList() }, definition) onSave(apps?.let { selectedApps.toList() }, definition)
dialog.dismiss() dialog.dismiss()
@@ -53,10 +53,6 @@ class AppRulesFragment : Fragment(R.layout.fragment_app_rules) {
private fun edit(existing: AlertRule?) { private fun edit(existing: AlertRule?) {
val configuration = store.load() 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, AlertRuleDialog.show(requireContext(), configuration, existing,
onSave = { _, definition -> onSave = { _, definition ->
store.save(if (existing == null) AlertRuleEditor.addForApps(configuration, listOf(packageName), definition) else AlertRuleEditor.updateForApp(configuration, existing.id, 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 android.widget.Button
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import se.ajpanton.notificationsmaster.alerts.AlertConfigurationStore 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.AlertRuleEditor
import se.ajpanton.notificationsmaster.alerts.AlertSource import se.ajpanton.notificationsmaster.alerts.AlertSource
import se.ajpanton.notificationsmaster.databinding.FragmentRulesBinding import se.ajpanton.notificationsmaster.databinding.FragmentRulesBinding
@@ -27,8 +29,8 @@ class RulesFragment : Fragment(R.layout.fragment_rules) {
binding!!.rules.removeAllViews() binding!!.rules.removeAllViews()
AlertRuleEditor.consolidate(configuration.rules).forEach { rule -> AlertRuleEditor.consolidate(configuration.rules).forEach { rule ->
binding!!.rules.addView(Button(requireContext()).apply { binding!!.rules.addView(Button(requireContext()).apply {
val profile = configuration.profiles.firstOrNull { it.id == rule.definition.profileId }?.name ?: "Missing profile" text = rule.definition.outcomeLabel(configuration) + "\n" +
text = profile + "\n" + rule.packageNames.joinToString() + "" + rule.definition.sources.joinToString { it.shortName() } rule.packageNames.joinToString() + "" + rule.definition.sources.joinToString { it.shortName() }
isAllCaps = false isAllCaps = false
setOnClickListener { showEditor(rule.ruleIds) } setOnClickListener { showEditor(rule.ruleIds) }
}) })
@@ -37,10 +39,6 @@ class RulesFragment : Fragment(R.layout.fragment_rules) {
private fun showEditor(ruleIds: Set<String>? = null) { private fun showEditor(ruleIds: Set<String>? = null) {
val configuration = store.load() 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 } } val existing = ruleIds?.let { ids -> configuration.rules.first { it.id in ids } }
AlertRuleDialog.show( AlertRuleDialog.show(
requireContext(), configuration, existing, if (existing == null) InstalledApps.all(requireContext()) else null, 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 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 }) 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( private fun configuration(enabled: Boolean = true) = AlertConfiguration(
enabled = enabled, enabled = enabled,
profiles = listOf(profile), profiles = listOf(profile),