Skip to content

Commit 24f07c1

Browse files
committed
Fix jump-to-date being off by one day in non-UTC timezones
MaterialDatePicker reports the selected day, the calendar constraints, and the validator callbacks in terms of UTC midnight. When reading those values back, both JumpToDateValidator.normalizeToLocalMidnight() and the date picker's positive-button handler converted the UTC-midnight instant using the local zone: Instant.ofEpochMilli(timestamp).atZone(ZoneId.systemDefault()).toLocalDate() For any negative UTC offset, the UTC-midnight instant of calendar day D falls on the previous evening locally, so toLocalDate() yields D-1. As a result a message sent on local day D became selectable under picker day D+1 (the reported "selectable dates are one day ahead" symptom), and the positive-button handler shifted the jump target by a day as well. Read the calendar day in UTC (matching how MaterialDatePicker emits it), then re-anchor to local midnight, which is how message days are keyed in the database (messageExistsOnDays). Add a regression test exercising a negative-offset zone (America/New_York); it fails before this change and passes after.
1 parent ec02d80 commit 24f07c1

3 files changed

Lines changed: 29 additions & 2 deletions

File tree

app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,7 @@ import org.thoughtcrime.securesms.wallpaper.ChatWallpaperDimLevelUtil
390390
import java.time.Instant
391391
import java.time.LocalDateTime
392392
import java.time.ZoneId
393+
import java.time.ZoneOffset
393394
import java.util.Locale
394395
import java.util.Optional
395396
import java.util.concurrent.ExecutionException
@@ -5247,7 +5248,7 @@ class ConversationFragment :
52475248
datePicker.addOnPositiveButtonClickListener { selectedDate ->
52485249
if (selectedDate != null) {
52495250
val localMidnightTimestamp = Instant.ofEpochMilli(selectedDate)
5250-
.atZone(ZoneId.systemDefault())
5251+
.atZone(ZoneOffset.UTC)
52515252
.toLocalDate()
52525253
.atStartOfDay(ZoneId.systemDefault())
52535254
.toInstant()

app/src/main/java/org/thoughtcrime/securesms/conversation/v2/JumpToDateValidator.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import java.time.Instant
1616
import java.time.LocalDate
1717
import java.time.LocalDateTime
1818
import java.time.ZoneId
19+
import java.time.ZoneOffset
1920
import java.time.temporal.TemporalAdjusters
2021
import java.util.concurrent.Executor
2122
import kotlin.time.Duration.Companion.days
@@ -105,7 +106,7 @@ class JumpToDateValidator private constructor(
105106

106107
private fun normalizeToLocalMidnight(timestamp: Long): Long {
107108
return Instant.ofEpochMilli(timestamp)
108-
.atZone(zoneId)
109+
.atZone(ZoneOffset.UTC)
109110
.toLocalDate()
110111
.atStartOfDay(zoneId)
111112
.toInstant()

app/src/test/java/org/thoughtcrime/securesms/conversation/v2/JumpToDateValidatorTest.kt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,4 +187,29 @@ class JumpToDateValidatorTest {
187187

188188
assertThat(validator.isValid(june15Utc)).isTrue()
189189
}
190+
191+
@Test
192+
fun `picker date maps to the same calendar day in a negative-offset zone`() {
193+
val newYorkZone = ZoneId.of("America/New_York")
194+
195+
// The database keys days by local midnight. A message sent on June 15 (New York) lives under
196+
// New York's midnight for that day.
197+
val june15NewYorkMidnight = LocalDate.of(2024, 6, 15)
198+
.atStartOfDay(newYorkZone)
199+
.toInstant()
200+
.toEpochMilli()
201+
202+
val lookup = { dates: Collection<Long> ->
203+
dates.associateWith { it == june15NewYorkMidnight }
204+
}
205+
val validator = createValidator(lookup, zone = newYorkZone)
206+
207+
// MaterialDatePicker reports the selected June 15 cell as UTC midnight. The validator must map
208+
// it to New York's June 15 (not June 14, which the off-by-one bug produced for negative offsets).
209+
val june15PickerUtc = timestampForDate(2024, 6, 15)
210+
211+
validator.isValid(june15PickerUtc)
212+
213+
assertThat(validator.isValid(june15PickerUtc)).isTrue()
214+
}
190215
}

0 commit comments

Comments
 (0)