Add per-app event logging overrides

This commit is contained in:
ajp_anton
2026-07-27 13:45:32 +00:00
parent 55057767c1
commit 1e4315d3dd
10 changed files with 229 additions and 24 deletions
@@ -1,6 +1,7 @@
package se.ajpanton.notificationlog
import android.os.Bundle
import android.content.res.ColorStateList
import android.text.SpannableString
import android.text.Spanned
import android.text.style.StyleSpan
@@ -9,12 +10,16 @@ import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.CheckBox
import android.widget.ImageButton
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.switchmaterial.SwitchMaterial
import se.ajpanton.notificationlog.capture.SeenApps
import se.ajpanton.notificationlog.databinding.FragmentEventSettingsBinding
import se.ajpanton.notificationlog.settings.AppListItem
@@ -23,10 +28,14 @@ import se.ajpanton.notificationlog.settings.AppRuleMode
import se.ajpanton.notificationlog.settings.ListedApp
import se.ajpanton.notificationlog.settings.AppFilterSettings
import se.ajpanton.notificationlog.settings.AppFilterSettingsStore
import se.ajpanton.notificationlog.settings.LoggingRuleStore
import se.ajpanton.notificationlog.settings.LoggingType
import se.ajpanton.notificationlog.settings.PerAppEventSettingsStore
class FilterAppsFragment : Fragment(R.layout.fragment_event_settings) {
private var binding: FragmentEventSettingsBinding? = null
private lateinit var store: AppFilterSettingsStore
private lateinit var perAppEventSettings: PerAppEventSettingsStore
private lateinit var appAdapter: AppListAdapter
private var appLoadGeneration = 0
@@ -34,7 +43,8 @@ class FilterAppsFragment : Fragment(R.layout.fragment_event_settings) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentEventSettingsBinding.bind(view)
store = AppFilterSettingsStore(requireContext())
appAdapter = AppListAdapter(::setPackageSelected)
perAppEventSettings = PerAppEventSettingsStore(requireContext())
appAdapter = AppListAdapter(::setPackageSelected, ::showPerAppEventSettings)
binding!!.appList.layoutManager = LinearLayoutManager(requireContext())
binding!!.appList.adapter = appAdapter
binding!!.appListRefresh.setOnRefreshListener(::reloadAppList)
@@ -118,9 +128,10 @@ class FilterAppsFragment : Fragment(R.layout.fragment_event_settings) {
packageName = info.packageName,
seen = info.packageName in seen,
selected = info.packageName in rule.selectedPackages,
hasEventOverride = perAppEventSettings.hasOverride(info.packageName),
)
}
val items = AppListOrdering.items(apps, rule.onlySeenApps, rule.seenAppsFirst, rule.mode)
val items = AppListOrdering.items(apps, rule.onlySeenApps, rule.seenAppsFirst)
activity?.runOnUiThread {
if (binding === currentBinding && generation == appLoadGeneration) {
appAdapter.submit(items)
@@ -140,8 +151,69 @@ class FilterAppsFragment : Fragment(R.layout.fragment_event_settings) {
// Do not reload: an unchecked unseen row remains visible until the next requested refresh.
}
private fun showPerAppEventSettings(app: ListedApp) {
val eventTypes = listOf(LoggingType.APPEARING, LoggingType.DISAPPEARING, LoggingType.EDITS)
val rules = LoggingRuleStore(requireContext())
val content = LinearLayout(requireContext()).apply {
orientation = LinearLayout.VERTICAL
setPadding(dp(24), 0, dp(24), 0)
}
val useGlobal = SwitchMaterial(requireContext()).apply { text = "Use global" }
val eventToggles = eventTypes.associateWith { type ->
SwitchMaterial(requireContext()).apply {
text = type.label()
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT,
).apply { marginStart = dp(24) }
}.also(content::addView)
}
content.addView(useGlobal, 0)
fun globalEvents() = eventTypes.associateWith { rules.ruleFor(it).enabled }
fun refresh() {
val useGlobalSetting = perAppEventSettings.usesGlobal(app.packageName)
useGlobal.setOnCheckedChangeListener(null)
useGlobal.isChecked = useGlobalSetting
eventToggles.forEach { (type, toggle) ->
toggle.setOnCheckedChangeListener(null)
toggle.isEnabled = !useGlobalSetting
toggle.isChecked = perAppEventSettings.isEnabled(
app.packageName,
type,
globalEvents().getValue(type),
)
toggle.setOnCheckedChangeListener { _, enabled ->
perAppEventSettings.setEnabled(app.packageName, type, enabled)
}
}
useGlobal.setOnCheckedChangeListener { _, enabled ->
if (enabled) perAppEventSettings.useGlobal(app.packageName)
else perAppEventSettings.startUsingOverride(app.packageName, globalEvents())
appAdapter.updateEventOverride(app.packageName, !enabled)
refresh()
}
}
refresh()
MaterialAlertDialogBuilder(requireContext())
.setTitle("When logging, only log")
.setView(content)
.setPositiveButton("Close", null)
.show()
}
private fun LoggingType.label() = when (this) {
LoggingType.APPEARING -> "Appearing"
LoggingType.DISAPPEARING -> "Disappearing"
LoggingType.EDITS -> "Edits"
else -> error("Only event types are shown in the per-app dialog")
}
private fun dp(value: Int): Int = (value * resources.displayMetrics.density).toInt()
private class AppListAdapter(
private val onSelectedChanged: (String, Boolean) -> Unit,
private val onEventFilterClicked: (ListedApp) -> Unit,
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private var items: List<AppListItem> = emptyList()
@@ -162,6 +234,12 @@ class FilterAppsFragment : Fragment(R.layout.fragment_event_settings) {
setPadding(0, dp(parent, 4), 0, dp(parent, 4))
}
val checkbox = CheckBox(parent.context)
val filter = ImageButton(parent.context).apply {
setImageResource(R.drawable.ic_filter_list)
contentDescription = "Edit logging events"
layoutParams = LinearLayout.LayoutParams(dp(parent, 48), dp(parent, 48))
setPadding(dp(parent, 12), dp(parent, 12), dp(parent, 12), dp(parent, 12))
}
val textColumn = LinearLayout(parent.context).apply {
orientation = LinearLayout.VERTICAL
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
@@ -171,8 +249,9 @@ class FilterAppsFragment : Fragment(R.layout.fragment_event_settings) {
textColumn.addView(label)
textColumn.addView(packageName)
row.addView(checkbox)
row.addView(filter)
row.addView(textColumn)
AppHolder(row, checkbox, label, packageName)
AppHolder(row, checkbox, filter, label, packageName)
} else if (viewType == SECTION_TITLE) {
SectionTitleHolder(TextView(parent.context).apply {
layoutParams = RecyclerView.LayoutParams(
@@ -208,6 +287,15 @@ class FilterAppsFragment : Fragment(R.layout.fragment_event_settings) {
} else current
}
}
holder.filter.contentDescription = "Edit logging events for ${app.label}"
holder.filter.imageTintList = ColorStateList.valueOf(
MaterialColors.getColor(
holder.filter,
if (app.hasEventOverride) com.google.android.material.R.attr.colorPrimary
else com.google.android.material.R.attr.colorOnSurface,
),
)
holder.filter.setOnClickListener { onEventFilterClicked(app) }
}
if (holder is SectionTitleHolder && item is AppListItem.SectionTitle) {
holder.text.text = item.value
@@ -216,9 +304,21 @@ class FilterAppsFragment : Fragment(R.layout.fragment_event_settings) {
override fun getItemCount(): Int = items.size
fun updateEventOverride(packageName: String, hasEventOverride: Boolean) {
items = items.map { item ->
if (item is AppListItem.App && item.value.packageName == packageName) {
AppListItem.App(item.value.copy(hasEventOverride = hasEventOverride))
} else {
item
}
}
notifyDataSetChanged()
}
private class AppHolder(
view: View,
val checkbox: CheckBox,
val filter: ImageButton,
val label: TextView,
val packageName: TextView,
) : RecyclerView.ViewHolder(view)