Restyle log rows as a responsive table
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user