From 44357bf047509ced5df8bb944385bb9efa68a329 Mon Sep 17 00:00:00 2001 From: ajp_anton Date: Wed, 19 Aug 2026 21:19:27 +0000 Subject: [PATCH] Improve alert profile and rule editors --- .../notificationsmaster/AlertRuleDialog.kt | 67 ++++++++++++++++--- .../notificationsmaster/ProfilesFragment.kt | 31 ++++++++- app/src/main/res/menu/drawer_menu.xml | 10 +-- app/src/main/res/values/strings.xml | 4 +- 4 files changed, 94 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/se/ajpanton/notificationsmaster/AlertRuleDialog.kt b/app/src/main/java/se/ajpanton/notificationsmaster/AlertRuleDialog.kt index 3c7846e..f5f96a4 100644 --- a/app/src/main/java/se/ajpanton/notificationsmaster/AlertRuleDialog.kt +++ b/app/src/main/java/se/ajpanton/notificationsmaster/AlertRuleDialog.kt @@ -8,6 +8,9 @@ import android.widget.CheckBox import android.widget.EditText import android.widget.LinearLayout import android.widget.Spinner +import android.widget.ScrollView +import android.widget.TextView +import androidx.core.widget.doAfterTextChanged import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.switchmaterial.SwitchMaterial import se.ajpanton.notificationsmaster.alerts.AlertConfiguration @@ -33,17 +36,7 @@ object AlertRuleDialog { val app = apps?.let { choices -> Button(context).apply { text = "Choose apps" - setOnClickListener { - MaterialAlertDialogBuilder(context).setTitle("Apps this rule applies to") - .setMultiChoiceItems(choices.map { choice -> choice.toString() }.toTypedArray(), choices.map { choice -> choice.packageName in selectedApps }.toBooleanArray()) { _, which, checked -> - if (checked) selectedApps += choices[which].packageName else selectedApps -= choices[which].packageName - text = when (selectedApps.size) { - 0 -> "Choose apps" - 1 -> choices.first { choice -> choice.packageName in selectedApps }.name - else -> choices.first { choice -> choice.packageName in selectedApps }.name + "…" - } - }.setPositiveButton("Done", null).show() - } + setOnClickListener { showAppChooser(context, choices, selectedApps, this) } }.also(form::addView) } val profiles = configuration.profiles @@ -134,4 +127,56 @@ object AlertRuleDialog { } private fun dp(context: Context, value: Int) = (value * context.resources.displayMetrics.density).toInt() + + private fun showAppChooser( + context: Context, + apps: List, + selected: MutableSet, + button: Button, + ) { + val list = LinearLayout(context).apply { orientation = LinearLayout.VERTICAL } + val search = EditText(context).apply { hint = "Filter apps" } + fun updateButton() { + button.text = when (selected.size) { + 0 -> "Choose apps" + 1 -> apps.first { it.packageName in selected }.name + else -> apps.first { it.packageName in selected }.name + "…" + } + } + fun showMatches(query: String) { + val terms = query.lowercase().split(Regex("\\s+")).filter(String::isNotBlank) + list.removeAllViews() + apps.filter { app -> + val searchable = "${app.name} ${app.packageName}".lowercase() + terms.all(searchable::contains) + }.forEach { app -> + val checkBox = CheckBox(context).apply { isChecked = app.packageName in selected } + list.addView(LinearLayout(context).apply { + orientation = LinearLayout.HORIZONTAL + isClickable = true + setPadding(0, dp(context, 4), 0, dp(context, 4)) + addView(checkBox) + addView(LinearLayout(context).apply { + orientation = LinearLayout.VERTICAL + addView(TextView(context).apply { text = app.name; textSize = 16f; setTypeface(typeface, android.graphics.Typeface.BOLD) }) + addView(TextView(context).apply { text = app.packageName; textSize = 12f; alpha = 0.72f }) + }) + setOnClickListener { checkBox.isChecked = !checkBox.isChecked } + }) + checkBox.setOnCheckedChangeListener { _, checked -> + if (checked) selected += app.packageName else selected -= app.packageName + } + } + } + search.doAfterTextChanged { showMatches(it.toString()) } + showMatches("") + val content = LinearLayout(context).apply { + orientation = LinearLayout.VERTICAL + setPadding(dp(context, 24), 0, dp(context, 24), 0) + addView(search) + addView(ScrollView(context).apply { addView(list) }, LinearLayout.LayoutParams(-1, dp(context, 420))) + } + MaterialAlertDialogBuilder(context).setTitle("Apps this rule applies to") + .setView(content).setPositiveButton("Done") { _, _ -> updateButton() }.show() + } } diff --git a/app/src/main/java/se/ajpanton/notificationsmaster/ProfilesFragment.kt b/app/src/main/java/se/ajpanton/notificationsmaster/ProfilesFragment.kt index e603477..1e90276 100644 --- a/app/src/main/java/se/ajpanton/notificationsmaster/ProfilesFragment.kt +++ b/app/src/main/java/se/ajpanton/notificationsmaster/ProfilesFragment.kt @@ -4,6 +4,9 @@ import android.content.Intent import android.media.RingtoneManager import android.net.Uri import android.os.Bundle +import android.os.VibrationAttributes +import android.os.VibrationEffect +import android.os.VibratorManager import android.view.View import android.widget.Button import android.widget.EditText @@ -22,9 +25,10 @@ class ProfilesFragment : Fragment(R.layout.fragment_profiles) { private lateinit var store: AlertConfigurationStore private var selectedSoundUri: String? = null private var soundButton: Button? = null + private var soundName: android.widget.TextView? = null private val chooseSound = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> selectedSoundUri = result.data?.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI, Uri::class.java)?.toString() - soundButton?.text = if (selectedSoundUri == null) "Choose sound" else "Change sound" + updateSoundSelection() } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -37,6 +41,7 @@ class ProfilesFragment : Fragment(R.layout.fragment_profiles) { override fun onDestroyView() { binding = null soundButton = null + soundName = null super.onDestroyView() } @@ -79,9 +84,24 @@ class ProfilesFragment : Fragment(R.layout.fragment_profiles) { } } soundButton = pickerButton + val selectedSound = android.widget.TextView(context).apply { text = soundLabel() } + soundName = selectedSound + val testVibration = Button(context).apply { + text = "Test vibration" + setOnClickListener { + parsePattern(vibration.text.toString())?.takeIf { it.any { duration -> duration > 0 } }?.let { pattern -> + context.getSystemService(VibratorManager::class.java).defaultVibrator.vibrate( + VibrationEffect.createWaveform(pattern.toLongArray(), -1), + VibrationAttributes.Builder().setUsage(VibrationAttributes.USAGE_NOTIFICATION).build(), + ) + } ?: run { vibration.error = "Enter a vibration pattern first" } + } + } form.addView(name) form.addView(pickerButton) + form.addView(selectedSound) form.addView(vibration) + form.addView(testVibration) val dialog = MaterialAlertDialogBuilder(context) .setTitle(if (existing == null) "Add profile" else "Edit profile") .setView(form) @@ -134,5 +154,14 @@ class ProfilesFragment : Fragment(R.layout.fragment_profiles) { if (profile.vibrationPattern.any { it > 0 }) add("Vibration") }.joinToString(" + ") + private fun updateSoundSelection() { + soundButton?.text = if (selectedSoundUri == null) "Choose sound" else "Change sound" + soundName?.text = soundLabel() + } + + private fun soundLabel(): String = selectedSoundUri?.let { uri -> + RingtoneManager.getRingtone(requireContext(), Uri.parse(uri))?.getTitle(requireContext()) ?: uri + } ?: "No sound selected" + private fun dp(value: Int) = (value * resources.displayMetrics.density).toInt() } diff --git a/app/src/main/res/menu/drawer_menu.xml b/app/src/main/res/menu/drawer_menu.xml index df84279..0533a2a 100644 --- a/app/src/main/res/menu/drawer_menu.xml +++ b/app/src/main/res/menu/drawer_menu.xml @@ -6,12 +6,9 @@ android:title="@string/page_view_logs" /> - + android:title="@string/navigation_alerts_header" /> @@ -24,6 +21,11 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8757266..0c01c5a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4,7 +4,8 @@ Notifications Master listener Open navigation Close navigation - Settings + Alert control + Logging View logs Settings Alerts @@ -16,7 +17,6 @@ Log display Filter logging     Log display -     Alerts     Alert status     Profiles     Rules