diff --git a/app/src/main/java/se/ajpanton/notificationsmaster/AlertRuleDialog.kt b/app/src/main/java/se/ajpanton/notificationsmaster/AlertRuleDialog.kt index a8c005f..3c7846e 100644 --- a/app/src/main/java/se/ajpanton/notificationsmaster/AlertRuleDialog.kt +++ b/app/src/main/java/se/ajpanton/notificationsmaster/AlertRuleDialog.kt @@ -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() diff --git a/app/src/main/java/se/ajpanton/notificationsmaster/AppRulesFragment.kt b/app/src/main/java/se/ajpanton/notificationsmaster/AppRulesFragment.kt index 2737f12..ef078a4 100644 --- a/app/src/main/java/se/ajpanton/notificationsmaster/AppRulesFragment.kt +++ b/app/src/main/java/se/ajpanton/notificationsmaster/AppRulesFragment.kt @@ -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)) diff --git a/app/src/main/java/se/ajpanton/notificationsmaster/RulesFragment.kt b/app/src/main/java/se/ajpanton/notificationsmaster/RulesFragment.kt index 3b0f10a..7636d4d 100644 --- a/app/src/main/java/se/ajpanton/notificationsmaster/RulesFragment.kt +++ b/app/src/main/java/se/ajpanton/notificationsmaster/RulesFragment.kt @@ -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? = 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" + } } diff --git a/app/src/test/java/se/ajpanton/notificationsmaster/alerts/AlertNotificationDispatcherTest.kt b/app/src/test/java/se/ajpanton/notificationsmaster/alerts/AlertNotificationDispatcherTest.kt index 07ac6f9..f9203f6 100644 --- a/app/src/test/java/se/ajpanton/notificationsmaster/alerts/AlertNotificationDispatcherTest.kt +++ b/app/src/test/java/se/ajpanton/notificationsmaster/alerts/AlertNotificationDispatcherTest.kt @@ -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),