From aeb7636d066aff841b423d652e71a0c591edeff7 Mon Sep 17 00:00:00 2001 From: K Ruggiero <49291963+kruggiero97@users.noreply.github.com> Date: Sat, 9 May 2026 17:48:44 -0400 Subject: [PATCH 1/3] fix: use event time zone when expanding recurring occurrences addIntervalTime wasn't using the event's timeZone field at all, it was just defaulting to whatever zone the device was in. Once a recurring event crossed a DST boundary, every following occurrence drifted by an hour, i.e. an event created in ET but read in AST would display the right time before the DST transition and the wrong time after. The fix reads the event's timeZone, falls back to the device default if it's empty or Joda-Time doesn't recognize it, and passes the resolved zone into Formatter.getDateTimeFromTS. Joda already does DST math correctly. Closes #165 --- .../main/kotlin/org/fossify/calendar/models/Event.kt | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/src/main/kotlin/org/fossify/calendar/models/Event.kt b/app/src/main/kotlin/org/fossify/calendar/models/Event.kt index 74312ff04..f44d97ab0 100644 --- a/app/src/main/kotlin/org/fossify/calendar/models/Event.kt +++ b/app/src/main/kotlin/org/fossify/calendar/models/Event.kt @@ -74,7 +74,16 @@ data class Event( } fun addIntervalTime(original: Event) { - val oldStart = Formatter.getDateTimeFromTS(startTS) + val zone = if (timeZone.isNotEmpty()) { + try { + DateTimeZone.forID(timeZone) + } catch (e: IllegalArgumentException) { + DateTimeZone.getDefault() + } + } else { + DateTimeZone.getDefault() + } + val oldStart = Formatter.getDateTimeFromTS(startTS, zone) val newStart = when (repeatInterval) { DAY -> oldStart.plusDays(1) else -> { From 2772bf1e860a02294c7652dc3438df0ae043b645 Mon Sep 17 00:00:00 2001 From: K Ruggiero <49291963+kruggiero97@users.noreply.github.com> Date: Sat, 9 May 2026 18:51:41 -0400 Subject: [PATCH 2/3] docs(changelog): note recurrence timezone fix --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 73a6fc1f1..7bfc75952 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed event text readability on colored backgrounds ([#1065]) - Fixed invisible current time indicator in weekly view ([#99]) - Fixed stuck zoom level in weekly view on some devices ([#621]) +- Fixed recurring events drifting by an hour after DST transitions ([#165]) ## [1.10.3] - 2026-02-14 ### Changed @@ -217,6 +218,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#135]: https://github.com/FossifyOrg/Calendar/issues/135 [#138]: https://github.com/FossifyOrg/Calendar/issues/138 [#148]: https://github.com/FossifyOrg/Calendar/issues/148 +[#165]: https://github.com/FossifyOrg/Calendar/issues/165 [#196]: https://github.com/FossifyOrg/Calendar/issues/196 [#217]: https://github.com/FossifyOrg/Calendar/issues/217 [#262]: https://github.com/FossifyOrg/Calendar/issues/262 From 948b923c99ba56f6ecdb37c63692af18968eb526 Mon Sep 17 00:00:00 2001 From: K Ruggiero <49291963+kruggiero97@users.noreply.github.com> Date: Sat, 9 May 2026 19:35:44 -0400 Subject: [PATCH 3/3] refactor: extract timezone resolution into a helper, log unrecognized zones --- .../org/fossify/calendar/models/Event.kt | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/app/src/main/kotlin/org/fossify/calendar/models/Event.kt b/app/src/main/kotlin/org/fossify/calendar/models/Event.kt index f44d97ab0..8654c9b57 100644 --- a/app/src/main/kotlin/org/fossify/calendar/models/Event.kt +++ b/app/src/main/kotlin/org/fossify/calendar/models/Event.kt @@ -2,6 +2,7 @@ package org.fossify.calendar.models import android.provider.CalendarContract import android.provider.CalendarContract.Attendees +import android.util.Log import androidx.collection.LongSparseArray import androidx.room.ColumnInfo import androidx.room.Entity @@ -74,16 +75,7 @@ data class Event( } fun addIntervalTime(original: Event) { - val zone = if (timeZone.isNotEmpty()) { - try { - DateTimeZone.forID(timeZone) - } catch (e: IllegalArgumentException) { - DateTimeZone.getDefault() - } - } else { - DateTimeZone.getDefault() - } - val oldStart = Formatter.getDateTimeFromTS(startTS, zone) + val oldStart = Formatter.getDateTimeFromTS(startTS, resolveTimeZone()) val newStart = when (repeatInterval) { DAY -> oldStart.plusDays(1) else -> { @@ -118,6 +110,20 @@ data class Event( endTS = newEndTS } + // Returns the event's time zone, or the device default if unset or unrecognized. + private fun resolveTimeZone(): DateTimeZone { + return if (timeZone.isNotEmpty()) { + try { + DateTimeZone.forID(timeZone) + } catch (e: IllegalArgumentException) { + Log.w("Event", "Unrecognized timezone '$timeZone', using default", e) + DateTimeZone.getDefault() + } + } else { + DateTimeZone.getDefault() + } + } + // if an event should happen on 29th Feb. with Same Day yearly repetition, show it only on leap years private fun addYearsWithSameDay(currStart: DateTime): DateTime { var newDateTime = currStart.plusYears(repeatInterval / YEAR)