Skip to content

Commit f410223

Browse files
committed
fix(android): normalize log timestamps to device local timezone in Logs screen
1 parent 0dade75 commit f410223

1 file changed

Lines changed: 59 additions & 2 deletions

File tree

android/app/src/main/java/com/masterdns/vpn/util/VpnManager.kt

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ import kotlinx.coroutines.launch
1414
import kotlinx.coroutines.flow.MutableStateFlow
1515
import kotlinx.coroutines.flow.StateFlow
1616
import kotlinx.coroutines.flow.asStateFlow
17+
import java.text.SimpleDateFormat
18+
import java.util.Date
19+
import java.util.Locale
20+
import java.util.TimeZone
1721

1822
/**
1923
* Singleton bridge between Kotlin UI and Go core.
@@ -78,13 +82,14 @@ object VpnManager {
7882
}
7983

8084
fun appendLog(line: String) {
85+
val normalizedLine = normalizeLogTimestampToLocal(line)
8186
val current = _logs.value.toMutableList()
82-
current.add(line)
87+
current.add(normalizedLine)
8388
if (current.size > MAX_LOG_LINES) {
8489
current.removeAt(0)
8590
}
8691
_logs.value = current
87-
parseScanLine(line)
92+
parseScanLine(normalizedLine)
8893
}
8994

9095
fun clearLogs() {
@@ -212,4 +217,56 @@ object VpnManager {
212217
_scanStatus.value = _scanStatus.value.copy(scanning = false)
213218
}
214219
}
220+
221+
private fun normalizeLogTimestampToLocal(line: String): String {
222+
val candidates = listOf(
223+
// Example: 2026-04-05T10:20:30.123Z
224+
Triple(
225+
Regex("^(\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z)(.*)$"),
226+
"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'",
227+
"yyyy-MM-dd HH:mm:ss.SSS"
228+
),
229+
// Example: 2026-04-05T10:20:30Z
230+
Triple(
231+
Regex("^(\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z)(.*)$"),
232+
"yyyy-MM-dd'T'HH:mm:ss'Z'",
233+
"yyyy-MM-dd HH:mm:ss"
234+
),
235+
// Example: 2026-04-05 10:20:30 UTC
236+
Triple(
237+
Regex("^(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2})\\s+UTC(.*)$"),
238+
"yyyy-MM-dd HH:mm:ss",
239+
"yyyy-MM-dd HH:mm:ss"
240+
)
241+
)
242+
243+
for ((regex, inputFormat, outputFormat) in candidates) {
244+
val match = regex.find(line) ?: continue
245+
val utcStamp = match.groupValues[1]
246+
val suffix = match.groupValues[2]
247+
val localStamp = convertUtcToLocal(utcStamp, inputFormat, outputFormat) ?: continue
248+
return "$localStamp$suffix"
249+
}
250+
return line
251+
}
252+
253+
private fun convertUtcToLocal(
254+
utcValue: String,
255+
inputPattern: String,
256+
outputPattern: String
257+
): String? {
258+
return try {
259+
val input = SimpleDateFormat(inputPattern, Locale.US).apply {
260+
timeZone = TimeZone.getTimeZone("UTC")
261+
isLenient = false
262+
}
263+
val parsed: Date = input.parse(utcValue) ?: return null
264+
val output = SimpleDateFormat(outputPattern, Locale.US).apply {
265+
timeZone = TimeZone.getDefault()
266+
}
267+
output.format(parsed)
268+
} catch (_: Exception) {
269+
null
270+
}
271+
}
215272
}

0 commit comments

Comments
 (0)