Protect locked previews and filter logs by app

This commit is contained in:
ajp_anton
2026-08-08 04:33:16 +00:00
parent 05d682601a
commit 5b8de63aac
6 changed files with 180 additions and 23 deletions
@@ -26,6 +26,7 @@ import se.ajpanton.notificationlog.model.NotificationLogEntry
import se.ajpanton.notificationlog.settings.LogField
import se.ajpanton.notificationlog.settings.LogViewSettings
import se.ajpanton.notificationlog.settings.LogViewSettingsStore
import se.ajpanton.notificationlog.settings.LogAppFilterStore
import se.ajpanton.notificationlog.settings.TimestampFormatter
import se.ajpanton.notificationlog.settings.DisplayEvent
import se.ajpanton.notificationlog.capture.NotificationCaptureService
@@ -49,10 +50,12 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) {
private var noMoreRows = false
private var loadGeneration = 0
private val logAdapter = LogAdapter()
private lateinit var appFilterStore: LogAppFilterStore
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentViewLogsBinding.bind(view)
appFilterStore = LogAppFilterStore(requireContext())
binding!!.logsRefresh.setOnRefreshListener { loadLogs(reset = true) }
binding!!.logList.layoutManager = LinearLayoutManager(requireContext())
binding!!.logList.adapter = logAdapter
@@ -82,7 +85,7 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) {
}
private fun loadLogs(reset: Boolean) {
if (loading) return
if (loading && !reset) return
if (!reset && noMoreRows) return
val context = requireContext().applicationContext
if (reset) {
@@ -98,30 +101,83 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) {
val page = withContext(Dispatchers.IO) {
EncryptedNotificationLogStore(context).readNewest(nextCursor, PAGE_SIZE)
}
if (generation == loadGeneration) {
nextCursor = page.nextCursor
noMoreRows = nextCursor == null
val addedRows = page.entries.map(::LogRow)
rows += addedRows
binding?.let(::render)
}
loading = false
binding?.logsRefresh?.isRefreshing = false
if (generation != loadGeneration) return@launch
nextCursor = page.nextCursor
noMoreRows = nextCursor == null
val visibleBefore = visibleRows(LogViewSettingsStore(requireContext()).load()).size
rows += page.entries.map(::LogRow)
binding?.let(::render)
val visibleAfter = visibleRows(LogViewSettingsStore(requireContext()).load()).size
val skipFilteredPage = appFilterStore.selectedPackages().isNotEmpty() &&
visibleAfter == visibleBefore && !noMoreRows
loading = false
binding?.logsRefresh?.isRefreshing = false
if (skipFilteredPage) loadLogs(reset = false)
}
}
private fun render(view: FragmentViewLogsBinding) {
view.emptyView.visibility = if (rows.isEmpty()) View.VISIBLE else View.GONE
val settings = LogViewSettingsStore(requireContext()).load()
val visibleRows = visibleRows(settings)
view.emptyView.visibility = if (visibleRows.isEmpty()) View.VISIBLE else View.GONE
view.emptyView.text = if (hasNotificationAccess()) {
"No logs yet."
if (appFilterStore.selectedPackages().isEmpty()) "No logs yet." else "No logs match the app filter."
} else {
"Notification access is disabled. Enable it in Settings."
}
val settings = LogViewSettingsStore(requireContext()).load()
val visibleRows = rows.filter { eventFor(it.entry.action) in settings.visibleEvents }
logAdapter.submit(visibleRows, settings, metadataColumnWidth(view.logList.width, visibleRows, settings))
}
/** Called by MainActivity's header action. Selections persist until Reset is chosen. */
fun showAppFilter() {
val appContext = requireContext().applicationContext
viewLifecycleOwner.lifecycleScope.launch {
val apps = withContext(Dispatchers.IO) {
val byPackage = linkedMapOf<String, LoggedApp>()
EncryptedNotificationLogStore(appContext).forEachNewest { entry ->
byPackage.putIfAbsent(entry.packageName, LoggedApp(entry.appName, entry.packageName))
}
byPackage.values.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
}
if (!isAdded) return@launch
val selected = appFilterStore.selectedPackages().toMutableSet()
var changed = false
val dialog = MaterialAlertDialogBuilder(requireContext())
.setTitle("Filter logs by app")
.setMultiChoiceItems(
apps.map { "${it.name}\n${it.packageName}" }.toTypedArray(),
apps.map { it.packageName in selected }.toBooleanArray(),
) { _, which, checked ->
if (checked) selected += apps[which].packageName else selected -= apps[which].packageName
appFilterStore.save(selected)
changed = true
}
.setNeutralButton("Reset") { _, _ ->
appFilterStore.clear()
changed = true
}
.setPositiveButton("Close", null)
.create()
dialog.setOnDismissListener {
if (changed) refreshAppFilter()
}
dialog.show()
}
}
private fun refreshAppFilter() {
(activity as? MainActivity)?.updateLogFilterAction()
loadLogs(reset = true)
}
private fun visibleRows(settings: LogViewSettings): List<LogRow> {
val selectedPackages = appFilterStore.selectedPackages()
return rows.filter { row ->
eventFor(row.entry.action) in settings.visibleEvents &&
(selectedPackages.isEmpty() || row.entry.packageName in selectedPackages)
}
}
private fun logRowView(row: LogRow, settings: LogViewSettings, metadataColumnWidth: Int): View = LinearLayout(requireContext()).apply {
orientation = LinearLayout.HORIZONTAL
gravity = android.view.Gravity.TOP
@@ -463,6 +519,7 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) {
)
private data class LogRow(val entry: NotificationLogEntry)
private data class LoggedApp(val name: String, val packageName: String)
private data class MetadataValue(
val text: String,
val textSize: Float,