Add custom alert profiles

This commit is contained in:
ajp_anton
2026-08-18 01:37:28 +00:00
parent 1b21ff6751
commit 40a9a1b042
5 changed files with 170 additions and 0 deletions
@@ -155,6 +155,7 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
page == Page.VIEW_LOGS -> ViewLogsFragment()
page == Page.SETTINGS -> SettingsFragment()
page == Page.ALERTS -> AlertsFragment()
page == Page.PROFILES -> ProfilesFragment()
page == Page.LOG_DISPLAY -> LogDisplayFragment()
page == Page.FILTER_LOGGING -> FilterLoggingFragment()
page == Page.FILTER_APPS -> FilterAppsFragment.newInstance()
@@ -347,6 +348,7 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
VIEW_LOGS(R.id.nav_view_logs, R.string.page_view_logs),
SETTINGS(R.id.nav_settings, R.string.page_settings),
ALERTS(R.id.nav_alerts, R.string.page_alerts),
PROFILES(R.id.nav_profiles, R.string.page_profiles),
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),
@@ -0,0 +1,138 @@
package se.ajpanton.notificationsmaster
import android.content.Intent
import android.media.RingtoneManager
import android.net.Uri
import android.os.Bundle
import android.view.View
import android.widget.Button
import android.widget.EditText
import android.widget.LinearLayout
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.Fragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import se.ajpanton.notificationsmaster.alerts.AlertConfigurationStore
import se.ajpanton.notificationsmaster.alerts.AlertProfile
import se.ajpanton.notificationsmaster.databinding.FragmentProfilesBinding
import java.util.UUID
class ProfilesFragment : Fragment(R.layout.fragment_profiles) {
private var binding: FragmentProfilesBinding? = null
private lateinit var store: AlertConfigurationStore
private var selectedSoundUri: String? = null
private var soundButton: Button? = 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"
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding = FragmentProfilesBinding.bind(view)
store = AlertConfigurationStore(requireContext())
binding!!.addProfile.setOnClickListener { showEditor() }
refresh()
}
override fun onDestroyView() {
binding = null
soundButton = null
super.onDestroyView()
}
private fun refresh() {
val container = binding!!.profiles
container.removeAllViews()
store.load().profiles.forEach { profile ->
container.addView(Button(requireContext()).apply {
text = profile.name + "\n" + profileSummary(profile)
isAllCaps = false
setOnClickListener { showEditor(profile) }
})
}
}
private fun showEditor(existing: AlertProfile? = null) {
selectedSoundUri = existing?.soundUri
val context = requireContext()
val form = LinearLayout(context).apply {
orientation = LinearLayout.VERTICAL
setPadding(dp(24), 0, dp(24), 0)
}
val name = EditText(context).apply {
hint = "Profile name"
setText(existing?.name)
}
val vibration = EditText(context).apply {
hint = "Vibration timing in milliseconds, for example 0, 200, 100, 200"
setText(existing?.vibrationPattern?.joinToString(", ") ?: "0, 200")
inputType = android.text.InputType.TYPE_CLASS_TEXT
}
val pickerButton = Button(context).apply {
text = if (selectedSoundUri == null) "Choose sound" else "Change sound"
setOnClickListener {
chooseSound.launch(
Intent(RingtoneManager.ACTION_RINGTONE_PICKER)
.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_NOTIFICATION)
.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, selectedSoundUri?.let(Uri::parse)),
)
}
}
soundButton = pickerButton
form.addView(name)
form.addView(pickerButton)
form.addView(vibration)
val dialog = MaterialAlertDialogBuilder(context)
.setTitle(if (existing == null) "Add profile" else "Edit profile")
.setView(form)
.setNegativeButton("Cancel", null)
.setPositiveButton("Save", null)
.apply {
if (existing != null) setNeutralButton("Delete", null)
}
.show()
dialog.getButton(android.content.DialogInterface.BUTTON_POSITIVE).setOnClickListener {
val pattern = parsePattern(vibration.text.toString()) ?: run {
vibration.error = "Use non-negative comma-separated milliseconds"
return@setOnClickListener
}
val profileName = name.text.toString().trim()
if (profileName.isEmpty() || selectedSoundUri == null && pattern.none { it > 0 }) {
name.error = if (profileName.isEmpty()) "Enter a name" else null
vibration.error = if (selectedSoundUri == null && pattern.none { it > 0 }) "Choose a sound or vibration" else null
return@setOnClickListener
}
val profile = AlertProfile(existing?.id ?: UUID.randomUUID().toString(), profileName, selectedSoundUri, pattern)
val configuration = store.load()
store.save(configuration.copy(profiles = configuration.profiles.filterNot { it.id == profile.id } + profile))
dialog.dismiss()
refresh()
}
if (existing != null) {
dialog.getButton(android.content.DialogInterface.BUTTON_NEUTRAL).setOnClickListener {
val configuration = store.load()
if (configuration.rules.any { it.profileId == existing.id }) {
Toast.makeText(context, "Remove or change rules using this profile first", Toast.LENGTH_LONG).show()
return@setOnClickListener
}
store.save(configuration.copy(profiles = configuration.profiles.filterNot { it.id == existing.id }))
dialog.dismiss()
refresh()
}
}
}
private fun parsePattern(text: String): List<Long>? = text.split(',')
.map { it.trim() }
.filter { it.isNotEmpty() }
.map { it.toLongOrNull() ?: return null }
.takeIf { it.all { value -> value >= 0 } }
?: emptyList()
private fun profileSummary(profile: AlertProfile): String = buildList {
if (profile.soundUri != null) add("Sound")
if (profile.vibrationPattern.any { it > 0 }) add("Vibration")
}.joinToString(" + ")
private fun dp(value: Int) = (value * resources.displayMetrics.density).toInt()
}
@@ -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">
<Button
android:id="@+id/add_profile"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Add profile" />
<LinearLayout
android:id="@+id/profiles"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:orientation="vertical" />
</LinearLayout>
</ScrollView>
+3
View File
@@ -12,6 +12,9 @@
<item
android:id="@+id/nav_alerts"
android:title="@string/navigation_indented_alerts" />
<item
android:id="@+id/nav_profiles"
android:title="@string/navigation_indented_profiles" />
<item
android:id="@+id/nav_log_display"
android:title="@string/navigation_indented_log_display" />
+2
View File
@@ -8,11 +8,13 @@
<string name="page_view_logs">View logs</string>
<string name="page_settings">Settings</string>
<string name="page_alerts">Alerts</string>
<string name="page_profiles">Profiles</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>
<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_profiles">&#160;&#160;&#160;&#160;Profiles</string>
<string name="navigation_indented_filter_logging">&#160;&#160;&#160;&#160;Filter logging</string>
<string name="navigation_indented_filter_apps">&#160;&#160;&#160;&#160;Filter apps</string>
</resources>