Improve alert profile and rule editors

This commit is contained in:
ajp_anton
2026-08-19 21:19:27 +00:00
parent de984fb36c
commit 44357bf047
4 changed files with 94 additions and 18 deletions
@@ -8,6 +8,9 @@ import android.widget.CheckBox
import android.widget.EditText import android.widget.EditText
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.Spinner 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.dialog.MaterialAlertDialogBuilder
import com.google.android.material.switchmaterial.SwitchMaterial import com.google.android.material.switchmaterial.SwitchMaterial
import se.ajpanton.notificationsmaster.alerts.AlertConfiguration import se.ajpanton.notificationsmaster.alerts.AlertConfiguration
@@ -33,17 +36,7 @@ object AlertRuleDialog {
val app = apps?.let { choices -> val app = apps?.let { choices ->
Button(context).apply { Button(context).apply {
text = "Choose apps" text = "Choose apps"
setOnClickListener { setOnClickListener { showAppChooser(context, choices, selectedApps, this) }
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()
}
}.also(form::addView) }.also(form::addView)
} }
val profiles = configuration.profiles 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 dp(context: Context, value: Int) = (value * context.resources.displayMetrics.density).toInt()
private fun showAppChooser(
context: Context,
apps: List<InstalledApp>,
selected: MutableSet<String>,
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()
}
} }
@@ -4,6 +4,9 @@ import android.content.Intent
import android.media.RingtoneManager import android.media.RingtoneManager
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.VibrationAttributes
import android.os.VibrationEffect
import android.os.VibratorManager
import android.view.View import android.view.View
import android.widget.Button import android.widget.Button
import android.widget.EditText import android.widget.EditText
@@ -22,9 +25,10 @@ class ProfilesFragment : Fragment(R.layout.fragment_profiles) {
private lateinit var store: AlertConfigurationStore private lateinit var store: AlertConfigurationStore
private var selectedSoundUri: String? = null private var selectedSoundUri: String? = null
private var soundButton: Button? = null private var soundButton: Button? = null
private var soundName: android.widget.TextView? = null
private val chooseSound = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> private val chooseSound = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
selectedSoundUri = result.data?.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI, Uri::class.java)?.toString() 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?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -37,6 +41,7 @@ class ProfilesFragment : Fragment(R.layout.fragment_profiles) {
override fun onDestroyView() { override fun onDestroyView() {
binding = null binding = null
soundButton = null soundButton = null
soundName = null
super.onDestroyView() super.onDestroyView()
} }
@@ -79,9 +84,24 @@ class ProfilesFragment : Fragment(R.layout.fragment_profiles) {
} }
} }
soundButton = pickerButton 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(name)
form.addView(pickerButton) form.addView(pickerButton)
form.addView(selectedSound)
form.addView(vibration) form.addView(vibration)
form.addView(testVibration)
val dialog = MaterialAlertDialogBuilder(context) val dialog = MaterialAlertDialogBuilder(context)
.setTitle(if (existing == null) "Add profile" else "Edit profile") .setTitle(if (existing == null) "Add profile" else "Edit profile")
.setView(form) .setView(form)
@@ -134,5 +154,14 @@ class ProfilesFragment : Fragment(R.layout.fragment_profiles) {
if (profile.vibrationPattern.any { it > 0 }) add("Vibration") if (profile.vibrationPattern.any { it > 0 }) add("Vibration")
}.joinToString(" + ") }.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() private fun dp(value: Int) = (value * resources.displayMetrics.density).toInt()
} }
+6 -4
View File
@@ -6,12 +6,9 @@
android:title="@string/page_view_logs" /> android:title="@string/page_view_logs" />
</group> </group>
<group android:checkableBehavior="single"> <group android:checkableBehavior="single">
<item
android:id="@+id/nav_settings"
android:title="@string/navigation_settings_header" />
<item <item
android:id="@+id/nav_alerts" android:id="@+id/nav_alerts"
android:title="@string/navigation_indented_alerts" /> android:title="@string/navigation_alerts_header" />
<item <item
android:id="@+id/nav_alert_status" android:id="@+id/nav_alert_status"
android:title="@string/navigation_indented_alert_status" /> android:title="@string/navigation_indented_alert_status" />
@@ -24,6 +21,11 @@
<item <item
android:id="@+id/nav_apps" android:id="@+id/nav_apps"
android:title="@string/navigation_indented_apps" /> android:title="@string/navigation_indented_apps" />
</group>
<group android:checkableBehavior="single">
<item
android:id="@+id/nav_settings"
android:title="@string/navigation_logging_header" />
<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 -2
View File
@@ -4,7 +4,8 @@
<string name="notification_listener_label">Notifications Master listener</string> <string name="notification_listener_label">Notifications Master listener</string>
<string name="navigation_open">Open navigation</string> <string name="navigation_open">Open navigation</string>
<string name="navigation_close">Close navigation</string> <string name="navigation_close">Close navigation</string>
<string name="navigation_settings_header">Settings</string> <string name="navigation_alerts_header">Alert control</string>
<string name="navigation_logging_header">Logging</string>
<string name="page_view_logs">View logs</string> <string name="page_view_logs">View logs</string>
<string name="page_settings">Settings</string> <string name="page_settings">Settings</string>
<string name="page_alerts">Alerts</string> <string name="page_alerts">Alerts</string>
@@ -16,7 +17,6 @@
<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_alert_status">&#160;&#160;&#160;&#160;Alert status</string> <string name="navigation_indented_alert_status">&#160;&#160;&#160;&#160;Alert status</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_rules">&#160;&#160;&#160;&#160;Rules</string>