From c628ce2c970cfd7ff98b45ffcec00d21238f76df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Rebelo?= Date: Sat, 26 Oct 2024 11:53:20 +0100 Subject: [PATCH] Garmin: Fix all-day events As per the CalendarContract, the begin timestamp corresponds to the UTC midnight-boundary of the start of the day. - Remove the redundant logic to truncate the date to the start of day - Remove the workaround for negative timezones (confirmed not workin with some EDT users) - Ensure birthdays also respect the UTC boundary - Offset the garmin timestamps by the UTC offset, ensuring they match midnight on the user's timezome (fixes all-day events offset by 1 day) --- .../externalevents/CalendarReceiver.java | 18 ++++++------------ .../devices/garmin/ProtocolBufferHandler.java | 19 +++++++++++++++++-- .../gadgetbridge/util/DateTimeUtils.java | 18 ++++++++++++++++-- .../util/calendar/CalendarManager.java | 10 ++++++++-- 4 files changed, 47 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/CalendarReceiver.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/CalendarReceiver.java index 6c09e6a44..d835c4c64 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/CalendarReceiver.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/CalendarReceiver.java @@ -28,16 +28,13 @@ import android.os.Handler; import android.provider.CalendarContract; import android.widget.Toast; -import androidx.core.content.ContextCompat; import androidx.localbroadcastmanager.content.LocalBroadcastManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; -import java.util.Calendar; import java.util.Enumeration; -import java.util.GregorianCalendar; import java.util.concurrent.TimeUnit; import java.util.Hashtable; import java.util.List; @@ -234,15 +231,12 @@ public class CalendarReceiver extends ContentObserver { calendarEventSpec.timestamp = calendarEvent.getBeginSeconds(); calendarEventSpec.durationInSeconds = calendarEvent.getDurationSeconds(); //FIXME: leads to problems right now if (calendarEvent.isAllDay()) { - //force the all day events to begin at midnight and last N whole days - Calendar c = GregorianCalendar.getInstance(); - int numDays = (int) TimeUnit.DAYS.convert(calendarEvent.getEnd() - calendarEvent.getBegin(), - TimeUnit.MILLISECONDS); - c.setTimeInMillis(calendarEvent.getBegin()); - c.set(Calendar.HOUR_OF_DAY, 0); - //workaround for negative timezones - if (c.getTimeZone().getRawOffset() < 0) c.add(Calendar.DAY_OF_MONTH, 1); - calendarEventSpec.timestamp = (int) (c.getTimeInMillis() / 1000); + // As per the CalendarContract, for all-day events, the start timestamp is always in UTC + // and corresponds to the midnight boundary + final int numDays = (int) TimeUnit.DAYS.convert( + calendarEvent.getEnd() - calendarEvent.getBegin(), + TimeUnit.MILLISECONDS + ); calendarEventSpec.durationInSeconds = 24 * 60 * 60 * numDays; } calendarEventSpec.description = calendarEvent.getDescription(); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/ProtocolBufferHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/ProtocolBufferHandler.java index 14c55d958..566b88c11 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/ProtocolBufferHandler.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/ProtocolBufferHandler.java @@ -41,6 +41,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.GFDI import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.ProtobufMessage; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status.ProtobufStatusMessage; import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.webview.CurrentPosition; +import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; import nodomain.freeyourgadget.gadgetbridge.util.GB; import nodomain.freeyourgadget.gadgetbridge.util.calendar.CalendarEvent; import nodomain.freeyourgadget.gadgetbridge.util.calendar.CalendarManager; @@ -212,11 +213,25 @@ public class ProtocolBufferHandler implements MessageHandler { break; } + final int startDateSeconds; + final int endDateSeconds; + + if (mEvt.isAllDay()) { + // For all-day events, garmin expects the start and end date to match the midnight boundaries + // in the user's timezone. However, the calendar event will have them in the UTC timezone, + // so we need to convert it + startDateSeconds = (int) (DateTimeUtils.utcDateTimeToLocal(mEvt.getBegin()) / 1000); + endDateSeconds = (int) (DateTimeUtils.utcDateTimeToLocal(mEvt.getEnd()) / 1000); + } else { + startDateSeconds = mEvt.getBeginSeconds(); + endDateSeconds = mEvt.getEndSeconds(); + } + final GdiCalendarService.CalendarService.CalendarEvent.Builder event = GdiCalendarService.CalendarService.CalendarEvent.newBuilder() .setTitle(mEvt.getTitle().substring(0, Math.min(mEvt.getTitle().length(), calendarServiceRequest.getMaxTitleLength()))) .setAllDay(mEvt.isAllDay()) - .setStartDate(mEvt.getBeginSeconds()) - .setEndDate(mEvt.getEndSeconds()); + .setStartDate(startDateSeconds) + .setEndDate(endDateSeconds); if (calendarServiceRequest.getIncludeLocation() && mEvt.getLocation() != null) { event.setLocation(mEvt.getLocation().substring(0, Math.min(mEvt.getLocation().length(), calendarServiceRequest.getMaxLocationLength()))); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DateTimeUtils.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DateTimeUtils.java index 652a13e15..e8f2dd58d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DateTimeUtils.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DateTimeUtils.java @@ -140,8 +140,8 @@ public class DateTimeUtils { return ret; } - public static Date dayStart(final LocalDate date) { - final Calendar calendar = Calendar.getInstance(); + public static Date dayStartUtc(final LocalDate date) { + final Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")); calendar.set(Calendar.YEAR, date.getYear()); calendar.set(Calendar.MONTH, date.getMonthValue() - 1); calendar.set(Calendar.DAY_OF_MONTH, date.getDayOfMonth()); @@ -152,6 +152,20 @@ public class DateTimeUtils { return calendar.getTime(); } + public static long utcDateTimeToLocal(final long timestamp) { + final Calendar utcCalendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + utcCalendar.setTimeInMillis(timestamp); + final Calendar localCalendar = Calendar.getInstance(TimeZone.getDefault()); + localCalendar.set(Calendar.YEAR, utcCalendar.get(Calendar.YEAR)); + localCalendar.set(Calendar.MONTH, utcCalendar.get(Calendar.MONTH)); + localCalendar.set(Calendar.DAY_OF_MONTH, utcCalendar.get(Calendar.DAY_OF_MONTH)); + localCalendar.set(Calendar.HOUR_OF_DAY, utcCalendar.get(Calendar.HOUR_OF_DAY)); + localCalendar.set(Calendar.MINUTE, utcCalendar.get(Calendar.MINUTE)); + localCalendar.set(Calendar.SECOND, utcCalendar.get(Calendar.SECOND)); + localCalendar.set(Calendar.MILLISECOND, utcCalendar.get(Calendar.MILLISECOND)); + return localCalendar.getTimeInMillis(); + } + public static Date dayEnd(final Date date) { final Calendar calendar = Calendar.getInstance(); calendar.setTime(date); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/calendar/CalendarManager.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/calendar/CalendarManager.java index a44725c56..c1823dcf5 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/calendar/CalendarManager.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/calendar/CalendarManager.java @@ -35,11 +35,13 @@ import java.time.format.DateTimeParseException; import java.util.ArrayList; import java.util.Calendar; import java.util.Comparator; +import java.util.Date; import java.util.GregorianCalendar; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; +import java.util.TimeZone; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; @@ -212,9 +214,13 @@ public class CalendarManager { continue; } + // Follow the same logic as CalendarContract - all day events have the start + // timestamp at the UTC midnight boundary + final long startTimestampUtc = DateTimeUtils.dayStartUtc(birthday).getTime(); + birthdays.add(new CalendarEvent( - DateTimeUtils.dayStart(birthday).getTime(), - DateTimeUtils.dayStart(birthday).getTime() + 86400000L - 1L, + startTimestampUtc, + startTimestampUtc + 86400000L - 1L, contactId.hashCode(), mContext.getString(R.string.contact_birthday, displayName), null,