Add per-app alert rule management
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
package se.ajpanton.notificationsmaster
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.CheckBox
|
||||
import android.widget.EditText
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.Spinner
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.switchmaterial.SwitchMaterial
|
||||
import se.ajpanton.notificationsmaster.alerts.AlertConfiguration
|
||||
import se.ajpanton.notificationsmaster.alerts.AlertOutcome
|
||||
import se.ajpanton.notificationsmaster.alerts.AlertRule
|
||||
import se.ajpanton.notificationsmaster.alerts.AlertRuleDefinition
|
||||
import se.ajpanton.notificationsmaster.alerts.AlertSource
|
||||
import se.ajpanton.notificationsmaster.alerts.AlertTextField
|
||||
import se.ajpanton.notificationsmaster.alerts.AlertTextMatcher
|
||||
import se.ajpanton.notificationsmaster.alerts.AlertTextMode
|
||||
|
||||
object AlertRuleDialog {
|
||||
fun show(
|
||||
context: Context,
|
||||
configuration: AlertConfiguration,
|
||||
existing: AlertRule?,
|
||||
apps: List<InstalledApp>? = null,
|
||||
onSave: (String?, AlertRuleDefinition) -> Unit,
|
||||
) {
|
||||
val form = LinearLayout(context).apply { orientation = LinearLayout.VERTICAL; setPadding(dp(context, 24), 0, dp(context, 24), 0) }
|
||||
val app = apps?.let {
|
||||
Spinner(context).apply {
|
||||
adapter = ArrayAdapter(context, android.R.layout.simple_spinner_dropdown_item, it)
|
||||
existing?.let { rule -> setSelection(apps.indexOfFirst { choice -> choice.packageName == rule.packageName }.coerceAtLeast(0)) }
|
||||
}.also(form::addView)
|
||||
}
|
||||
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)) }
|
||||
}
|
||||
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 pattern = EditText(context).apply { hint = "Text to match (optional)"; setText(existing?.matcher?.value) }
|
||||
val regex = SwitchMaterial(context).apply { text = "Use regular expression"; isChecked = existing?.matcher?.mode == AlertTextMode.REGEX }
|
||||
val protected = SwitchMaterial(context).apply { text = "Play to completion"; isChecked = existing?.playToCompletion == true }
|
||||
val dnd = SwitchMaterial(context).apply { text = "Allow during DND when supported"; isChecked = existing?.allowDuringDnd == true }
|
||||
listOf(profile, post, update, pattern, regex, protected, dnd).forEach(form::addView)
|
||||
val dialog = MaterialAlertDialogBuilder(context).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 value = pattern.text.toString().trim()
|
||||
val matcher = runCatching {
|
||||
AlertTextMatcher(
|
||||
AlertTextField.ANY_TEXT,
|
||||
if (value.isEmpty()) AlertTextMode.ANY else if (regex.isChecked) AlertTextMode.REGEX else AlertTextMode.CONTAINS,
|
||||
value,
|
||||
)
|
||||
}.getOrElse {
|
||||
pattern.error = "Invalid regular expression"
|
||||
return@setOnClickListener
|
||||
}
|
||||
val definition = AlertRuleDefinition(
|
||||
sources,
|
||||
matcher,
|
||||
AlertOutcome.PLAY_PROFILE,
|
||||
configuration.profiles[profile.selectedItemPosition].id,
|
||||
protected.isChecked,
|
||||
dnd.isChecked,
|
||||
)
|
||||
onSave(app?.selectedItem?.let { it as InstalledApp }?.packageName, definition)
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private fun dp(context: Context, value: Int) = (value * context.resources.displayMetrics.density).toInt()
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package se.ajpanton.notificationsmaster
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import se.ajpanton.notificationsmaster.alerts.AlertConfigurationStore
|
||||
import se.ajpanton.notificationsmaster.alerts.AlertRule
|
||||
import se.ajpanton.notificationsmaster.alerts.AlertRuleEditor
|
||||
import se.ajpanton.notificationsmaster.databinding.FragmentAppRulesBinding
|
||||
|
||||
class AppRulesFragment : Fragment(R.layout.fragment_app_rules) {
|
||||
private var binding: FragmentAppRulesBinding? = null
|
||||
private lateinit var packageName: String
|
||||
private lateinit var store: AlertConfigurationStore
|
||||
private val adapter = RuleAdapter(::edit)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
packageName = requireArguments().getString(ARG_PACKAGE)!!
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
binding = FragmentAppRulesBinding.bind(view)
|
||||
store = AlertConfigurationStore(requireContext())
|
||||
binding!!.rules.adapter = adapter
|
||||
binding!!.addRule.setOnClickListener { edit(null) }
|
||||
ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0) {
|
||||
override fun onMove(recyclerView: RecyclerView, holder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
|
||||
adapter.move(holder.bindingAdapterPosition, target.bindingAdapterPosition)
|
||||
saveOrder()
|
||||
return true
|
||||
}
|
||||
override fun onSwiped(holder: RecyclerView.ViewHolder, direction: Int) = Unit
|
||||
}).attachToRecyclerView(binding!!.rules)
|
||||
refresh()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
activity?.title = InstalledApps.name(requireContext(), packageName)
|
||||
}
|
||||
|
||||
override fun onDestroyView() { binding = null; super.onDestroyView() }
|
||||
|
||||
private fun refresh() = adapter.submit(store.load().rules.filter { it.packageName == packageName }.sortedBy { it.order })
|
||||
|
||||
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) { _, definition ->
|
||||
store.save(if (existing == null) AlertRuleEditor.addForApps(configuration, listOf(packageName), definition) else AlertRuleEditor.updateForApp(configuration, existing.id, definition))
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveOrder() {
|
||||
store.save(AlertRuleEditor.reorderForApp(store.load(), packageName, adapter.items.map { it.id }))
|
||||
}
|
||||
|
||||
private class RuleAdapter(private val onClick: (AlertRule) -> Unit) : RecyclerView.Adapter<RuleAdapter.Holder>() {
|
||||
var items = emptyList<AlertRule>(); private set
|
||||
fun submit(rules: List<AlertRule>) { items = rules; notifyDataSetChanged() }
|
||||
fun move(from: Int, to: Int) { items = items.toMutableList().apply { add(to, removeAt(from)) }; notifyItemMoved(from, to) }
|
||||
override fun onCreateViewHolder(parent: android.view.ViewGroup, viewType: Int) = Holder(TextView(parent.context).apply {
|
||||
layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT)
|
||||
val padding = (16 * parent.resources.displayMetrics.density).toInt()
|
||||
setPadding(padding, padding, padding, padding); textSize = 16f; isClickable = true
|
||||
})
|
||||
override fun onBindViewHolder(holder: Holder, position: Int) {
|
||||
val rule = items[position]
|
||||
holder.text.text = rule.name ?: "Rule ${position + 1}" + "\n" + rule.sources.joinToString { if (it.name == "NOTIFICATION_POST") "Appearing" else "Edits" } + rule.matcher.value.takeIf { it.isNotBlank() }?.let { " • contains $it" }.orEmpty()
|
||||
holder.text.setOnClickListener { onClick(rule) }
|
||||
}
|
||||
override fun getItemCount() = items.size
|
||||
class Holder(val text: TextView) : RecyclerView.ViewHolder(text)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ARG_PACKAGE = "package"
|
||||
fun newInstance(packageName: String) = AppRulesFragment().apply { arguments = bundleOf(ARG_PACKAGE to packageName) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package se.ajpanton.notificationsmaster
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.Button
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.commit
|
||||
import se.ajpanton.notificationsmaster.alerts.AlertConfigurationStore
|
||||
import se.ajpanton.notificationsmaster.databinding.FragmentAppsBinding
|
||||
|
||||
class AppsFragment : Fragment(R.layout.fragment_apps) {
|
||||
private var binding: FragmentAppsBinding? = null
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
binding = FragmentAppsBinding.bind(view)
|
||||
refresh()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
activity?.title = getString(R.string.page_apps)
|
||||
refresh()
|
||||
}
|
||||
|
||||
override fun onDestroyView() { binding = null; super.onDestroyView() }
|
||||
|
||||
private fun refresh() {
|
||||
val container = binding?.apps ?: return
|
||||
val context = requireContext()
|
||||
val grouped = AlertConfigurationStore(context).load().rules.groupBy { it.packageName }
|
||||
container.removeAllViews()
|
||||
grouped.keys.sortedBy { InstalledApps.name(context, it).lowercase() }.forEach { packageName ->
|
||||
container.addView(Button(context).apply {
|
||||
val count = grouped.getValue(packageName).size
|
||||
text = InstalledApps.name(context, packageName) + "\n" + packageName + " • " + count + if (count == 1) " rule" else " rules"
|
||||
isAllCaps = false
|
||||
setOnClickListener { open(packageName) }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun open(packageName: String) = parentFragmentManager.commit {
|
||||
replace(R.id.content_frame, AppRulesFragment.newInstance(packageName))
|
||||
addToBackStack(null)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package se.ajpanton.notificationsmaster
|
||||
|
||||
import android.content.Context
|
||||
|
||||
data class InstalledApp(val name: String, val packageName: String) {
|
||||
override fun toString() = "$name ($packageName)"
|
||||
}
|
||||
|
||||
object InstalledApps {
|
||||
fun all(context: Context): List<InstalledApp> = context.packageManager.getInstalledApplications(0).map {
|
||||
InstalledApp(context.packageManager.getApplicationLabel(it).toString(), it.packageName)
|
||||
}.sortedBy { it.name.lowercase() }
|
||||
|
||||
fun name(context: Context, packageName: String): String = runCatching {
|
||||
context.packageManager.getApplicationLabel(context.packageManager.getApplicationInfo(packageName, 0)).toString()
|
||||
}.getOrDefault(packageName)
|
||||
}
|
||||
@@ -157,6 +157,7 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
|
||||
page == Page.ALERTS -> AlertsFragment()
|
||||
page == Page.PROFILES -> ProfilesFragment()
|
||||
page == Page.RULES -> RulesFragment()
|
||||
page == Page.APPS -> AppsFragment()
|
||||
page == Page.LOG_DISPLAY -> LogDisplayFragment()
|
||||
page == Page.FILTER_LOGGING -> FilterLoggingFragment()
|
||||
page == Page.FILTER_APPS -> FilterAppsFragment.newInstance()
|
||||
@@ -351,6 +352,7 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
|
||||
ALERTS(R.id.nav_alerts, R.string.page_alerts),
|
||||
PROFILES(R.id.nav_profiles, R.string.page_profiles),
|
||||
RULES(R.id.nav_rules, R.string.page_rules),
|
||||
APPS(R.id.nav_apps, R.string.page_apps),
|
||||
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_APPS(R.id.nav_filter_apps, R.string.page_filter_apps),
|
||||
|
||||
@@ -2,30 +2,14 @@ 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
|
||||
|
||||
@@ -58,55 +42,16 @@ class RulesFragment : Fragment(R.layout.fragment_rules) {
|
||||
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)
|
||||
AlertRuleDialog.show(requireContext(), configuration, existing, if (existing == null) InstalledApps.all(requireContext()) else null) { packageName, definition ->
|
||||
val updated = if (ruleIds == null) {
|
||||
AlertRuleEditor.addForApps(configuration, listOf(apps[app.selectedItemPosition].packageName), definition)
|
||||
AlertRuleEditor.addForApps(configuration, listOfNotNull(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()
|
||||
}
|
||||
|
||||
@@ -76,6 +76,18 @@ object AlertRuleEditor {
|
||||
return configuration.copy(rules = configuration.rules + added)
|
||||
}
|
||||
|
||||
fun reorderForApp(
|
||||
configuration: AlertConfiguration,
|
||||
packageName: String,
|
||||
orderedRuleIds: List<String>,
|
||||
): AlertConfiguration {
|
||||
require(configuration.rules.filter { it.packageName == packageName }.map { it.id }.toSet() == orderedRuleIds.toSet())
|
||||
val positions = orderedRuleIds.withIndex().associate { it.value to it.index }
|
||||
return configuration.copy(rules = configuration.rules.map { rule ->
|
||||
if (rule.packageName == packageName) rule.copy(order = positions.getValue(rule.id)) else rule
|
||||
})
|
||||
}
|
||||
|
||||
private fun AlertRule.definition() = AlertRuleDefinition(
|
||||
sources, matcher, outcome, profileId, playToCompletion, allowDuringDnd, enabled, name,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
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" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="Drag rules to set their priority. The first matching rule is used."
|
||||
android:textSize="12sp" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rules"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_weight="1"
|
||||
android:clipToPadding="false"
|
||||
android:paddingBottom="16dp" />
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,25 @@
|
||||
<?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">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Apps with custom rules"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/apps"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:orientation="vertical" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
@@ -18,6 +18,9 @@
|
||||
<item
|
||||
android:id="@+id/nav_rules"
|
||||
android:title="@string/navigation_indented_rules" />
|
||||
<item
|
||||
android:id="@+id/nav_apps"
|
||||
android:title="@string/navigation_indented_apps" />
|
||||
<item
|
||||
android:id="@+id/nav_log_display"
|
||||
android:title="@string/navigation_indented_log_display" />
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
<string name="page_alerts">Alerts</string>
|
||||
<string name="page_profiles">Profiles</string>
|
||||
<string name="page_rules">Rules</string>
|
||||
<string name="page_apps">Apps</string>
|
||||
<string name="page_filter_apps">Filter apps</string>
|
||||
<string name="page_log_display">Log display</string>
|
||||
<string name="page_filter_logging">Filter logging</string>
|
||||
@@ -17,6 +18,7 @@
|
||||
<string name="navigation_indented_alerts">    Alerts</string>
|
||||
<string name="navigation_indented_profiles">    Profiles</string>
|
||||
<string name="navigation_indented_rules">    Rules</string>
|
||||
<string name="navigation_indented_apps">    Apps</string>
|
||||
<string name="navigation_indented_filter_logging">    Filter logging</string>
|
||||
<string name="navigation_indented_filter_apps">    Filter apps</string>
|
||||
</resources>
|
||||
|
||||
@@ -55,6 +55,17 @@ class AlertRuleEditorTest {
|
||||
assertEquals(listOf("chat-new", "mail-new"), edited.rules.drop(1).map { it.id })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun reordersOnlyTheRulesForThatApp() {
|
||||
val original = configuration(rule("first", "chat.app", 0), rule("other", "mail.app", 0), rule("second", "chat.app", 1))
|
||||
|
||||
val edited = AlertRuleEditor.reorderForApp(original, "chat.app", listOf("second", "first"))
|
||||
|
||||
assertEquals(1, edited.rules.first { it.id == "first" }.order)
|
||||
assertEquals(0, edited.rules.first { it.id == "second" }.order)
|
||||
assertEquals(0, edited.rules.first { it.id == "other" }.order)
|
||||
}
|
||||
|
||||
private fun rule(id: String, packageName: String, order: Int) = AlertRule(
|
||||
id, packageName, order, definition.sources, definition.matcher, definition.outcome, definition.profileId,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user