|
|
|
@@ -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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|