Restyle log rows as a responsive table

This commit is contained in:
ajp_anton
2026-07-27 21:36:03 +00:00
parent 627fda6bf8
commit 94250286e6
4 changed files with 76 additions and 35 deletions
@@ -1,9 +1,11 @@
package se.ajpanton.notificationlog
import android.graphics.BitmapFactory
import android.graphics.Paint
import android.os.Bundle
import android.content.ComponentName
import android.app.NotificationManager
import android.text.TextPaint
import android.text.TextUtils
import android.view.View
import android.widget.ImageView
@@ -84,7 +86,7 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) {
noMoreRows = entries.size < PAGE_SIZE
val addedRows = entries.map(::LogRow)
rows += addedRows
binding?.let { render(it, addedRows, reset) }
binding?.let(::render)
}
loading = false
binding?.logsRefresh?.isRefreshing = false
@@ -92,8 +94,8 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) {
}.start()
}
private fun render(view: FragmentViewLogsBinding, addedRows: List<LogRow>, reset: Boolean) {
if (reset) view.logRows.removeAllViews()
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."
@@ -101,15 +103,19 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) {
"Notification access is disabled. Enable it in Settings."
}
val settings = LogViewSettingsStore(requireContext()).load()
addedRows.filter { eventFor(it.entry.action) in settings.visibleEvents }
.forEach { row -> view.logRows.addView(logRowView(row, settings)) }
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())
}
}
private fun logRowView(row: LogRow, settings: LogViewSettings): View = LinearLayout(requireContext()).apply {
private fun logRowView(row: LogRow, settings: LogViewSettings, metadataColumnWidth: Int): View = LinearLayout(requireContext()).apply {
orientation = LinearLayout.HORIZONTAL
gravity = android.view.Gravity.TOP
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT).apply {
bottomMargin = dp(6)
bottomMargin = 0
}
fun renderExpanded(expanded: Boolean) {
@@ -124,26 +130,24 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) {
collapseAnchor?.let(::restoreCollapseAnchor)
Unit
}
val metadataPill = metadataPill(metadata, expanded, toggleExpanded, row.entry.id).apply {
val metadataCell = metadataCell(metadata, expanded, toggleExpanded, row.entry.id).apply {
layoutParams = LinearLayout.LayoutParams(
if (hasContents) 0 else LinearLayout.LayoutParams.MATCH_PARENT,
if (hasContents) metadataColumnWidth else LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT,
if (hasContents) LEFT_PILL_WEIGHT else 0f,
).apply {
if (hasContents) marginEnd = dp(6)
if (hasContents) marginEnd = dp(COLUMN_GAP_DP)
}
}
if (metadata.isNotEmpty()) addView(metadataPill)
if (metadata.isNotEmpty()) addView(metadataCell)
if (hasContents) {
val contentsPill = contentsPill(row, contents!!, metadata.size.coerceAtLeast(1), expanded, toggleExpanded).apply {
val contentsCell = contentsCell(row, contents!!, metadata.size.coerceAtLeast(1), expanded, toggleExpanded).apply {
layoutParams = LinearLayout.LayoutParams(
if (metadata.isNotEmpty()) 0 else LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT,
if (metadata.isNotEmpty()) RIGHT_PILL_WEIGHT else 0f,
if (metadata.isNotEmpty()) 1f else 0f,
)
}
addView(contentsPill)
if (metadata.isNotEmpty()) equalizePillHeights(metadataPill, contentsPill)
addView(contentsCell)
}
}
@@ -153,15 +157,14 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) {
}
}
private fun metadataPill(
private fun metadataCell(
values: List<MetadataValue>,
expanded: Boolean,
onToggleExpanded: () -> Unit,
entryId: String,
): LinearLayout = LinearLayout(requireContext()).apply {
orientation = LinearLayout.VERTICAL
background = androidx.core.content.ContextCompat.getDrawable(context, R.drawable.log_pill_background)
setPadding(dp(12), dp(8), dp(12), dp(8))
setPadding(dp(CELL_HORIZONTAL_PADDING_DP), dp(CELL_VERTICAL_PADDING_DP), dp(CELL_HORIZONTAL_PADDING_DP), dp(CELL_VERTICAL_PADDING_DP))
isClickable = true
isFocusable = true
values.forEach { value ->
@@ -183,7 +186,7 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) {
setOnLongClickListener { showDeleteDialog(entryId) }
}
private fun contentsPill(
private fun contentsCell(
row: LogRow,
contents: String,
collapsedLines: Int,
@@ -191,8 +194,7 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) {
onToggleExpanded: () -> Unit,
): LinearLayout = LinearLayout(requireContext()).apply {
orientation = LinearLayout.VERTICAL
background = androidx.core.content.ContextCompat.getDrawable(context, R.drawable.log_pill_background)
setPadding(dp(12), dp(8), dp(12), dp(8))
setPadding(0, dp(CELL_VERTICAL_PADDING_DP), 0, dp(CELL_VERTICAL_PADDING_DP))
isClickable = true
isFocusable = true
val message = TextView(context).apply {
@@ -241,7 +243,7 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) {
}
private fun metadataValues(entry: NotificationLogEntry, settings: LogViewSettings): List<MetadataValue> = buildList {
if (LogField.TIMESTAMP in settings.visibleFields) add(MetadataValue(timestamp(entry.recordedAtEpochMillis, settings.timestampZone, entry.eventTimeZoneId), 12f))
if (LogField.TIMESTAMP in settings.visibleFields) add(MetadataValue(timestamp(entry.recordedAtEpochMillis, settings.timestampZone, entry.eventTimeZoneId), 12f, isTimestamp = true))
if (LogField.APP_NAME in settings.visibleFields) add(MetadataValue(entry.appName, 15f, indented = true, bold = true))
if (LogField.PACKAGE_NAME in settings.visibleFields) add(MetadataValue(entry.packageName, 12f, indented = true, breakBeforePeriods = true))
if (LogField.ACTION in settings.visibleFields) add(MetadataValue(actionLabel(entry.action), 14f, indented = true))
@@ -277,6 +279,41 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) {
.getSystemService(NotificationManager::class.java)
.isNotificationListenerAccessGranted(ComponentName(requireContext(), NotificationCaptureService::class.java))
private fun metadataColumnWidth(availableWidth: Int, rows: List<LogRow>, settings: LogViewSettings): Int {
val values = rows.flatMap { metadataValues(it.entry, settings) }
val widestTimestamp = values.filter { it.isTimestamp }
.maxOfOrNull(::metadataTextWidth)
?: 0
val widestMetadata = values
.maxOfOrNull(::metadataTextWidth)
?: 0
val timestampMinimumWidth = maxOf(
dp(MINIMUM_METADATA_COLUMN_DP),
widestTimestamp + dp(CELL_HORIZONTAL_PADDING_DP * 2),
)
val preferredWidth = maxOf(timestampMinimumWidth, widestMetadata + dp(CELL_HORIZONTAL_PADDING_DP * 2))
if (availableWidth <= 0) return preferredWidth
val maximumWidth = maxOf(
timestampMinimumWidth,
(availableWidth * MAXIMUM_METADATA_COLUMN_FRACTION).toInt(),
)
return minOf(preferredWidth, maximumWidth)
}
private fun metadataTextWidth(value: MetadataValue): Int = TextPaint(Paint.ANTI_ALIAS_FLAG).run {
textSize = sp(value.textSize)
typeface = if (value.bold) android.graphics.Typeface.DEFAULT_BOLD else android.graphics.Typeface.DEFAULT
measureText(value.text).toInt() + if (value.indented) dp(METADATA_INDENT_DP) else 0
}
private fun rowSeparator(): View = View(requireContext()).apply {
background = androidx.core.content.ContextCompat.getDrawable(context, R.drawable.log_row_separator)
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, dp(1)).apply {
topMargin = dp(ROW_SEPARATOR_MARGIN_DP)
bottomMargin = dp(ROW_SEPARATOR_MARGIN_DP)
}
}
private fun timestamp(value: Long, zone: TimestampZone, eventTimeZoneId: String?): String = DateFormat.getDateTimeInstance().apply {
timeZone = when (zone) {
TimestampZone.UTC -> java.util.TimeZone.getTimeZone("UTC")
@@ -300,16 +337,6 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) {
return true
}
private fun equalizePillHeights(left: View, right: View) {
left.post {
val targetHeight = maxOf(left.height, right.height)
if (left.minimumHeight != targetHeight || right.minimumHeight != targetHeight) {
left.minimumHeight = targetHeight
right.minimumHeight = targetHeight
}
}
}
private fun captureCollapseAnchor(collapsingRow: View): CollapseAnchor? {
val currentBinding = binding ?: return null
val scroll = currentBinding.logScroll
@@ -349,6 +376,7 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) {
}
private fun dp(value: Int): Int = (value * resources.displayMetrics.density).toInt()
private fun sp(value: Float): Float = value * resources.displayMetrics.scaledDensity
private data class LogRow(val entry: NotificationLogEntry)
private data class MetadataValue(
@@ -357,6 +385,7 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) {
val indented: Boolean = false,
val bold: Boolean = false,
val breakBeforePeriods: Boolean = false,
val isTimestamp: Boolean = false,
)
private sealed interface CollapseAnchor {
@@ -366,11 +395,16 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) {
}
private companion object {
const val LEFT_PILL_WEIGHT = 0.42f
const val RIGHT_PILL_WEIGHT = 0.58f
const val SCROLL_TO_TOP_THRESHOLD_DP = 120
const val LOAD_MORE_THRESHOLD_DP = 480
const val PAGE_SIZE = 80
const val IMAGE_VIEWER_TAG = "image-viewer"
const val MINIMUM_METADATA_COLUMN_DP = 120
const val CELL_HORIZONTAL_PADDING_DP = 8
const val CELL_VERTICAL_PADDING_DP = 6
const val COLUMN_GAP_DP = 12
const val ROW_SEPARATOR_MARGIN_DP = 4
const val METADATA_INDENT_DP = 8
const val MAXIMUM_METADATA_COLUMN_FRACTION = 0.45f
}
}
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/log_row_separator" />
</shape>
+1
View File
@@ -8,4 +8,5 @@
<color name="surface">#121318</color>
<color name="on_surface">#E3E2E9</color>
<color name="window_background">#121318</color>
<color name="log_row_separator">#8F8D96</color>
</resources>
+1
View File
@@ -8,4 +8,5 @@
<color name="surface">#FBF8FF</color>
<color name="on_surface">#1B1B21</color>
<color name="window_background">#FBF8FF</color>
<color name="log_row_separator">#A5A2AB</color>
</resources>