Present logs in compact metadata and content pills
This commit is contained in:
@@ -65,41 +65,91 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun logRowView(row: LogRow, settings: LogViewSettings): View = LinearLayout(requireContext()).apply {
|
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 {
|
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) {
|
fun renderExpanded(expanded: Boolean) {
|
||||||
removeAllViews()
|
removeAllViews()
|
||||||
if (!expanded) {
|
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 (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<MetadataValue>): 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 {
|
addView(TextView(context).apply {
|
||||||
text = values(row.entry, settings).joinToString(" · ")
|
text = value.text
|
||||||
maxLines = 1
|
maxLines = 1
|
||||||
ellipsize = TextUtils.TruncateAt.END
|
ellipsize = TextUtils.TruncateAt.END
|
||||||
setPadding(0, dp(8), 0, dp(8))
|
textSize = value.textSize
|
||||||
})
|
if (value.indented) setPadding(dp(8), 0, 0, 0)
|
||||||
return
|
setLineSpacing(0f, 1f)
|
||||||
}
|
if (value.bold) setTypeface(typeface, android.graphics.Typeface.BOLD)
|
||||||
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))
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (row.entry.action == NotificationAction.EDITED && !row.entry.previousContents.isNullOrEmpty()) {
|
|
||||||
|
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 {
|
addView(TextView(context).apply {
|
||||||
text = "Previous: ${row.entry.previousContents}"
|
text = "Previous: ${row.entry.previousContents}"
|
||||||
setLineSpacing(0f, 0.92f)
|
setLineSpacing(0f, 1f)
|
||||||
setPadding(0, dp(1), 0, dp(1))
|
setPadding(0, dp(4), 0, 0)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
if (expanded) {
|
||||||
row.imageBytes?.let { bytes ->
|
row.imageBytes?.let { bytes ->
|
||||||
BitmapFactory.decodeByteArray(bytes, 0, bytes.size)?.let { bitmap ->
|
BitmapFactory.decodeByteArray(bytes, 0, bytes.size)?.let { bitmap ->
|
||||||
addView(ImageView(context).apply {
|
addView(ImageView(context).apply {
|
||||||
@@ -107,35 +157,21 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) {
|
|||||||
adjustViewBounds = true
|
adjustViewBounds = true
|
||||||
maxHeight = dp(240)
|
maxHeight = dp(240)
|
||||||
contentDescription = "Notification image"
|
contentDescription = "Notification image"
|
||||||
setPadding(0, dp(4), 0, dp(4))
|
setPadding(0, dp(4), 0, 0)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
setOnClickListener { onToggleExpanded() }
|
||||||
renderExpanded(row.entry.id in expandedIds)
|
setOnLongClickListener { showDeleteDialog(row.entry.id) }
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun values(entry: NotificationLogEntry, settings: LogViewSettings): List<String> =
|
private fun metadataValues(entry: NotificationLogEntry, settings: LogViewSettings): List<MetadataValue> = buildList {
|
||||||
settings.order.filter { it in settings.visibleFields }.mapNotNull { fieldValue(it, entry, settings) }
|
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) {
|
private fun fieldValue(field: LogField, entry: NotificationLogEntry, settings: LogViewSettings): String? = when (field) {
|
||||||
LogField.TIMESTAMP -> timestamp(entry.recordedAtEpochMillis, settings.timestampZone, entry.eventTimeZoneId)
|
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) {
|
private fun actionLabel(action: NotificationAction): String = when (action) {
|
||||||
NotificationAction.APP_CANCELLED -> "App cancelled notification"
|
NotificationAction.APPEARED -> "Appeared"
|
||||||
NotificationAction.APP_CANCELLED_ALL -> "App cancelled all notifications"
|
NotificationAction.ALREADY_ACTIVE -> "Already active"
|
||||||
NotificationAction.USER_DISMISSED -> "User dismissed notification"
|
NotificationAction.EDITED -> "Edited"
|
||||||
NotificationAction.USER_DISMISSED_ALL -> "User dismissed all notifications"
|
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)
|
else -> action.name.lowercase().replace('_', ' ').replaceFirstChar(Char::uppercase)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,7 +201,28 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) {
|
|||||||
}
|
}
|
||||||
}.format(Date(value))
|
}.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 fun dp(value: Int): Int = (value * resources.displayMetrics.density).toInt()
|
||||||
|
|
||||||
private data class LogRow(val entry: NotificationLogEntry, val imageBytes: ByteArray?)
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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="?attr/colorSurfaceVariant" />
|
||||||
|
<corners android:radius="18dp" />
|
||||||
|
</shape>
|
||||||
@@ -15,11 +15,4 @@
|
|||||||
android:textColor="@color/on_primary"
|
android:textColor="@color/on_primary"
|
||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="6dp"
|
|
||||||
android:text="@string/drawer_subtitle"
|
|
||||||
android:textAppearance="?attr/textAppearanceCaption"
|
|
||||||
android:textColor="@color/on_primary" />
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<dimen name="navigation_drawer_width">216dp</dimen>
|
<dimen name="navigation_drawer_width">177dp</dimen>
|
||||||
<dimen name="folded_portrait_width">360dp</dimen>
|
<dimen name="folded_portrait_width">360dp</dimen>
|
||||||
<dimen name="navigation_menu_item_height">48dp</dimen>
|
<dimen name="navigation_menu_item_height">48dp</dimen>
|
||||||
<dimen name="drawer_header_height">86dp</dimen>
|
<dimen name="drawer_header_height">86dp</dimen>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">Notification Log</string>
|
<string name="app_name">Notification Log</string>
|
||||||
<string name="drawer_subtitle">Notification history</string>
|
|
||||||
<string name="notification_listener_label">Notification Log listener</string>
|
<string name="notification_listener_label">Notification Log listener</string>
|
||||||
<string name="navigation_open">Open navigation</string>
|
<string name="navigation_open">Open navigation</string>
|
||||||
<string name="navigation_close">Close navigation</string>
|
<string name="navigation_close">Close navigation</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user