diff --git a/app/src/main/java/se/ajpanton/notificationlog/ViewLogsFragment.kt b/app/src/main/java/se/ajpanton/notificationlog/ViewLogsFragment.kt index b3b0b74..4b37d75 100644 --- a/app/src/main/java/se/ajpanton/notificationlog/ViewLogsFragment.kt +++ b/app/src/main/java/se/ajpanton/notificationlog/ViewLogsFragment.kt @@ -19,6 +19,7 @@ import android.widget.TextView import com.google.android.material.dialog.MaterialAlertDialogBuilder import se.ajpanton.notificationlog.data.EncryptedImageStore import se.ajpanton.notificationlog.data.EncryptedNotificationLogStore +import se.ajpanton.notificationlog.data.NewestLogCursor import se.ajpanton.notificationlog.databinding.FragmentViewLogsBinding import se.ajpanton.notificationlog.model.NotificationAction import se.ajpanton.notificationlog.model.NotificationLogEntry @@ -37,7 +38,7 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) { private var binding: FragmentViewLogsBinding? = null private val expandedIds = mutableSetOf() private val rows = mutableListOf() - private var loadedEntries = 0 + private var nextCursor: NewestLogCursor? = null private var loading = false private var noMoreRows = false private var loadGeneration = 0 @@ -75,7 +76,7 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) { val context = requireContext().applicationContext if (reset) { rows.clear() - loadedEntries = 0 + nextCursor = null noMoreRows = false loadGeneration++ } @@ -83,12 +84,12 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) { loading = true binding?.logsRefresh?.isRefreshing = true Thread { - val entries = EncryptedNotificationLogStore(context).readNewest(loadedEntries, PAGE_SIZE) + val page = EncryptedNotificationLogStore(context).readNewest(nextCursor, PAGE_SIZE) activity?.runOnUiThread { if (generation == loadGeneration) { - loadedEntries += entries.size - noMoreRows = entries.size < PAGE_SIZE - val addedRows = entries.map(::LogRow) + nextCursor = page.nextCursor + noMoreRows = nextCursor == null + val addedRows = page.entries.map(::LogRow) rows += addedRows binding?.let(::render) } diff --git a/app/src/main/java/se/ajpanton/notificationlog/data/EncryptedNotificationLogStore.kt b/app/src/main/java/se/ajpanton/notificationlog/data/EncryptedNotificationLogStore.kt index 948fe07..ac5f7ff 100644 --- a/app/src/main/java/se/ajpanton/notificationlog/data/EncryptedNotificationLogStore.kt +++ b/app/src/main/java/se/ajpanton/notificationlog/data/EncryptedNotificationLogStore.kt @@ -11,6 +11,16 @@ import java.io.DataOutputStream import java.io.File import java.io.FileNotFoundException +data class NewestLogCursor( + val chunkName: String, + val entryIndex: Int, +) + +data class NotificationLogPage( + val entries: List, + val nextCursor: NewestLogCursor?, +) + /** * Independently encrypted log chunks. Reading or appending a record needs only * one bounded chunk; older chunks stay on disk until requested by the viewer. @@ -30,22 +40,33 @@ class EncryptedNotificationLogStore(context: Context) { chunkFiles().flatMap(::readChunk) } - fun readNewest(offset: Int, limit: Int): List = synchronized(lock) { - require(offset >= 0) + fun readNewest(cursor: NewestLogCursor?, limit: Int): NotificationLogPage = synchronized(lock) { require(limit > 0) val result = ArrayList(limit) - var skipped = 0 - for (chunk in chunkFiles().asReversed()) { - readChunk(chunk).asReversed().forEach { entry -> - if (skipped < offset) { - skipped++ - } else if (result.size < limit) { - result += entry - } + val chunks = chunkFiles().asReversed() + var chunkIndex = cursor?.let { saved -> chunks.indexOfFirst { it.name == saved.chunkName } } ?: 0 + var entryIndex = cursor?.entryIndex ?: Int.MAX_VALUE + if (chunkIndex < 0) return@synchronized NotificationLogPage(emptyList(), null) + + while (chunkIndex < chunks.size) { + 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) } /**