Complete settings controls and refresh behavior

This commit is contained in:
ajp_anton
2026-07-23 10:48:46 +00:00
parent a65fa80903
commit 6eb9058595
6 changed files with 255 additions and 17 deletions
@@ -2,10 +2,12 @@ package se.ajpanton.notificationlog
import android.os.Bundle
import android.content.ClipData
import android.net.Uri
import android.view.View
import android.widget.ArrayAdapter
import androidx.fragment.app.Fragment
import com.google.android.material.switchmaterial.SwitchMaterial
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import se.ajpanton.notificationlog.databinding.FragmentSettingsBinding
import se.ajpanton.notificationlog.settings.LogField
import se.ajpanton.notificationlog.settings.LogViewSettingsStore
@@ -13,11 +15,17 @@ import se.ajpanton.notificationlog.settings.TimestampZone
import se.ajpanton.notificationlog.settings.LoggingType
import se.ajpanton.notificationlog.settings.LoggingRuleStore
import se.ajpanton.notificationlog.settings.AppLockStore
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import se.ajpanton.notificationlog.data.EncryptedNotificationLogStore
import se.ajpanton.notificationlog.export.LogExporter
import java.util.zip.ZipOutputStream
class SettingsFragment : Fragment(R.layout.fragment_settings) {
private var binding: FragmentSettingsBinding? = null
private var selectedCopyTargets = emptySet<LoggingType>()
private var pendingExport: ExportFormat? = null
private val createDocument = registerForActivityResult(androidx.activity.result.contract.ActivityResultContracts.CreateDocument("application/octet-stream")) { uri ->
uri?.let(::writeExport)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentSettingsBinding.bind(view)
@@ -64,19 +72,57 @@ class SettingsFragment : Fragment(R.layout.fragment_settings) {
}
}
setupCopyControls()
setupEventMasterToggles()
binding!!.exportLogs.setOnClickListener { chooseExportFormat() }
binding!!.clearLogs.setOnClickListener { confirmClearLogs() }
val lockStore = AppLockStore(requireContext())
binding!!.appLock.isChecked = lockStore.enabled
binding!!.appLock.setOnCheckedChangeListener { _, enabled -> lockStore.enabled = enabled }
}
override fun onDestroyView() { binding = null; super.onDestroyView() }
override fun onResume() {
super.onResume()
refreshEventMasterToggles()
}
private fun setupEventMasterToggles() {
val container = binding!!.eventMasterToggles
LoggingType.entries.forEach { type ->
container.addView(SwitchMaterial(requireContext()).apply {
text = "${type.label()} logging"
tag = type
setOnCheckedChangeListener { _, enabled ->
LoggingRuleStore(requireContext()).save(type, LoggingRuleStore(requireContext()).ruleFor(type).copy(enabled = enabled))
}
})
}
refreshEventMasterToggles()
}
private fun refreshEventMasterToggles() {
val store = LoggingRuleStore(requireContext())
val container = binding?.eventMasterToggles ?: return
for (index in 0 until container.childCount) {
val toggle = container.getChildAt(index) as SwitchMaterial
toggle.setOnCheckedChangeListener(null)
toggle.isChecked = store.ruleFor(toggle.tag as LoggingType).enabled
toggle.setOnCheckedChangeListener { _, enabled ->
val type = toggle.tag as LoggingType
store.save(type, store.ruleFor(type).copy(enabled = enabled))
}
}
}
private fun setupCopyControls() {
val labels = listOf("From") + LoggingType.entries.map { it.label() }
binding!!.copyFrom.adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_dropdown_item, labels)
binding!!.copyFrom.onItemSelectedListener = object : android.widget.AdapterView.OnItemSelectedListener {
override fun onNothingSelected(parent: android.widget.AdapterView<*>?) = Unit
override fun onItemSelected(parent: android.widget.AdapterView<*>?, view: View?, position: Int, id: Long) {
selectedCopyTargets = emptySet(); binding!!.copyTo.isEnabled = position > 0; updateCopyButton()
selectedCopyTargets = emptySet()
binding!!.copyTo.isEnabled = position > 0
updateCopyButton()
}
}
binding!!.copyTo.setOnClickListener { chooseCopyTargets(LoggingType.entries[binding!!.copyFrom.selectedItemPosition - 1]) }
@@ -87,17 +133,70 @@ class SettingsFragment : Fragment(R.layout.fragment_settings) {
private fun chooseCopyTargets(source: LoggingType) {
val targets = LoggingType.entries.filterNot { it == source }
val checked = BooleanArray(targets.size)
MaterialAlertDialogBuilder(requireContext())
val checked = BooleanArray(targets.size) { targets[it] in selectedCopyTargets }
val dialog = MaterialAlertDialogBuilder(requireContext())
.setTitle("Copy ${source.label()} settings to")
.setMultiChoiceItems(targets.map { it.label() }.toTypedArray(), checked) { _, index, selected -> checked[index] = selected }
.setNegativeButton("Cancel", null)
.setPositiveButton("Copy") { _, _ ->
selectedCopyTargets = targets.filterIndexed { index, _ -> checked[index] }.toSet(); updateCopyButton()
}.show()
.setNeutralButton("All", null)
.setPositiveButton("Done") { _, _ ->
selectedCopyTargets = targets.filterIndexed { index, _ -> checked[index] }.toSet()
updateCopyButton()
}
.create()
dialog.setOnShowListener {
dialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_NEUTRAL).setOnClickListener {
val selectAll = checked.any { !it }
checked.indices.forEach { index ->
checked[index] = selectAll
dialog.listView.setItemChecked(index, selectAll)
}
}
}
dialog.show()
}
private fun updateCopyButton() { binding?.copyEventSettings?.isEnabled = selectedCopyTargets.isNotEmpty() }
private fun confirmClearLogs() {
MaterialAlertDialogBuilder(requireContext())
.setTitle("Clear logs?")
.setMessage("This permanently removes every stored notification log and copied image.")
.setNegativeButton("Cancel", null)
.setPositiveButton("Clear") { _, _ -> EncryptedNotificationLogStore(requireContext()).clear() }
.show()
}
private fun chooseExportFormat() {
MaterialAlertDialogBuilder(requireContext())
.setItems(arrayOf("CSV", "Formatted text", "HTML ZIP")) { _, which ->
pendingExport = ExportFormat.entries[which]
createDocument.launch("notification-log.${pendingExport!!.extension}")
}
.show()
}
private fun writeExport(uri: Uri) {
val format = pendingExport ?: return
val context = requireContext().applicationContext
Thread {
val entries = EncryptedNotificationLogStore(context).readAll()
val settings = LogViewSettingsStore(context).load()
context.contentResolver.openOutputStream(uri)?.use { output ->
when (format) {
ExportFormat.CSV -> output.write(LogExporter.csv(entries, settings).encodeToByteArray())
ExportFormat.FORMATTED -> output.write(LogExporter.formatted(entries, settings).encodeToByteArray())
ExportFormat.HTML_ZIP -> ZipOutputStream(output).use { zip ->
zip.putNextEntry(java.util.zip.ZipEntry("notification-log.html"))
zip.write(LogExporter.html(entries, settings).encodeToByteArray())
zip.closeEntry()
}
}
}
}.start()
}
private fun LoggingType.label() = name.lowercase().replace('_', ' ').replaceFirstChar(Char::uppercase)
private enum class ExportFormat(val extension: String) { CSV("csv"), FORMATTED("txt"), HTML_ZIP("zip") }
}