From 0091eb5d49fe4990806afcb60792b9d90bee9e7b Mon Sep 17 00:00:00 2001 From: ajp_anton Date: Thu, 23 Jul 2026 10:51:51 +0000 Subject: [PATCH] Preserve event-local timestamp timezone --- .../se/ajpanton/notificationlog/ViewLogsFragment.kt | 10 +++++++--- .../capture/NotificationCaptureService.kt | 1 + .../notificationlog/data/NotificationLogEntryJson.kt | 2 ++ .../ajpanton/notificationlog/export/LogExporter.kt | 12 +++++++++--- .../notificationlog/model/NotificationLogEntry.kt | 2 ++ 5 files changed, 21 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/se/ajpanton/notificationlog/ViewLogsFragment.kt b/app/src/main/java/se/ajpanton/notificationlog/ViewLogsFragment.kt index 8adf6a5..7d6a3fd 100644 --- a/app/src/main/java/se/ajpanton/notificationlog/ViewLogsFragment.kt +++ b/app/src/main/java/se/ajpanton/notificationlog/ViewLogsFragment.kt @@ -77,7 +77,7 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) { entries.forEach { entry -> view.logRows.addView(TextView(requireContext()).apply { text = settings.order.filter { it in settings.visibleFields }.mapNotNull { field -> when (field) { - LogField.TIMESTAMP -> timestamp(entry.recordedAtEpochMillis, settings.timestampZone) + LogField.TIMESTAMP -> timestamp(entry.recordedAtEpochMillis, settings.timestampZone, entry.eventTimeZoneId) LogField.APP_NAME -> entry.appName LogField.PACKAGE_NAME -> entry.packageName LogField.ACTION -> entry.action.name.lowercase().replace('_', ' ') @@ -95,9 +95,13 @@ class ViewLogsFragment : Fragment(R.layout.fragment_view_logs) { } } - private fun timestamp(value: Long, zone: TimestampZone): String { + private fun timestamp(value: Long, zone: TimestampZone, eventTimeZoneId: String?): String { val format = DateFormat.getDateTimeInstance() - if (zone == TimestampZone.UTC) format.timeZone = java.util.TimeZone.getTimeZone("UTC") + format.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() + } return format.format(Date(value)) } diff --git a/app/src/main/java/se/ajpanton/notificationlog/capture/NotificationCaptureService.kt b/app/src/main/java/se/ajpanton/notificationlog/capture/NotificationCaptureService.kt index d390895..d9ade2e 100644 --- a/app/src/main/java/se/ajpanton/notificationlog/capture/NotificationCaptureService.kt +++ b/app/src/main/java/se/ajpanton/notificationlog/capture/NotificationCaptureService.kt @@ -81,6 +81,7 @@ class NotificationCaptureService : NotificationListenerService() { NotificationRuleEvaluator.allows(ruleStore.ruleFor(LoggingType.IMAGE_CONTENT), snapshot.packageName) val entry = NotificationLogEntry( recordedAtEpochMillis = System.currentTimeMillis(), + eventTimeZoneId = java.util.TimeZone.getDefault().id, packageName = snapshot.packageName, appName = appName, action = action, diff --git a/app/src/main/java/se/ajpanton/notificationlog/data/NotificationLogEntryJson.kt b/app/src/main/java/se/ajpanton/notificationlog/data/NotificationLogEntryJson.kt index f7f3c58..d2b142d 100644 --- a/app/src/main/java/se/ajpanton/notificationlog/data/NotificationLogEntryJson.kt +++ b/app/src/main/java/se/ajpanton/notificationlog/data/NotificationLogEntryJson.kt @@ -13,6 +13,7 @@ object NotificationLogEntryJson { JSONObject() .put("id", entry.id) .put("recordedAtEpochMillis", entry.recordedAtEpochMillis) + .put("eventTimeZoneId", entry.eventTimeZoneId) .put("packageName", entry.packageName) .put("appName", entry.appName) .put("action", entry.action.name) @@ -29,6 +30,7 @@ object NotificationLogEntryJson { NotificationLogEntry( id = entry.getString("id"), recordedAtEpochMillis = entry.getLong("recordedAtEpochMillis"), + eventTimeZoneId = entry.optString("eventTimeZoneId").takeIf { it.isNotEmpty() }, packageName = entry.getString("packageName"), appName = entry.getString("appName"), action = NotificationAction.valueOf(entry.getString("action")), 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 ab98054..1a42c02 100644 --- a/app/src/main/java/se/ajpanton/notificationlog/export/LogExporter.kt +++ b/app/src/main/java/se/ajpanton/notificationlog/export/LogExporter.kt @@ -40,7 +40,7 @@ object LogExporter { settings.order.filter { it in settings.visibleFields }.map(::header), ) + entries.sortedByDescending { it.recordedAtEpochMillis }.map { entry -> settings.order.filter { it in settings.visibleFields }.map { field -> when (field) { - LogField.TIMESTAMP -> timestamp(entry.recordedAtEpochMillis, settings.timestampZone) + LogField.TIMESTAMP -> timestamp(entry.recordedAtEpochMillis, settings.timestampZone, entry.eventTimeZoneId) LogField.APP_NAME -> entry.appName; LogField.PACKAGE_NAME -> entry.packageName LogField.ACTION -> entry.action.name.lowercase().replace('_', ' ') LogField.CONTENTS -> entry.contents ?: "" @@ -49,7 +49,7 @@ object LogExporter { private fun header(field: LogField) = field.name.lowercase().replace('_', ' ') private fun htmlValue(field: LogField, entry: NotificationLogEntry, settings: LogViewSettings, imagePath: String?): String { val value = when (field) { - LogField.TIMESTAMP -> timestamp(entry.recordedAtEpochMillis, settings.timestampZone) + LogField.TIMESTAMP -> timestamp(entry.recordedAtEpochMillis, settings.timestampZone, entry.eventTimeZoneId) LogField.APP_NAME -> entry.appName LogField.PACKAGE_NAME -> entry.packageName LogField.ACTION -> entry.action.name.lowercase().replace('_', ' ') @@ -62,7 +62,13 @@ object LogExporter { escaped } } - private fun timestamp(value: Long, zone: TimestampZone): String = DateFormat.getDateTimeInstance().apply { if (zone == TimestampZone.UTC) timeZone = TimeZone.getTimeZone("UTC") }.format(Date(value)) + 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 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/model/NotificationLogEntry.kt b/app/src/main/java/se/ajpanton/notificationlog/model/NotificationLogEntry.kt index d941fb7..5355fda 100644 --- a/app/src/main/java/se/ajpanton/notificationlog/model/NotificationLogEntry.kt +++ b/app/src/main/java/se/ajpanton/notificationlog/model/NotificationLogEntry.kt @@ -6,6 +6,8 @@ import java.util.UUID data class NotificationLogEntry( val id: String = UUID.randomUUID().toString(), val recordedAtEpochMillis: Long, + /** The device timezone at capture time, retained for event-local display/export. */ + val eventTimeZoneId: String? = null, val packageName: String, val appName: String, val action: NotificationAction,