package se.ajpanton.notificationlog import android.os.Bundle import android.view.View import android.widget.CheckBox import android.widget.LinearLayout import android.widget.TextView import androidx.annotation.StringRes import androidx.fragment.app.Fragment import se.ajpanton.notificationlog.databinding.FragmentEventSettingsBinding import se.ajpanton.notificationlog.settings.AppRuleMode import se.ajpanton.notificationlog.settings.LoggingRuleStore import se.ajpanton.notificationlog.settings.LoggingType import se.ajpanton.notificationlog.capture.SeenApps class EventSettingsFragment : Fragment(R.layout.fragment_event_settings) { private var binding: FragmentEventSettingsBinding? = null private lateinit var type: LoggingType private lateinit var store: LoggingRuleStore override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding = FragmentEventSettingsBinding.bind(view) type = requireArguments().getSerializable(ARG_TYPE, LoggingType::class.java)!! store = LoggingRuleStore(requireContext()) binding!!.pageTitle.setText(requireArguments().getInt(ARG_TITLE)) bindRule() binding!!.appListRefresh.setOnRefreshListener { reloadAppList(); binding!!.appListRefresh.isRefreshing = false } } override fun onDestroyView() { binding = null; super.onDestroyView() } private fun bindRule() { val rule = store.ruleFor(type) listOf( binding!!.masterToggle, binding!!.appRuleToggle, binding!!.onlySeenToggle, binding!!.seenFirstToggle, binding!!.routineUpdatesToggle, ).forEach { it.setOnCheckedChangeListener(null) } binding!!.masterToggle.isChecked = rule.enabled binding!!.appRuleToggle.isChecked = rule.appRuleMode == AppRuleMode.WHITELIST binding!!.onlySeenToggle.isChecked = rule.onlySeenApps binding!!.seenFirstToggle.isChecked = rule.seenAppsFirst binding!!.seenFirstToggle.isEnabled = !rule.onlySeenApps binding!!.routineUpdatesToggle.visibility = if (type == LoggingType.EDITS) View.VISIBLE else View.GONE binding!!.routineUpdatesToggle.isChecked = rule.ignoreRoutineUpdates updateRuleLabels() binding!!.masterToggle.setOnCheckedChangeListener { _, checked -> save { copy(enabled = checked) } } binding!!.appRuleToggle.setOnCheckedChangeListener { _, checked -> save { copy(appRuleMode = if (checked) AppRuleMode.WHITELIST else AppRuleMode.BLACKLIST) } } binding!!.onlySeenToggle.setOnCheckedChangeListener { _, checked -> save { copy(onlySeenApps = checked) } } binding!!.seenFirstToggle.setOnCheckedChangeListener { _, checked -> save { copy(seenAppsFirst = checked) } } binding!!.routineUpdatesToggle.setOnCheckedChangeListener { _, checked -> save { copy(ignoreRoutineUpdates = checked) } } reloadAppList() } private fun save(change: se.ajpanton.notificationlog.settings.LoggingRule.() -> se.ajpanton.notificationlog.settings.LoggingRule) { store.save(type, store.ruleFor(type).change()) bindRule() } private fun updateRuleLabels() { val rule = store.ruleFor(type) binding!!.appRuleToggle.text = if (rule.appRuleMode == AppRuleMode.WHITELIST) "Whitelist chosen" else "Blacklist chosen" binding!!.onlySeenToggle.text = if (rule.onlySeenApps) "Show only seen apps" else "Show all apps" } private fun reloadAppList() { val container = binding!!.appList container.removeAllViews() val rule = store.ruleFor(type) val seen = SeenApps.snapshot() val apps = requireContext().packageManager.getInstalledApplications(0) .map { info -> AppRow( label = requireContext().packageManager.getApplicationLabel(info).toString(), packageName = info.packageName, seen = info.packageName in seen, selected = info.packageName in rule.selectedPackages, ) } .sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.label }) val shown = if (rule.onlySeenApps) apps.filter { it.seen || it.selected } else apps val seenRows = shown.filter { it.seen } val otherRows = shown.filterNot { it.seen } if (!rule.onlySeenApps && rule.seenAppsFirst && seenRows.isNotEmpty()) { seenRows.forEach { container.addView(appView(it)) } if (otherRows.isNotEmpty()) container.addView(separator()) otherRows.forEach { container.addView(appView(it)) } } else if (rule.onlySeenApps && seenRows.isNotEmpty() && otherRows.isNotEmpty()) { seenRows.forEach { container.addView(appView(it)) } container.addView(separator()) otherRows.forEach { container.addView(appView(it)) } } else { shown.forEach { container.addView(appView(it)) } } } private fun appView(row: AppRow): View = LinearLayout(requireContext()).apply { orientation = LinearLayout.HORIZONTAL val checkbox = CheckBox(context).apply { isChecked = row.selected } checkbox.setOnCheckedChangeListener { _, checked -> val current = store.ruleFor(type) val packages = current.selectedPackages.toMutableSet().apply { if (checked) add(row.packageName) else remove(row.packageName) } store.save(type, current.copy(selectedPackages = packages)) // Deliberately do not reload: an unchecked unseen row must remain reachable. } addView(checkbox) addView(LinearLayout(context).apply { orientation = LinearLayout.VERTICAL layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f) addView(TextView(context).apply { text = row.label; textSize = 16f; setTypeface(typeface, 1) }) addView(TextView(context).apply { text = row.packageName; textSize = 12f }) }) } private fun separator() = View(requireContext()).apply { layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 1).apply { setMargins(0, 12, 0, 12) } setBackgroundColor(0x33000000) } private data class AppRow(val label: String, val packageName: String, val seen: Boolean, val selected: Boolean) companion object { private const val ARG_TITLE = "title" private const val ARG_TYPE = "type" fun newInstance(@StringRes title: Int, type: LoggingType) = EventSettingsFragment().apply { arguments = Bundle().apply { putInt(ARG_TITLE, title); putSerializable(ARG_TYPE, type) } } } }