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.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<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.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()
}
+6 -4
View File
@@ -6,12 +6,9 @@
android:title="@string/page_view_logs" />
</group>
<group android:checkableBehavior="single">
<item
android:id="@+id/nav_settings"
android:title="@string/navigation_settings_header" />
<item
android:id="@+id/nav_alerts"
android:title="@string/navigation_indented_alerts" />
android:title="@string/navigation_alerts_header" />
<item
android:id="@+id/nav_alert_status"
android:title="@string/navigation_indented_alert_status" />
@@ -24,6 +21,11 @@
<item
android:id="@+id/nav_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
android:id="@+id/nav_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="navigation_open">Open 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_settings">Settings</string>
<string name="page_alerts">Alerts</string>
@@ -16,7 +17,6 @@
<string name="page_log_display">Log display</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_alerts">&#160;&#160;&#160;&#160;Alerts</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_rules">&#160;&#160;&#160;&#160;Rules</string>