diff --git a/app/src/main/java/se/ajpanton/notificationsmaster/MainActivity.kt b/app/src/main/java/se/ajpanton/notificationsmaster/MainActivity.kt index aa0fb74..49cda3c 100644 --- a/app/src/main/java/se/ajpanton/notificationsmaster/MainActivity.kt +++ b/app/src/main/java/se/ajpanton/notificationsmaster/MainActivity.kt @@ -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), diff --git a/app/src/main/java/se/ajpanton/notificationsmaster/ProfilesFragment.kt b/app/src/main/java/se/ajpanton/notificationsmaster/ProfilesFragment.kt new file mode 100644 index 0000000..e603477 --- /dev/null +++ b/app/src/main/java/se/ajpanton/notificationsmaster/ProfilesFragment.kt @@ -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? = 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() +} diff --git a/app/src/main/res/layout/fragment_profiles.xml b/app/src/main/res/layout/fragment_profiles.xml new file mode 100644 index 0000000..209f716 --- /dev/null +++ b/app/src/main/res/layout/fragment_profiles.xml @@ -0,0 +1,25 @@ + + + + + +