diff --git a/app/src/main/java/se/ajpanton/notificationlog/ViewLogsFragment.kt b/app/src/main/java/se/ajpanton/notificationlog/ViewLogsFragment.kt index 6894af0..a4a5ef4 100644 --- a/app/src/main/java/se/ajpanton/notificationlog/ViewLogsFragment.kt +++ b/app/src/main/java/se/ajpanton/notificationlog/ViewLogsFragment.kt @@ -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, 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, 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 = 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, 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 } } diff --git a/app/src/main/res/drawable/log_row_separator.xml b/app/src/main/res/drawable/log_row_separator.xml new file mode 100644 index 0000000..fb0cf04 --- /dev/null +++ b/app/src/main/res/drawable/log_row_separator.xml @@ -0,0 +1,5 @@ + + + + diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index 9bd6ba2..17136f0 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -8,4 +8,5 @@ #121318 #E3E2E9 #121318 + #8F8D96 diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 054b439..4d60a1f 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -8,4 +8,5 @@ #FBF8FF #1B1B21 #FBF8FF + #A5A2AB