Recycle paged log rows
This commit is contained in:
@@ -31,8 +31,14 @@ import se.ajpanton.notificationlog.settings.DisplayEvent
|
||||
import se.ajpanton.notificationlog.capture.NotificationCaptureService
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.children
|
||||
import androidx.core.view.doOnPreDraw
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) {
|
||||
private var binding: FragmentViewLogsBinding? = null
|
||||
@@ -42,20 +48,25 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) {
|
||||
private var loading = false
|
||||
private var noMoreRows = false
|
||||
private var loadGeneration = 0
|
||||
private val logAdapter = LogAdapter()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding = FragmentViewLogsBinding.bind(view)
|
||||
binding!!.logsRefresh.setOnRefreshListener { loadLogs(reset = true) }
|
||||
binding!!.logScroll.setOnScrollChangeListener { _, _, scrollY, _, _ ->
|
||||
binding!!.logList.layoutManager = LinearLayoutManager(requireContext())
|
||||
binding!!.logList.adapter = logAdapter
|
||||
binding!!.logList.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
val scrollY = recyclerView.computeVerticalScrollOffset()
|
||||
binding?.scrollToTop?.visibility = if (scrollY > dp(SCROLL_TO_TOP_THRESHOLD_DP)) View.VISIBLE else View.GONE
|
||||
val scroll = binding?.logScroll ?: return@setOnScrollChangeListener
|
||||
if (!noMoreRows && !loading && scrollY >= scroll.getChildAt(0).height - scroll.height - dp(LOAD_MORE_THRESHOLD_DP)) {
|
||||
if (!noMoreRows && !loading && !recyclerView.canScrollVertically(1)) {
|
||||
loadLogs(reset = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
binding!!.scrollToTop.setOnClickListener {
|
||||
binding?.logScroll?.smoothScrollTo(0, 0)
|
||||
binding?.logList?.smoothScrollToPosition(0)
|
||||
}
|
||||
loadLogs(reset = true)
|
||||
}
|
||||
@@ -83,9 +94,10 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) {
|
||||
val generation = loadGeneration
|
||||
loading = true
|
||||
binding?.logsRefresh?.isRefreshing = true
|
||||
Thread {
|
||||
val page = EncryptedNotificationLogStore(context).readNewest(nextCursor, PAGE_SIZE)
|
||||
activity?.runOnUiThread {
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
val page = withContext(Dispatchers.IO) {
|
||||
EncryptedNotificationLogStore(context).readNewest(nextCursor, PAGE_SIZE)
|
||||
}
|
||||
if (generation == loadGeneration) {
|
||||
nextCursor = page.nextCursor
|
||||
noMoreRows = nextCursor == null
|
||||
@@ -95,12 +107,10 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) {
|
||||
}
|
||||
loading = false
|
||||
binding?.logsRefresh?.isRefreshing = false
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
}
|
||||
|
||||
private fun render(view: FragmentViewLogsBinding) {
|
||||
view.logRows.removeAllViews()
|
||||
view.emptyView.visibility = if (rows.isEmpty()) View.VISIBLE else View.GONE
|
||||
view.emptyView.text = if (hasNotificationAccess()) {
|
||||
"No logs yet."
|
||||
@@ -109,11 +119,7 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) {
|
||||
}
|
||||
val settings = LogViewSettingsStore(requireContext()).load()
|
||||
val visibleRows = rows.filter { eventFor(it.entry.action) in settings.visibleEvents }
|
||||
val metadataColumnWidth = metadataColumnWidth(view.logRows.width, visibleRows, settings)
|
||||
visibleRows.forEachIndexed { index, row ->
|
||||
view.logRows.addView(logRowView(row, settings, metadataColumnWidth))
|
||||
if (index < visibleRows.lastIndex) view.logRows.addView(rowSeparator())
|
||||
}
|
||||
logAdapter.submit(visibleRows, settings, metadataColumnWidth(view.logList.width, visibleRows, settings))
|
||||
}
|
||||
|
||||
private fun logRowView(row: LogRow, settings: LogViewSettings, metadataColumnWidth: Int): View = LinearLayout(requireContext()).apply {
|
||||
@@ -228,12 +234,13 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) {
|
||||
val imageId = row.entry.imageId
|
||||
val appContext = requireContext().applicationContext
|
||||
val contentsContainer = this
|
||||
Thread {
|
||||
EncryptedImageStore(appContext).read(imageId)?.let { bytes ->
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
val bitmap = withContext(Dispatchers.IO) {
|
||||
EncryptedImageStore(appContext).read(imageId)?.let { bytes ->
|
||||
BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
|
||||
}?.let { bitmap ->
|
||||
activity?.runOnUiThread {
|
||||
if (contentsContainer.isAttachedToWindow) {
|
||||
}
|
||||
}
|
||||
if (contentsContainer.isAttachedToWindow && bitmap != null) {
|
||||
contentsContainer.addView(ImageView(contentsContainer.context).apply {
|
||||
setImageBitmap(bitmap)
|
||||
adjustViewBounds = true
|
||||
@@ -246,10 +253,8 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) {
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
}
|
||||
setOnClickListener { onToggleExpanded() }
|
||||
setOnLongClickListener { showDeleteDialog(row.entry.id) }
|
||||
@@ -327,6 +332,48 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) {
|
||||
}
|
||||
}
|
||||
|
||||
private inner class LogAdapter : RecyclerView.Adapter<LogAdapter.Holder>() {
|
||||
private var rows: List<LogRow> = emptyList()
|
||||
private var settings = LogViewSettings()
|
||||
private var metadataWidth = 0
|
||||
|
||||
fun submit(rows: List<LogRow>, settings: LogViewSettings, metadataWidth: Int) {
|
||||
val previous = this.rows
|
||||
this.rows = rows
|
||||
this.settings = settings
|
||||
this.metadataWidth = metadataWidth
|
||||
DiffUtil.calculateDiff(object : DiffUtil.Callback() {
|
||||
override fun getOldListSize() = previous.size
|
||||
override fun getNewListSize() = rows.size
|
||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
|
||||
previous[oldItemPosition].entry.id == rows[newItemPosition].entry.id
|
||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
|
||||
previous[oldItemPosition] == rows[newItemPosition]
|
||||
}).dispatchUpdatesTo(this)
|
||||
notifyItemRangeChanged(0, itemCount)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: android.view.ViewGroup, viewType: Int): Holder {
|
||||
val root = LinearLayout(parent.context).apply { orientation = LinearLayout.VERTICAL }
|
||||
val content = LinearLayout(parent.context)
|
||||
root.addView(content)
|
||||
root.addView(rowSeparator())
|
||||
return Holder(root, content)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: Holder, position: Int) {
|
||||
holder.content.removeAllViews()
|
||||
holder.content.addView(logRowView(rows[position], settings, metadataWidth))
|
||||
holder.separator.visibility = if (position == itemCount - 1) View.GONE else View.VISIBLE
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = rows.size
|
||||
|
||||
inner class Holder(val root: LinearLayout, val content: LinearLayout) : RecyclerView.ViewHolder(root) {
|
||||
val separator: View get() = root.getChildAt(1)
|
||||
}
|
||||
}
|
||||
|
||||
private fun timestamp(value: Long, settings: LogViewSettings, eventTimeZoneId: String?): String =
|
||||
TimestampFormatter.format(requireContext(), value, settings, eventTimeZoneId)
|
||||
|
||||
@@ -336,10 +383,12 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) {
|
||||
.setMessage("This permanently removes this log and its copied image, if any.")
|
||||
.setNegativeButton("Cancel", null)
|
||||
.setPositiveButton("Delete") { _, _ ->
|
||||
Thread {
|
||||
EncryptedNotificationLogStore(requireContext().applicationContext).delete(entryId)
|
||||
activity?.runOnUiThread { loadLogs(reset = true) }
|
||||
}.start()
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
EncryptedNotificationLogStore(requireContext().applicationContext).delete(entryId)
|
||||
}
|
||||
loadLogs(reset = true)
|
||||
}
|
||||
}
|
||||
.show()
|
||||
return true
|
||||
@@ -347,35 +396,37 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) {
|
||||
|
||||
private fun captureCollapseAnchor(collapsingRow: View): CollapseAnchor? {
|
||||
val currentBinding = binding ?: return null
|
||||
val scroll = currentBinding.logScroll
|
||||
val rowsTop = currentBinding.logRows.top
|
||||
val topEdge = scroll.scrollY - rowsTop
|
||||
val bottomEdge = topEdge + scroll.height
|
||||
val topRow = rowAt(currentBinding.logRows, topEdge)
|
||||
val bottomRow = rowAt(currentBinding.logRows, bottomEdge)
|
||||
val list = currentBinding.logList
|
||||
val manager = list.layoutManager as? LinearLayoutManager ?: return null
|
||||
val collapsingItem = list.findContainingItemView(collapsingRow) ?: return null
|
||||
val collapsingPosition = list.getChildAdapterPosition(collapsingItem)
|
||||
val topPosition = manager.findFirstVisibleItemPosition()
|
||||
val bottomPosition = manager.findLastVisibleItemPosition()
|
||||
val topRow = manager.findViewByPosition(topPosition)
|
||||
val bottomRow = manager.findViewByPosition(bottomPosition)
|
||||
return when {
|
||||
topRow === collapsingRow && bottomRow === collapsingRow -> CollapseAnchor.CENTER_COLLAPSED_ROW(collapsingRow)
|
||||
topRow === collapsingRow && bottomRow != null ->
|
||||
CollapseAnchor.KEEP_BOTTOM_EDGE(bottomRow, bottomRow.bottom - bottomEdge)
|
||||
bottomRow === collapsingRow && topRow != null ->
|
||||
CollapseAnchor.KEEP_TOP_EDGE(topRow, topEdge - topRow.top)
|
||||
topPosition == collapsingPosition && bottomPosition == collapsingPosition -> CollapseAnchor.CENTER_COLLAPSED_ROW(collapsingPosition)
|
||||
topPosition == collapsingPosition && bottomRow != null ->
|
||||
CollapseAnchor.KEEP_BOTTOM_EDGE(bottomPosition, bottomRow.bottom - list.height)
|
||||
bottomPosition == collapsingPosition && topRow != null ->
|
||||
CollapseAnchor.KEEP_TOP_EDGE(topPosition, topRow.top)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun restoreCollapseAnchor(anchor: CollapseAnchor, afterRestore: () -> Unit) {
|
||||
val currentBinding = binding ?: return
|
||||
val scroll = currentBinding.logScroll
|
||||
val rows = currentBinding.logRows
|
||||
rows.doOnPreDraw {
|
||||
val list = currentBinding.logList
|
||||
val manager = list.layoutManager as? LinearLayoutManager ?: return
|
||||
list.doOnPreDraw {
|
||||
if (binding !== currentBinding) return@doOnPreDraw
|
||||
val target = when (anchor) {
|
||||
is CollapseAnchor.CENTER_COLLAPSED_ROW -> rows.top + anchor.row.top + anchor.row.height / 2 - scroll.height / 2
|
||||
is CollapseAnchor.KEEP_TOP_EDGE -> rows.top + anchor.row.top + anchor.offset
|
||||
is CollapseAnchor.KEEP_BOTTOM_EDGE -> rows.top + anchor.row.bottom - anchor.offset - scroll.height
|
||||
val row = manager.findViewByPosition(anchor.position) ?: return@doOnPreDraw
|
||||
val delta = when (anchor) {
|
||||
is CollapseAnchor.CENTER_COLLAPSED_ROW -> row.top + row.height / 2 - list.height / 2
|
||||
is CollapseAnchor.KEEP_TOP_EDGE -> row.top - anchor.offset
|
||||
is CollapseAnchor.KEEP_BOTTOM_EDGE -> row.bottom - list.height - anchor.offset
|
||||
}
|
||||
val maximum = (scroll.getChildAt(0).height - scroll.height).coerceAtLeast(0)
|
||||
scroll.scrollTo(0, target.coerceIn(0, maximum))
|
||||
list.scrollBy(0, delta)
|
||||
afterRestore()
|
||||
}
|
||||
}
|
||||
@@ -403,9 +454,6 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun rowAt(rows: LinearLayout, edge: Int): View? = rows.children.firstOrNull { row ->
|
||||
edge >= row.top && edge <= row.bottom
|
||||
}
|
||||
|
||||
private fun dp(value: Int): Int = (value * resources.displayMetrics.density).toInt()
|
||||
private fun sp(value: Float): Float = value * resources.displayMetrics.scaledDensity
|
||||
@@ -421,9 +469,10 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) {
|
||||
)
|
||||
|
||||
private sealed interface CollapseAnchor {
|
||||
data class CENTER_COLLAPSED_ROW(val row: View) : CollapseAnchor
|
||||
data class KEEP_TOP_EDGE(val row: View, val offset: Int) : CollapseAnchor
|
||||
data class KEEP_BOTTOM_EDGE(val row: View, val offset: Int) : CollapseAnchor
|
||||
val position: Int
|
||||
data class CENTER_COLLAPSED_ROW(override val position: Int) : CollapseAnchor
|
||||
data class KEEP_TOP_EDGE(override val position: Int, val offset: Int) : CollapseAnchor
|
||||
data class KEEP_BOTTOM_EDGE(override val position: Int, val offset: Int) : CollapseAnchor
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
Reference in New Issue
Block a user