From bbee80dbd0a43809221f2de0f0070f8c6df7aee6 Mon Sep 17 00:00:00 2001 From: ravi-signal <99042880+ravi-signal@users.noreply.github.com> Date: Thu, 29 May 2025 18:14:23 -0500 Subject: [PATCH] Fix class cast exceptions with SchedulingUtil --- .../scheduler/SchedulingUtil.java | 25 ++++++++++-- .../scheduler/SchedulingUtilTest.java | 39 +++++++++++++++++++ 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/scheduler/SchedulingUtil.java b/service/src/main/java/org/whispersystems/textsecuregcm/scheduler/SchedulingUtil.java index 579dc0391..e1c688d6b 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/scheduler/SchedulingUtil.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/scheduler/SchedulingUtil.java @@ -5,16 +5,21 @@ import com.google.i18n.phonenumbers.NumberParseException; import com.google.i18n.phonenumbers.PhoneNumberToTimeZonesMapper; import com.google.i18n.phonenumbers.PhoneNumberUtil; import com.google.i18n.phonenumbers.Phonenumber; -import org.whispersystems.textsecuregcm.storage.Account; import java.time.Clock; import java.time.Instant; import java.time.LocalTime; import java.time.ZoneId; import java.time.ZoneOffset; import java.time.ZonedDateTime; +import java.time.zone.ZoneRules; +import java.util.Comparator; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.whispersystems.textsecuregcm.storage.Account; public class SchedulingUtil { @@ -70,7 +75,10 @@ public class SchedulingUtil { return Optional.empty(); } - final List sortedZoneOffsets = timeZonesForNumber + final Instant now = clock.instant(); + + // Consider each unique ZoneRules and pick an arbitrary representative ZoneId for it + final Map byOffset = timeZonesForNumber .stream() .map(id -> { try { @@ -80,10 +88,19 @@ public class SchedulingUtil { } }) .filter(Objects::nonNull) - .sorted() + .collect(Collectors.toMap( + ZoneId::getRules, + Function.identity(), + (v1, v2) -> v2)); + + // Sort the ZoneRules by the offsets they produce + final List zoneRulesSortedByOffset = byOffset.keySet() + .stream() + .sorted(Comparator.comparing(z -> z.getOffset(now))) .toList(); - return Optional.of(sortedZoneOffsets.get(sortedZoneOffsets.size() / 2)); + // Select the "middle" ZoneRule and return one of the ZoneIds that have that ZoneRule + return Optional.of(byOffset.get(zoneRulesSortedByOffset.get(zoneRulesSortedByOffset.size() / 2))); } catch (final NumberParseException e) { return Optional.empty(); } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/scheduler/SchedulingUtilTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/scheduler/SchedulingUtilTest.java index ee7631d62..ee95cc976 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/scheduler/SchedulingUtilTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/scheduler/SchedulingUtilTest.java @@ -4,16 +4,23 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import com.google.i18n.phonenumbers.PhoneNumberToTimeZonesMapper; import com.google.i18n.phonenumbers.PhoneNumberUtil; import java.time.Clock; +import java.time.Instant; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.ZoneId; import java.time.ZoneOffset; import java.time.ZonedDateTime; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import com.google.i18n.phonenumbers.Phonenumber; import org.junit.jupiter.api.Test; import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.Device; +import org.whispersystems.textsecuregcm.util.TestClock; class SchedulingUtilTest { @@ -89,4 +96,36 @@ class SchedulingUtilTest { SchedulingUtil.getNextRecommendedNotificationTime(account, LocalTime.of(14, 0), Clock.fixed(afterNotificationTime.toInstant(berlineZoneOffset), berlinZoneId))); } + + @Test + void zoneIdSelectionSingleOffset() { + final Account account = mock(Account.class); + final Phonenumber.PhoneNumber phoneNumber = PhoneNumberUtil.getInstance().getExampleNumber("DE"); + + when(account.getNumber()) + .thenReturn(PhoneNumberUtil.getInstance().format(phoneNumber , PhoneNumberUtil.PhoneNumberFormat.E164)); + + final Instant now = Instant.now(); + + assertEquals( + ZoneId.of("Europe/Berlin").getRules().getOffset(now), + SchedulingUtil.getZoneId(account, TestClock.pinned(now)).orElseThrow().getRules().getOffset(now)); + } + + @Test + void zoneIdSelectionMultipleOffsets() { + final Account account = mock(Account.class); + + // A US VOIP number spans multiple time zones, we should pick a 'middle' one + final Phonenumber.PhoneNumber phoneNumber = + PhoneNumberUtil.getInstance().getExampleNumberForType("US", PhoneNumberUtil.PhoneNumberType.VOIP); + when(account.getNumber()) + .thenReturn(PhoneNumberUtil.getInstance().format(phoneNumber, PhoneNumberUtil.PhoneNumberFormat.E164)); + + final Instant now = Instant.now(); + + assertEquals( + ZoneId.of("America/Chicago").getRules().getOffset(now), + SchedulingUtil.getZoneId(account, TestClock.pinned(now)).orElseThrow().getRules().getOffset(now)); + } }