Add custom alert rules

This commit is contained in:
ajp_anton
2026-08-18 01:46:22 +00:00
parent 40a9a1b042
commit 2f546afc89
5 changed files with 126 additions and 0 deletions
@@ -156,6 +156,7 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
page == Page.SETTINGS -> SettingsFragment() page == Page.SETTINGS -> SettingsFragment()
page == Page.ALERTS -> AlertsFragment() page == Page.ALERTS -> AlertsFragment()
page == Page.PROFILES -> ProfilesFragment() page == Page.PROFILES -> ProfilesFragment()
page == Page.RULES -> RulesFragment()
page == Page.LOG_DISPLAY -> LogDisplayFragment() page == Page.LOG_DISPLAY -> LogDisplayFragment()
page == Page.FILTER_LOGGING -> FilterLoggingFragment() page == Page.FILTER_LOGGING -> FilterLoggingFragment()
page == Page.FILTER_APPS -> FilterAppsFragment.newInstance() page == Page.FILTER_APPS -> FilterAppsFragment.newInstance()
@@ -349,6 +350,7 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
SETTINGS(R.id.nav_settings, R.string.page_settings), SETTINGS(R.id.nav_settings, R.string.page_settings),
ALERTS(R.id.nav_alerts, R.string.page_alerts), ALERTS(R.id.nav_alerts, R.string.page_alerts),
PROFILES(R.id.nav_profiles, R.string.page_profiles), PROFILES(R.id.nav_profiles, R.string.page_profiles),
RULES(R.id.nav_rules, R.string.page_rules),
LOG_DISPLAY(R.id.nav_log_display, R.string.page_log_display), LOG_DISPLAY(R.id.nav_log_display, R.string.page_log_display),
FILTER_LOGGING(R.id.nav_filter_logging, R.string.page_filter_logging), FILTER_LOGGING(R.id.nav_filter_logging, R.string.page_filter_logging),
FILTER_APPS(R.id.nav_filter_apps, R.string.page_filter_apps), FILTER_APPS(R.id.nav_filter_apps, R.string.page_filter_apps),
@@ -0,0 +1,112 @@
package se.ajpanton.notificationsmaster
import android.os.Bundle
import android.view.View
import android.widget.ArrayAdapter
import android.widget.Button
import android.widget.CheckBox
import android.widget.EditText
import android.widget.LinearLayout
import android.widget.Spinner
import androidx.fragment.app.Fragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.switchmaterial.SwitchMaterial
import se.ajpanton.notificationsmaster.alerts.AlertConfigurationStore
import se.ajpanton.notificationsmaster.alerts.AlertRuleDefinition
import se.ajpanton.notificationsmaster.alerts.AlertRuleEditor
import se.ajpanton.notificationsmaster.alerts.AlertSource
import se.ajpanton.notificationsmaster.alerts.AlertTextField
import se.ajpanton.notificationsmaster.alerts.AlertTextMatcher
import se.ajpanton.notificationsmaster.alerts.AlertTextMode
import se.ajpanton.notificationsmaster.alerts.AlertOutcome
import se.ajpanton.notificationsmaster.databinding.FragmentRulesBinding
class RulesFragment : Fragment(R.layout.fragment_rules) {
private data class AppChoice(val name: String, val packageName: String) {
override fun toString() = "$name ($packageName)"
}
private var binding: FragmentRulesBinding? = null
private lateinit var store: AlertConfigurationStore
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding = FragmentRulesBinding.bind(view)
store = AlertConfigurationStore(requireContext())
binding!!.addRule.setOnClickListener { showEditor() }
refresh()
}
override fun onDestroyView() { binding = null; super.onDestroyView() }
private fun refresh() {
val configuration = store.load()
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() }
isAllCaps = false
setOnClickListener { showEditor(rule.ruleIds) }
})
}
}
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 } }
val form = LinearLayout(requireContext()).apply { orientation = LinearLayout.VERTICAL; setPadding(dp(24), 0, dp(24), 0) }
val apps = installedApps()
val app = Spinner(requireContext()).apply { adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_dropdown_item, apps) }
val profile = Spinner(requireContext()).apply { adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_dropdown_item, configuration.profiles.map { it.name }) }
val post = CheckBox(requireContext()).apply { text = "Appearing"; isChecked = existing == null || AlertSource.NOTIFICATION_POST in existing.sources }
val update = CheckBox(requireContext()).apply { text = "Edits"; isChecked = existing == null || AlertSource.NOTIFICATION_UPDATE in existing.sources }
val pattern = EditText(requireContext()).apply { hint = "Text to match (optional)"; setText(existing?.matcher?.value) }
val regex = SwitchMaterial(requireContext()).apply { text = "Use regular expression"; isChecked = existing?.matcher?.mode == AlertTextMode.REGEX }
val protected = SwitchMaterial(requireContext()).apply { text = "Play to completion"; isChecked = existing?.playToCompletion == true }
val dnd = SwitchMaterial(requireContext()).apply {
text = "Allow during DND when supported"
isChecked = existing?.allowDuringDnd == true
}
listOf(app, profile, post, update, pattern, regex, protected, dnd).forEach(form::addView)
existing?.let {
app.setSelection(apps.indexOfFirst { value -> value.packageName == it.packageName }.coerceAtLeast(0))
profile.setSelection(configuration.profiles.indexOfFirst { value -> value.id == it.profileId }.coerceAtLeast(0))
}
val dialog = MaterialAlertDialogBuilder(requireContext()).setTitle(if (existing == null) "Add rule" else "Edit rule")
.setView(form).setNegativeButton("Cancel", null).setPositiveButton("Save", null).show()
dialog.getButton(android.content.DialogInterface.BUTTON_POSITIVE).setOnClickListener {
val sources = buildSet {
if (post.isChecked) add(AlertSource.NOTIFICATION_POST)
if (update.isChecked) add(AlertSource.NOTIFICATION_UPDATE)
}
if (sources.isEmpty()) { post.error = "Choose an event"; return@setOnClickListener }
val text = pattern.text.toString().trim()
val matcher = AlertTextMatcher(
AlertTextField.ANY_TEXT,
if (text.isEmpty()) AlertTextMode.ANY else if (regex.isChecked) AlertTextMode.REGEX else AlertTextMode.CONTAINS,
text,
)
val selectedProfile = configuration.profiles[profile.selectedItemPosition]
val definition = AlertRuleDefinition(sources, matcher, AlertOutcome.PLAY_PROFILE, selectedProfile.id, protected.isChecked, dnd.isChecked)
val updated = if (ruleIds == null) {
AlertRuleEditor.addForApps(configuration, listOf(apps[app.selectedItemPosition].packageName), definition)
} else {
AlertRuleEditor.updateConsolidated(configuration, ruleIds, definition)
}
store.save(updated)
dialog.dismiss()
refresh()
}
}
private fun installedApps() = requireContext().packageManager.getInstalledApplications(0).map {
AppChoice(requireContext().packageManager.getApplicationLabel(it).toString(), it.packageName)
}.sortedBy { it.name.lowercase() }
private fun AlertSource.shortName() = if (this == AlertSource.NOTIFICATION_POST) "Appearing" else "Edits"
private fun dp(value: Int) = (value * resources.displayMetrics.density).toInt()
}
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent">
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="@dimen/page_padding">
<Button android:id="@+id/add_rule" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Add rule" />
<LinearLayout android:id="@+id/rules" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="16dp" android:orientation="vertical" />
</LinearLayout>
</ScrollView>
+3
View File
@@ -15,6 +15,9 @@
<item <item
android:id="@+id/nav_profiles" android:id="@+id/nav_profiles"
android:title="@string/navigation_indented_profiles" /> android:title="@string/navigation_indented_profiles" />
<item
android:id="@+id/nav_rules"
android:title="@string/navigation_indented_rules" />
<item <item
android:id="@+id/nav_log_display" android:id="@+id/nav_log_display"
android:title="@string/navigation_indented_log_display" /> android:title="@string/navigation_indented_log_display" />
+2
View File
@@ -9,12 +9,14 @@
<string name="page_settings">Settings</string> <string name="page_settings">Settings</string>
<string name="page_alerts">Alerts</string> <string name="page_alerts">Alerts</string>
<string name="page_profiles">Profiles</string> <string name="page_profiles">Profiles</string>
<string name="page_rules">Rules</string>
<string name="page_filter_apps">Filter apps</string> <string name="page_filter_apps">Filter apps</string>
<string name="page_log_display">Log display</string> <string name="page_log_display">Log display</string>
<string name="page_filter_logging">Filter logging</string> <string name="page_filter_logging">Filter logging</string>
<string name="navigation_indented_log_display">&#160;&#160;&#160;&#160;Log display</string> <string name="navigation_indented_log_display">&#160;&#160;&#160;&#160;Log display</string>
<string name="navigation_indented_alerts">&#160;&#160;&#160;&#160;Alerts</string> <string name="navigation_indented_alerts">&#160;&#160;&#160;&#160;Alerts</string>
<string name="navigation_indented_profiles">&#160;&#160;&#160;&#160;Profiles</string> <string name="navigation_indented_profiles">&#160;&#160;&#160;&#160;Profiles</string>
<string name="navigation_indented_rules">&#160;&#160;&#160;&#160;Rules</string>
<string name="navigation_indented_filter_logging">&#160;&#160;&#160;&#160;Filter logging</string> <string name="navigation_indented_filter_logging">&#160;&#160;&#160;&#160;Filter logging</string>
<string name="navigation_indented_filter_apps">&#160;&#160;&#160;&#160;Filter apps</string> <string name="navigation_indented_filter_apps">&#160;&#160;&#160;&#160;Filter apps</string>
</resources> </resources>