From 6116c13e006e02d1d3f54e207a7ee9be79fd4e71 Mon Sep 17 00:00:00 2001 From: ajp_anton Date: Mon, 27 Jul 2026 22:06:47 +0000 Subject: [PATCH] Add configurable timestamp date and clock formats --- .../notificationlog/LogDisplayFragment.kt | 104 ++++++++++++++---- .../notificationlog/SettingsFragment.kt | 14 ++- .../notificationlog/ViewLogsFragment.kt | 17 +-- .../notificationlog/export/LogExporter.kt | 39 +++---- .../settings/LogViewSettings.kt | 4 + .../settings/LogViewSettingsStore.kt | 4 + .../settings/TimestampFormatter.kt | 42 +++++++ .../main/res/layout/fragment_log_display.xml | 2 - .../notificationlog/export/LogExporterTest.kt | 13 +++ 9 files changed, 173 insertions(+), 66 deletions(-) create mode 100644 app/src/main/java/se/ajpanton/notificationlog/settings/TimestampFormatter.kt diff --git a/app/src/main/java/se/ajpanton/notificationlog/LogDisplayFragment.kt b/app/src/main/java/se/ajpanton/notificationlog/LogDisplayFragment.kt index 0e8473f..b7d257e 100644 --- a/app/src/main/java/se/ajpanton/notificationlog/LogDisplayFragment.kt +++ b/app/src/main/java/se/ajpanton/notificationlog/LogDisplayFragment.kt @@ -4,12 +4,18 @@ import android.content.ClipData import android.os.Bundle import android.view.View import android.widget.ArrayAdapter +import android.widget.LinearLayout +import android.widget.Spinner +import android.widget.TextView import androidx.fragment.app.Fragment import com.google.android.material.switchmaterial.SwitchMaterial import se.ajpanton.notificationlog.databinding.FragmentLogDisplayBinding import se.ajpanton.notificationlog.settings.DisplayEvent import se.ajpanton.notificationlog.settings.LogField +import se.ajpanton.notificationlog.settings.LogViewSettings import se.ajpanton.notificationlog.settings.LogViewSettingsStore +import se.ajpanton.notificationlog.settings.TimestampClockFormat +import se.ajpanton.notificationlog.settings.TimestampDateFormat import se.ajpanton.notificationlog.settings.TimestampZone class LogDisplayFragment : Fragment(R.layout.fragment_log_display) { @@ -35,15 +41,18 @@ class LogDisplayFragment : Fragment(R.layout.fragment_log_display) { }) } settings.order.forEach { field -> - binding!!.fieldToggles.addView(SwitchMaterial(requireContext()).apply { + val fieldGroup = LinearLayout(requireContext()).apply { + orientation = LinearLayout.VERTICAL + tag = field + } + val toggle = SwitchMaterial(requireContext()).apply { text = field.label() isChecked = field in settings.visibleFields - tag = field setOnLongClickListener { startDragAndDrop( ClipData.newPlainText("field", field.name), - View.DragShadowBuilder(this), - this, + View.DragShadowBuilder(fieldGroup), + fieldGroup, 0, ) true @@ -56,7 +65,26 @@ class LogDisplayFragment : Fragment(R.layout.fragment_log_display) { ) store.save(settings) } - }) + } + fieldGroup.addView(toggle) + if (field == LogField.TIMESTAMP) { + val controls = timestampControls({ settings }) { updated -> + settings = updated + store.save(settings) + } + fieldGroup.addView(controls) + setEnabledRecursively(controls, toggle.isChecked) + toggle.setOnCheckedChangeListener { _, checked -> + settings = settings.copy( + visibleFields = settings.visibleFields.toMutableSet().apply { + if (checked) add(field) else remove(field) + }, + ) + store.save(settings) + setEnabledRecursively(controls, checked) + } + } + binding!!.fieldToggles.addView(fieldGroup) } binding!!.fieldToggles.setOnDragListener { _, event -> if (event.action != android.view.DragEvent.ACTION_DROP) return@setOnDragListener true @@ -69,29 +97,57 @@ class LogDisplayFragment : Fragment(R.layout.fragment_log_display) { store.save(settings) true } - binding!!.timezone.adapter = ArrayAdapter( - requireContext(), - android.R.layout.simple_spinner_dropdown_item, - listOf("Current local timezone", "UTC", "Local timezone when event happened"), - ) - binding!!.timezone.setSelection(TimestampZone.entries.indexOf(settings.timestampZone)) - binding!!.timezone.onItemSelectedListener = object : android.widget.AdapterView.OnItemSelectedListener { - override fun onNothingSelected(parent: android.widget.AdapterView<*>?) = Unit - - override fun onItemSelected( - parent: android.widget.AdapterView<*>?, - view: View?, - position: Int, - id: Long, - ) { - settings = settings.copy(timestampZone = TimestampZone.entries[position]) - store.save(settings) - } - } } override fun onDestroyView() { binding = null; super.onDestroyView() } + private fun timestampControls( + currentSettings: () -> LogViewSettings, + saveSettings: (LogViewSettings) -> Unit, + ): LinearLayout = LinearLayout(requireContext()).apply { + orientation = LinearLayout.VERTICAL + setPadding(dp(TIMESTAMP_CONTROLS_INDENT_DP), 0, 0, 0) + addDropdown("Timestamp timezone", listOf("Current local timezone", "UTC", "Local timezone when event happened"), TimestampZone.entries.indexOf(currentSettings().timestampZone)) { position -> + val updated = currentSettings().copy(timestampZone = TimestampZone.entries[position]) + saveSettings(updated) + } + addDropdown("Date format", listOf("System default", "YYYY-MM-DD", "DD-MM-YYYY", "MM-DD-YYYY"), TimestampDateFormat.entries.indexOf(currentSettings().timestampDateFormat)) { position -> + val updated = currentSettings().copy(timestampDateFormat = TimestampDateFormat.entries[position]) + saveSettings(updated) + } + addDropdown("Clock format", listOf("System default", "24-hour clock", "12-hour clock"), TimestampClockFormat.entries.indexOf(currentSettings().timestampClockFormat)) { position -> + val updated = currentSettings().copy(timestampClockFormat = TimestampClockFormat.entries[position]) + saveSettings(updated) + } + } + + private fun LinearLayout.addDropdown(label: String, values: List, selected: Int, onSelected: (Int) -> Unit) { + addView(TextView(context).apply { text = label }) + addView(Spinner(context).apply { + adapter = ArrayAdapter(context, android.R.layout.simple_spinner_dropdown_item, values) + setSelection(selected) + onItemSelectedListener = object : android.widget.AdapterView.OnItemSelectedListener { + override fun onNothingSelected(parent: android.widget.AdapterView<*>?) = Unit + override fun onItemSelected(parent: android.widget.AdapterView<*>?, view: View?, position: Int, id: Long) = onSelected(position) + } + }) + } + + private fun setEnabledRecursively(view: View, enabled: Boolean) { + view.isEnabled = enabled + view.alpha = if (enabled) 1f else DISABLED_TIMESTAMP_CONTROLS_ALPHA + if (view is android.view.ViewGroup) { + (0 until view.childCount).forEach { setEnabledRecursively(view.getChildAt(it), enabled) } + } + } + + private fun dp(value: Int): Int = (value * resources.displayMetrics.density).toInt() + + private companion object { + const val TIMESTAMP_CONTROLS_INDENT_DP = 24 + const val DISABLED_TIMESTAMP_CONTROLS_ALPHA = 0.5f + } + private fun LogField.label() = when (this) { LogField.TIMESTAMP -> "Timestamp" LogField.APP_NAME -> "App name" diff --git a/app/src/main/java/se/ajpanton/notificationlog/SettingsFragment.kt b/app/src/main/java/se/ajpanton/notificationlog/SettingsFragment.kt index 4ee6b67..5ed5bf6 100644 --- a/app/src/main/java/se/ajpanton/notificationlog/SettingsFragment.kt +++ b/app/src/main/java/se/ajpanton/notificationlog/SettingsFragment.kt @@ -103,8 +103,8 @@ class SettingsFragment : Fragment(R.layout.fragment_settings) { val settings = LogViewSettingsStore(context).load() context.contentResolver.openOutputStream(uri)?.use { output -> when (format) { - ExportFormat.CSV -> writeCsv(output.bufferedWriter(Charsets.UTF_8), logStore, settings) - ExportFormat.FORMATTED -> writeFormatted(output.bufferedWriter(Charsets.UTF_8), logStore, settings) + ExportFormat.CSV -> writeCsv(output.bufferedWriter(Charsets.UTF_8), logStore, settings, context) + ExportFormat.FORMATTED -> writeFormatted(output.bufferedWriter(Charsets.UTF_8), logStore, settings, context) ExportFormat.HTML_ZIP -> writeHtmlZip(ZipOutputStream(output), logStore, settings, context) } } @@ -115,11 +115,12 @@ class SettingsFragment : Fragment(R.layout.fragment_settings) { writer: java.io.BufferedWriter, logStore: EncryptedNotificationLogStore, settings: se.ajpanton.notificationlog.settings.LogViewSettings, + context: android.content.Context, ) = writer.use { it.write(LogExporter.csvHeader(settings)) logStore.forEachNewest { entry -> it.newLine() - it.write(LogExporter.csvRow(entry, settings)) + it.write(LogExporter.csvRow(entry, settings, context)) } } @@ -127,18 +128,19 @@ class SettingsFragment : Fragment(R.layout.fragment_settings) { writer: java.io.BufferedWriter, logStore: EncryptedNotificationLogStore, settings: se.ajpanton.notificationlog.settings.LogViewSettings, + context: android.content.Context, ) = writer.use { val headers = LogExporter.headers(settings) val widths = headers.map { header -> header.length }.toMutableList() logStore.forEachNewest { entry -> - LogExporter.values(entry, settings).forEachIndexed { index, value -> + LogExporter.values(entry, settings, context).forEachIndexed { index, value -> widths[index] = maxOf(widths[index], value.length) } } it.write(LogExporter.formattedRow(headers, widths)) logStore.forEachNewest { entry -> it.newLine() - it.write(LogExporter.formattedRow(LogExporter.values(entry, settings), widths)) + it.write(LogExporter.formattedRow(LogExporter.values(entry, settings, context), widths)) } } @@ -155,7 +157,7 @@ class SettingsFragment : Fragment(R.layout.fragment_settings) { val imagePath = entry.imageId ?.takeIf { LogField.CONTENTS in settings.visibleFields && imageStore.exists(it) } ?.let { imageId -> "images/$imageId.png" } - zipOutput.write(LogExporter.htmlRow(entry, settings, imagePath).encodeToByteArray()) + zipOutput.write(LogExporter.htmlRow(entry, settings, imagePath, context).encodeToByteArray()) } zipOutput.write(LogExporter.htmlEnd().encodeToByteArray()) zipOutput.closeEntry() diff --git a/app/src/main/java/se/ajpanton/notificationlog/ViewLogsFragment.kt b/app/src/main/java/se/ajpanton/notificationlog/ViewLogsFragment.kt index 3cd29d8..a4c19f5 100644 --- a/app/src/main/java/se/ajpanton/notificationlog/ViewLogsFragment.kt +++ b/app/src/main/java/se/ajpanton/notificationlog/ViewLogsFragment.kt @@ -25,11 +25,9 @@ import se.ajpanton.notificationlog.model.NotificationLogEntry import se.ajpanton.notificationlog.settings.LogField import se.ajpanton.notificationlog.settings.LogViewSettings import se.ajpanton.notificationlog.settings.LogViewSettingsStore -import se.ajpanton.notificationlog.settings.TimestampZone +import se.ajpanton.notificationlog.settings.TimestampFormatter import se.ajpanton.notificationlog.settings.DisplayEvent import se.ajpanton.notificationlog.capture.NotificationCaptureService -import java.text.DateFormat -import java.util.Date import androidx.fragment.app.Fragment import androidx.core.content.ContextCompat import androidx.core.view.children @@ -257,14 +255,14 @@ 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, isTimestamp = true)) + if (LogField.TIMESTAMP in settings.visibleFields) add(MetadataValue(timestamp(entry.recordedAtEpochMillis, settings, 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)) } 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, entry.eventTimeZoneId) LogField.APP_NAME -> entry.appName LogField.PACKAGE_NAME -> entry.packageName LogField.ACTION -> actionLabel(entry.action) @@ -328,13 +326,8 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) { } } - private fun timestamp(value: Long, zone: TimestampZone, eventTimeZoneId: String?): String = DateFormat.getDateTimeInstance().apply { - timeZone = when (zone) { - TimestampZone.UTC -> java.util.TimeZone.getTimeZone("UTC") - TimestampZone.LOCAL_NOW -> java.util.TimeZone.getDefault() - TimestampZone.EVENT_LOCAL -> eventTimeZoneId?.let(java.util.TimeZone::getTimeZone) ?: java.util.TimeZone.getDefault() - } - }.format(Date(value)) + private fun timestamp(value: Long, settings: LogViewSettings, eventTimeZoneId: String?): String = + TimestampFormatter.format(requireContext(), value, settings, eventTimeZoneId) private fun showDeleteDialog(entryId: String): Boolean { MaterialAlertDialogBuilder(requireContext()) diff --git a/app/src/main/java/se/ajpanton/notificationlog/export/LogExporter.kt b/app/src/main/java/se/ajpanton/notificationlog/export/LogExporter.kt index 20c46fd..f2f9f46 100644 --- a/app/src/main/java/se/ajpanton/notificationlog/export/LogExporter.kt +++ b/app/src/main/java/se/ajpanton/notificationlog/export/LogExporter.kt @@ -1,28 +1,26 @@ package se.ajpanton.notificationlog.export +import android.content.Context import se.ajpanton.notificationlog.model.NotificationLogEntry import se.ajpanton.notificationlog.settings.LogField import se.ajpanton.notificationlog.settings.LogViewSettings -import se.ajpanton.notificationlog.settings.TimestampZone -import java.text.DateFormat -import java.util.Date -import java.util.TimeZone +import se.ajpanton.notificationlog.settings.TimestampFormatter object LogExporter { - fun csv(entries: List, settings: LogViewSettings): String = ( - listOf(csvHeader(settings)) + entries.sortedByDescending { it.recordedAtEpochMillis }.map { csvRow(it, settings) } + fun csv(entries: List, settings: LogViewSettings, context: Context? = null): String = ( + listOf(csvHeader(settings)) + entries.sortedByDescending { it.recordedAtEpochMillis }.map { csvRow(it, settings, context) } ).joinToString("\n") - fun formatted(entries: List, settings: LogViewSettings): String { - val rows = listOf(headers(settings)) + entries.sortedByDescending { it.recordedAtEpochMillis }.map { values(it, settings) } + fun formatted(entries: List, settings: LogViewSettings, context: Context? = null): String { + val rows = listOf(headers(settings)) + entries.sortedByDescending { it.recordedAtEpochMillis }.map { values(it, settings, context) } val widths = rows.fold(emptyList()) { current, row -> row.mapIndexed { i, value -> maxOf(current.getOrElse(i) { 0 }, value.length) } } return rows.joinToString("\n") { formattedRow(it, widths) } } fun headers(settings: LogViewSettings): List = fields(settings).map(::header) - fun values(entry: NotificationLogEntry, settings: LogViewSettings): List = fields(settings).map { field -> when (field) { - LogField.TIMESTAMP -> timestamp(entry.recordedAtEpochMillis, settings.timestampZone, entry.eventTimeZoneId) + fun values(entry: NotificationLogEntry, settings: LogViewSettings, context: Context? = null): List = fields(settings).map { field -> when (field) { + LogField.TIMESTAMP -> timestamp(context, entry.recordedAtEpochMillis, settings, entry.eventTimeZoneId) LogField.APP_NAME -> entry.appName LogField.PACKAGE_NAME -> entry.packageName LogField.ACTION -> entry.action.name.lowercase().replace('_', ' ') @@ -31,7 +29,7 @@ object LogExporter { fun csvHeader(settings: LogViewSettings): String = csvLine(headers(settings)) - fun csvRow(entry: NotificationLogEntry, settings: LogViewSettings): String = csvLine(values(entry, settings)) + fun csvRow(entry: NotificationLogEntry, settings: LogViewSettings, context: Context? = null): String = csvLine(values(entry, settings, context)) fun formattedRow(values: List, widths: List): String = values.mapIndexed { index, value -> value.padEnd(widths[index]) }.joinToString(" ") @@ -46,9 +44,10 @@ object LogExporter { entry: NotificationLogEntry, settings: LogViewSettings, imagePath: String? = null, + context: Context? = null, ): String = buildString { append("") - fields(settings).forEach { field -> append("${htmlValue(field, entry, settings, imagePath)}") } + fields(settings).forEach { field -> append("${htmlValue(field, entry, settings, imagePath, context)}") } append("") } @@ -57,11 +56,12 @@ object LogExporter { fun html( entries: List, settings: LogViewSettings, + context: Context? = null, imagePath: (NotificationLogEntry) -> String? = { null }, ): String = buildString { append(htmlStart(settings)) entries.sortedByDescending { it.recordedAtEpochMillis }.forEach { entry -> - append(htmlRow(entry, settings, imagePath(entry))) + append(htmlRow(entry, settings, imagePath(entry), context)) } append(htmlEnd()) } @@ -69,9 +69,9 @@ object LogExporter { private fun fields(settings: LogViewSettings): List = settings.order.filter { it in settings.visibleFields } private fun header(field: LogField) = field.name.lowercase().replace('_', ' ') - private fun htmlValue(field: LogField, entry: NotificationLogEntry, settings: LogViewSettings, imagePath: String?): String { + private fun htmlValue(field: LogField, entry: NotificationLogEntry, settings: LogViewSettings, imagePath: String?, context: Context?): String { val value = when (field) { - LogField.TIMESTAMP -> timestamp(entry.recordedAtEpochMillis, settings.timestampZone, entry.eventTimeZoneId) + LogField.TIMESTAMP -> timestamp(context, entry.recordedAtEpochMillis, settings, entry.eventTimeZoneId) LogField.APP_NAME -> entry.appName LogField.PACKAGE_NAME -> entry.packageName LogField.ACTION -> entry.action.name.lowercase().replace('_', ' ') @@ -84,13 +84,8 @@ object LogExporter { escaped } } - private fun timestamp(value: Long, zone: TimestampZone, eventTimeZoneId: String?): String = DateFormat.getDateTimeInstance().apply { - timeZone = when (zone) { - TimestampZone.UTC -> TimeZone.getTimeZone("UTC") - TimestampZone.LOCAL_NOW -> TimeZone.getDefault() - TimestampZone.EVENT_LOCAL -> eventTimeZoneId?.let(TimeZone::getTimeZone) ?: TimeZone.getDefault() - } - }.format(Date(value)) + private fun timestamp(context: Context?, value: Long, settings: LogViewSettings, eventTimeZoneId: String?): String = + TimestampFormatter.format(context, value, settings, eventTimeZoneId) private fun csvLine(values: List): String = values.joinToString(",") { csvValue(it) } private fun csvValue(value: String) = "\"${value.replace("\"", "\"\"")}\"" private fun escape(value: String) = value.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """) diff --git a/app/src/main/java/se/ajpanton/notificationlog/settings/LogViewSettings.kt b/app/src/main/java/se/ajpanton/notificationlog/settings/LogViewSettings.kt index a92b957..3622602 100644 --- a/app/src/main/java/se/ajpanton/notificationlog/settings/LogViewSettings.kt +++ b/app/src/main/java/se/ajpanton/notificationlog/settings/LogViewSettings.kt @@ -2,11 +2,15 @@ package se.ajpanton.notificationlog.settings enum class LogField { TIMESTAMP, APP_NAME, PACKAGE_NAME, ACTION, CONTENTS } enum class TimestampZone { LOCAL_NOW, UTC, EVENT_LOCAL } +enum class TimestampDateFormat { SYSTEM_DEFAULT, YEAR_MONTH_DAY, DAY_MONTH_YEAR, MONTH_DAY_YEAR } +enum class TimestampClockFormat { SYSTEM_DEFAULT, HOUR_24, HOUR_12 } enum class DisplayEvent { APPEARING, DISAPPEARING, EDITS } data class LogViewSettings( val visibleFields: Set = setOf(LogField.TIMESTAMP, LogField.APP_NAME, LogField.ACTION, LogField.CONTENTS), val order: List = LogField.entries, val timestampZone: TimestampZone = TimestampZone.LOCAL_NOW, + val timestampDateFormat: TimestampDateFormat = TimestampDateFormat.SYSTEM_DEFAULT, + val timestampClockFormat: TimestampClockFormat = TimestampClockFormat.SYSTEM_DEFAULT, val visibleEvents: Set = DisplayEvent.entries.toSet(), ) diff --git a/app/src/main/java/se/ajpanton/notificationlog/settings/LogViewSettingsStore.kt b/app/src/main/java/se/ajpanton/notificationlog/settings/LogViewSettingsStore.kt index 155c1c0..b9e155f 100644 --- a/app/src/main/java/se/ajpanton/notificationlog/settings/LogViewSettingsStore.kt +++ b/app/src/main/java/se/ajpanton/notificationlog/settings/LogViewSettingsStore.kt @@ -10,6 +10,8 @@ class LogViewSettingsStore(context: Context) { .map(LogField::valueOf).toSet(), order = prefs.getString("order", null)?.split(',')?.map(LogField::valueOf) ?: LogField.entries, timestampZone = TimestampZone.valueOf(prefs.getString("zone", TimestampZone.LOCAL_NOW.name)!!), + timestampDateFormat = TimestampDateFormat.valueOf(prefs.getString("date_format", TimestampDateFormat.SYSTEM_DEFAULT.name)!!), + timestampClockFormat = TimestampClockFormat.valueOf(prefs.getString("clock_format", TimestampClockFormat.SYSTEM_DEFAULT.name)!!), visibleEvents = prefs.getStringSet("visible_events", DisplayEvent.entries.map { it.name }.toSet())!! .map(DisplayEvent::valueOf).toSet(), ) @@ -17,6 +19,8 @@ class LogViewSettingsStore(context: Context) { putStringSet("visible", settings.visibleFields.map { it.name }.toSet()) putString("order", settings.order.joinToString(",") { it.name }) putString("zone", settings.timestampZone.name) + putString("date_format", settings.timestampDateFormat.name) + putString("clock_format", settings.timestampClockFormat.name) putStringSet("visible_events", settings.visibleEvents.map { it.name }.toSet()) } } diff --git a/app/src/main/java/se/ajpanton/notificationlog/settings/TimestampFormatter.kt b/app/src/main/java/se/ajpanton/notificationlog/settings/TimestampFormatter.kt new file mode 100644 index 0000000..c4e0ba3 --- /dev/null +++ b/app/src/main/java/se/ajpanton/notificationlog/settings/TimestampFormatter.kt @@ -0,0 +1,42 @@ +package se.ajpanton.notificationlog.settings + +import android.content.Context +import android.text.format.DateFormat as AndroidDateFormat +import java.text.DateFormat +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import java.util.TimeZone + +object TimestampFormatter { + fun format(context: Context?, value: Long, settings: LogViewSettings, eventTimeZoneId: String?): String { + val date = Date(value) + val timeZone = when (settings.timestampZone) { + TimestampZone.UTC -> TimeZone.getTimeZone("UTC") + TimestampZone.LOCAL_NOW -> TimeZone.getDefault() + TimestampZone.EVENT_LOCAL -> eventTimeZoneId?.let(TimeZone::getTimeZone) ?: TimeZone.getDefault() + } + if (context == null && + settings.timestampDateFormat == TimestampDateFormat.SYSTEM_DEFAULT && + settings.timestampClockFormat == TimestampClockFormat.SYSTEM_DEFAULT + ) { + return DateFormat.getDateTimeInstance().apply { this.timeZone = timeZone }.format(date) + } + return "${dateFormatter(settings.timestampDateFormat, timeZone).format(date)} ${clockFormatter(context, settings.timestampClockFormat, timeZone).format(date)}" + } + + private fun dateFormatter(format: TimestampDateFormat, timeZone: TimeZone): DateFormat = when (format) { + TimestampDateFormat.SYSTEM_DEFAULT -> DateFormat.getDateInstance() + TimestampDateFormat.YEAR_MONTH_DAY -> SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) + TimestampDateFormat.DAY_MONTH_YEAR -> SimpleDateFormat("dd-MM-yyyy", Locale.getDefault()) + TimestampDateFormat.MONTH_DAY_YEAR -> SimpleDateFormat("MM-dd-yyyy", Locale.getDefault()) + }.apply { this.timeZone = timeZone } + + private fun clockFormatter(context: Context?, format: TimestampClockFormat, timeZone: TimeZone): DateFormat = when (format) { + TimestampClockFormat.SYSTEM_DEFAULT -> context?.let { + SimpleDateFormat(if (AndroidDateFormat.is24HourFormat(it)) "HH:mm:ss" else "hh:mm:ss a", Locale.getDefault()) + } ?: DateFormat.getTimeInstance() + TimestampClockFormat.HOUR_24 -> SimpleDateFormat("HH:mm:ss", Locale.getDefault()) + TimestampClockFormat.HOUR_12 -> SimpleDateFormat("hh:mm:ss a", Locale.getDefault()) + }.apply { this.timeZone = timeZone } +} diff --git a/app/src/main/res/layout/fragment_log_display.xml b/app/src/main/res/layout/fragment_log_display.xml index 32bd56d..1ce288a 100644 --- a/app/src/main/res/layout/fragment_log_display.xml +++ b/app/src/main/res/layout/fragment_log_display.xml @@ -5,7 +5,5 @@ - - diff --git a/app/src/test/java/se/ajpanton/notificationlog/export/LogExporterTest.kt b/app/src/test/java/se/ajpanton/notificationlog/export/LogExporterTest.kt index b1b8052..c6b5d47 100644 --- a/app/src/test/java/se/ajpanton/notificationlog/export/LogExporterTest.kt +++ b/app/src/test/java/se/ajpanton/notificationlog/export/LogExporterTest.kt @@ -7,6 +7,9 @@ import org.junit.Test import se.ajpanton.notificationlog.model.NotificationAction import se.ajpanton.notificationlog.model.NotificationLogEntry import se.ajpanton.notificationlog.settings.LogViewSettings +import se.ajpanton.notificationlog.settings.TimestampClockFormat +import se.ajpanton.notificationlog.settings.TimestampDateFormat +import se.ajpanton.notificationlog.settings.TimestampZone class LogExporterTest { private val imageEntry = NotificationLogEntry( @@ -51,4 +54,14 @@ class LogExporterTest { assertEquals(LogExporter.formatted(listOf(imageEntry), settings), formatted) assertEquals(LogExporter.html(listOf(imageEntry), settings) { "images/${it.imageId}.png" }, html) } + + @Test fun `exports use the configured timestamp date and clock formats`() { + val settings = LogViewSettings( + timestampZone = TimestampZone.UTC, + timestampDateFormat = TimestampDateFormat.YEAR_MONTH_DAY, + timestampClockFormat = TimestampClockFormat.HOUR_24, + ) + + assertTrue(LogExporter.csv(listOf(imageEntry), settings).contains("1970-01-01 00:00:00")) + } }