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)
This commit is contained in:
José Rebelo 2024-10-26 11:53:20 +01:00
parent a46e970f84
commit c628ce2c97
4 changed files with 47 additions and 18 deletions

View File

@ -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();

View File

@ -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())));

View File

@ -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);

View File

@ -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,