Page encrypted logs with cursors

This commit is contained in:
ajp_anton
2026-07-27 22:55:58 +00:00
parent 6467d1ab6c
commit 66ab982a7e
2 changed files with 40 additions and 18 deletions
@@ -19,6 +19,7 @@ import android.widget.TextView
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import se.ajpanton.notificationlog.data.EncryptedImageStore import se.ajpanton.notificationlog.data.EncryptedImageStore
import se.ajpanton.notificationlog.data.EncryptedNotificationLogStore import se.ajpanton.notificationlog.data.EncryptedNotificationLogStore
import se.ajpanton.notificationlog.data.NewestLogCursor
import se.ajpanton.notificationlog.databinding.FragmentViewLogsBinding import se.ajpanton.notificationlog.databinding.FragmentViewLogsBinding
import se.ajpanton.notificationlog.model.NotificationAction import se.ajpanton.notificationlog.model.NotificationAction
import se.ajpanton.notificationlog.model.NotificationLogEntry import se.ajpanton.notificationlog.model.NotificationLogEntry
@@ -37,7 +38,7 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) {
private var binding: FragmentViewLogsBinding? = null private var binding: FragmentViewLogsBinding? = null
private val expandedIds = mutableSetOf<String>() private val expandedIds = mutableSetOf<String>()
private val rows = mutableListOf<LogRow>() private val rows = mutableListOf<LogRow>()
private var loadedEntries = 0 private var nextCursor: NewestLogCursor? = null
private var loading = false private var loading = false
private var noMoreRows = false private var noMoreRows = false
private var loadGeneration = 0 private var loadGeneration = 0
@@ -75,7 +76,7 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) {
val context = requireContext().applicationContext val context = requireContext().applicationContext
if (reset) { if (reset) {
rows.clear() rows.clear()
loadedEntries = 0 nextCursor = null
noMoreRows = false noMoreRows = false
loadGeneration++ loadGeneration++
} }
@@ -83,12 +84,12 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) {
loading = true loading = true
binding?.logsRefresh?.isRefreshing = true binding?.logsRefresh?.isRefreshing = true
Thread { Thread {
val entries = EncryptedNotificationLogStore(context).readNewest(loadedEntries, PAGE_SIZE) val page = EncryptedNotificationLogStore(context).readNewest(nextCursor, PAGE_SIZE)
activity?.runOnUiThread { activity?.runOnUiThread {
if (generation == loadGeneration) { if (generation == loadGeneration) {
loadedEntries += entries.size nextCursor = page.nextCursor
noMoreRows = entries.size < PAGE_SIZE noMoreRows = nextCursor == null
val addedRows = entries.map(::LogRow) val addedRows = page.entries.map(::LogRow)
rows += addedRows rows += addedRows
binding?.let(::render) binding?.let(::render)
} }
@@ -11,6 +11,16 @@ import java.io.DataOutputStream
import java.io.File import java.io.File
import java.io.FileNotFoundException import java.io.FileNotFoundException
data class NewestLogCursor(
val chunkName: String,
val entryIndex: Int,
)
data class NotificationLogPage(
val entries: List<NotificationLogEntry>,
val nextCursor: NewestLogCursor?,
)
/** /**
* Independently encrypted log chunks. Reading or appending a record needs only * Independently encrypted log chunks. Reading or appending a record needs only
* one bounded chunk; older chunks stay on disk until requested by the viewer. * one bounded chunk; older chunks stay on disk until requested by the viewer.
@@ -30,22 +40,33 @@ class EncryptedNotificationLogStore(context: Context) {
chunkFiles().flatMap(::readChunk) chunkFiles().flatMap(::readChunk)
} }
fun readNewest(offset: Int, limit: Int): List<NotificationLogEntry> = synchronized(lock) { fun readNewest(cursor: NewestLogCursor?, limit: Int): NotificationLogPage = synchronized(lock) {
require(offset >= 0)
require(limit > 0) require(limit > 0)
val result = ArrayList<NotificationLogEntry>(limit) val result = ArrayList<NotificationLogEntry>(limit)
var skipped = 0 val chunks = chunkFiles().asReversed()
for (chunk in chunkFiles().asReversed()) { var chunkIndex = cursor?.let { saved -> chunks.indexOfFirst { it.name == saved.chunkName } } ?: 0
readChunk(chunk).asReversed().forEach { entry -> var entryIndex = cursor?.entryIndex ?: Int.MAX_VALUE
if (skipped < offset) { if (chunkIndex < 0) return@synchronized NotificationLogPage(emptyList(), null)
skipped++
} else if (result.size < limit) { while (chunkIndex < chunks.size) {
result += entry val chunk = chunks[chunkIndex]
} val entries = readChunk(chunk)
var index = minOf(entryIndex, entries.lastIndex)
while (index >= 0 && result.size < limit) {
result += entries[index--]
} }
if (result.size == limit) break if (result.size == limit) {
val next = if (index >= 0) {
NewestLogCursor(chunk.name, index)
} else {
chunks.getOrNull(chunkIndex + 1)?.let { NewestLogCursor(it.name, Int.MAX_VALUE) }
}
return@synchronized NotificationLogPage(result, next)
}
chunkIndex++
entryIndex = Int.MAX_VALUE
} }
result.sortedByDescending { it.recordedAtEpochMillis } NotificationLogPage(result, null)
} }
/** /**