From 43905f72f099c27a6ef9723b5cf15c8d4564779d Mon Sep 17 00:00:00 2001 From: ajp_anton Date: Sun, 26 Jul 2026 23:22:37 +0000 Subject: [PATCH] Present logs in compact metadata and content pills --- .../notificationlog/ViewLogsFragment.kt | 167 ++++++++++++------ .../main/res/drawable/log_pill_background.xml | 5 + app/src/main/res/layout/drawer_header.xml | 7 - app/src/main/res/values/dimens.xml | 2 +- app/src/main/res/values/strings.xml | 1 - 5 files changed, 120 insertions(+), 62 deletions(-) create mode 100644 app/src/main/res/drawable/log_pill_background.xml diff --git a/app/src/main/java/se/ajpanton/notificationlog/ViewLogsFragment.kt b/app/src/main/java/se/ajpanton/notificationlog/ViewLogsFragment.kt index 6408a2d..d61112a 100644 --- a/app/src/main/java/se/ajpanton/notificationlog/ViewLogsFragment.kt +++ b/app/src/main/java/se/ajpanton/notificationlog/ViewLogsFragment.kt @@ -65,41 +65,91 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) { } private fun logRowView(row: LogRow, settings: LogViewSettings): View = LinearLayout(requireContext()).apply { - orientation = LinearLayout.VERTICAL + orientation = LinearLayout.HORIZONTAL + gravity = android.view.Gravity.TOP layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT).apply { - bottomMargin = dp(12) + bottomMargin = dp(6) } - isClickable = true - isFocusable = true fun renderExpanded(expanded: Boolean) { removeAllViews() - if (!expanded) { - addView(TextView(context).apply { - text = values(row.entry, settings).joinToString(" ยท ") - maxLines = 1 - ellipsize = TextUtils.TruncateAt.END - setPadding(0, dp(8), 0, dp(8)) - }) - return - } - settings.order.filter { it in settings.visibleFields }.forEach { field -> - val value = fieldValue(field, row.entry, settings).takeUnless { field == LogField.CONTENTS && it == "[image]" } - if (!value.isNullOrEmpty()) { - addView(TextView(context).apply { - text = value - setLineSpacing(0f, 0.92f) - setPadding(0, dp(1), 0, dp(1)) - }) + val metadata = metadataValues(row.entry, settings) + val contents = fieldValue(LogField.CONTENTS, row.entry, settings) + val hasContents = LogField.CONTENTS in settings.visibleFields && !contents.isNullOrEmpty() + val metadataPill = metadataPill(metadata).apply { + layoutParams = LinearLayout.LayoutParams( + if (hasContents) 0 else LinearLayout.LayoutParams.MATCH_PARENT, + if (hasContents) LinearLayout.LayoutParams.MATCH_PARENT else LinearLayout.LayoutParams.WRAP_CONTENT, + if (hasContents) LEFT_PILL_WEIGHT else 0f, + ).apply { + if (hasContents) marginEnd = dp(6) } } - if (row.entry.action == NotificationAction.EDITED && !row.entry.previousContents.isNullOrEmpty()) { - addView(TextView(context).apply { - text = "Previous: ${row.entry.previousContents}" - setLineSpacing(0f, 0.92f) - setPadding(0, dp(1), 0, dp(1)) + if (metadata.isNotEmpty()) addView(metadataPill) + if (hasContents) { + addView(contentsPill(row, contents!!, metadata.size.coerceAtLeast(1), expanded) { + if (!expandedIds.add(row.entry.id)) expandedIds.remove(row.entry.id) + renderExpanded(row.entry.id in expandedIds) + }.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, + ) }) } + } + + renderExpanded(row.entry.id in expandedIds) + setOnLongClickListener { + showDeleteDialog(row.entry.id) + } + } + + private fun metadataPill(values: List): 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)) + values.forEach { value -> + addView(TextView(context).apply { + text = value.text + maxLines = 1 + ellipsize = TextUtils.TruncateAt.END + textSize = value.textSize + if (value.indented) setPadding(dp(8), 0, 0, 0) + setLineSpacing(0f, 1f) + if (value.bold) setTypeface(typeface, android.graphics.Typeface.BOLD) + }) + } + } + + private fun contentsPill( + row: LogRow, + contents: String, + collapsedLines: Int, + expanded: Boolean, + 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)) + isClickable = true + isFocusable = true + val message = TextView(context).apply { + text = contents + maxLines = if (expanded) Int.MAX_VALUE else collapsedLines + ellipsize = if (expanded) null else TextUtils.TruncateAt.END + setLineSpacing(0f, 1f) + } + addView(message) + if (expanded && row.entry.action == NotificationAction.EDITED && !row.entry.previousContents.isNullOrEmpty()) { + addView(TextView(context).apply { + text = "Previous: ${row.entry.previousContents}" + setLineSpacing(0f, 1f) + setPadding(0, dp(4), 0, 0) + }) + } + if (expanded) { row.imageBytes?.let { bytes -> BitmapFactory.decodeByteArray(bytes, 0, bytes.size)?.let { bitmap -> addView(ImageView(context).apply { @@ -107,35 +157,21 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) { adjustViewBounds = true maxHeight = dp(240) contentDescription = "Notification image" - setPadding(0, dp(4), 0, dp(4)) + setPadding(0, dp(4), 0, 0) }) } } } - - renderExpanded(row.entry.id in expandedIds) - setOnClickListener { - if (!expandedIds.add(row.entry.id)) expandedIds.remove(row.entry.id) - renderExpanded(row.entry.id in expandedIds) - } - setOnLongClickListener { - MaterialAlertDialogBuilder(requireContext()) - .setTitle("Delete this log?") - .setMessage("This permanently removes this log and its copied image, if any.") - .setNegativeButton("Cancel", null) - .setPositiveButton("Delete") { _, _ -> - Thread { - EncryptedNotificationLogStore(requireContext().applicationContext).delete(row.entry.id) - activity?.runOnUiThread(::loadLogs) - }.start() - } - .show() - true - } + setOnClickListener { onToggleExpanded() } + setOnLongClickListener { showDeleteDialog(row.entry.id) } } - private fun values(entry: NotificationLogEntry, settings: LogViewSettings): List = - settings.order.filter { it in settings.visibleFields }.mapNotNull { fieldValue(it, entry, settings) } + 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.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)) + if (LogField.ACTION in settings.visibleFields) add(MetadataValue(actionLabel(entry.action), 14f, indented = true)) + } private fun fieldValue(field: LogField, entry: NotificationLogEntry, settings: LogViewSettings): String? = when (field) { LogField.TIMESTAMP -> timestamp(entry.recordedAtEpochMillis, settings.timestampZone, entry.eventTimeZoneId) @@ -146,10 +182,14 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) { } private fun actionLabel(action: NotificationAction): String = when (action) { - NotificationAction.APP_CANCELLED -> "App cancelled notification" - NotificationAction.APP_CANCELLED_ALL -> "App cancelled all notifications" - NotificationAction.USER_DISMISSED -> "User dismissed notification" - NotificationAction.USER_DISMISSED_ALL -> "User dismissed all notifications" + NotificationAction.APPEARED -> "Appeared" + NotificationAction.ALREADY_ACTIVE -> "Already active" + NotificationAction.EDITED -> "Edited" + NotificationAction.APP_CANCELLED -> "App cancelled" + NotificationAction.APP_CANCELLED_ALL -> "App cancelled all" + NotificationAction.USER_DISMISSED -> "User dismissed" + NotificationAction.USER_DISMISSED_ALL -> "User dismissed all" + NotificationAction.USER_CLICKED -> "User opened" else -> action.name.lowercase().replace('_', ' ').replaceFirstChar(Char::uppercase) } @@ -161,7 +201,28 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) { } }.format(Date(value)) + private fun showDeleteDialog(entryId: String): Boolean { + MaterialAlertDialogBuilder(requireContext()) + .setTitle("Delete this log?") + .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) + }.start() + } + .show() + return true + } + private fun dp(value: Int): Int = (value * resources.displayMetrics.density).toInt() private data class LogRow(val entry: NotificationLogEntry, val imageBytes: ByteArray?) + private data class MetadataValue(val text: String, val textSize: Float, val indented: Boolean = false, val bold: Boolean = false) + + private companion object { + const val LEFT_PILL_WEIGHT = 0.42f + const val RIGHT_PILL_WEIGHT = 0.58f + } } diff --git a/app/src/main/res/drawable/log_pill_background.xml b/app/src/main/res/drawable/log_pill_background.xml new file mode 100644 index 0000000..1445c5e --- /dev/null +++ b/app/src/main/res/drawable/log_pill_background.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/layout/drawer_header.xml b/app/src/main/res/layout/drawer_header.xml index ac52ddd..ba16c60 100644 --- a/app/src/main/res/layout/drawer_header.xml +++ b/app/src/main/res/layout/drawer_header.xml @@ -15,11 +15,4 @@ android:textColor="@color/on_primary" android:textStyle="bold" /> - diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 0971e76..faefcc3 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -1,6 +1,6 @@ - 216dp + 177dp 360dp 48dp 86dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ac942e8..1134b4f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,7 +1,6 @@ Notification Log - Notification history Notification Log listener Open navigation Close navigation