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.provider.CalendarContract;
import android.widget.Toast; import android.widget.Toast;
import androidx.core.content.ContextCompat;
import androidx.localbroadcastmanager.content.LocalBroadcastManager; import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Calendar;
import java.util.Enumeration; import java.util.Enumeration;
import java.util.GregorianCalendar;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.Hashtable; import java.util.Hashtable;
import java.util.List; import java.util.List;
@ -234,15 +231,12 @@ public class CalendarReceiver extends ContentObserver {
calendarEventSpec.timestamp = calendarEvent.getBeginSeconds(); calendarEventSpec.timestamp = calendarEvent.getBeginSeconds();
calendarEventSpec.durationInSeconds = calendarEvent.getDurationSeconds(); //FIXME: leads to problems right now calendarEventSpec.durationInSeconds = calendarEvent.getDurationSeconds(); //FIXME: leads to problems right now
if (calendarEvent.isAllDay()) { if (calendarEvent.isAllDay()) {
//force the all day events to begin at midnight and last N whole days // As per the CalendarContract, for all-day events, the start timestamp is always in UTC
Calendar c = GregorianCalendar.getInstance(); // and corresponds to the midnight boundary
int numDays = (int) TimeUnit.DAYS.convert(calendarEvent.getEnd() - calendarEvent.getBegin(), final int numDays = (int) TimeUnit.DAYS.convert(
TimeUnit.MILLISECONDS); calendarEvent.getEnd() - calendarEvent.getBegin(),
c.setTimeInMillis(calendarEvent.getBegin()); TimeUnit.MILLISECONDS
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);
calendarEventSpec.durationInSeconds = 24 * 60 * 60 * numDays; calendarEventSpec.durationInSeconds = 24 * 60 * 60 * numDays;
} }
calendarEventSpec.description = calendarEvent.getDescription(); 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.ProtobufMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status.ProtobufStatusMessage; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status.ProtobufStatusMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.webview.CurrentPosition; 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.GB;
import nodomain.freeyourgadget.gadgetbridge.util.calendar.CalendarEvent; import nodomain.freeyourgadget.gadgetbridge.util.calendar.CalendarEvent;
import nodomain.freeyourgadget.gadgetbridge.util.calendar.CalendarManager; import nodomain.freeyourgadget.gadgetbridge.util.calendar.CalendarManager;
@ -212,11 +213,25 @@ public class ProtocolBufferHandler implements MessageHandler {
break; 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() final GdiCalendarService.CalendarService.CalendarEvent.Builder event = GdiCalendarService.CalendarService.CalendarEvent.newBuilder()
.setTitle(mEvt.getTitle().substring(0, Math.min(mEvt.getTitle().length(), calendarServiceRequest.getMaxTitleLength()))) .setTitle(mEvt.getTitle().substring(0, Math.min(mEvt.getTitle().length(), calendarServiceRequest.getMaxTitleLength())))
.setAllDay(mEvt.isAllDay()) .setAllDay(mEvt.isAllDay())
.setStartDate(mEvt.getBeginSeconds()) .setStartDate(startDateSeconds)
.setEndDate(mEvt.getEndSeconds()); .setEndDate(endDateSeconds);
if (calendarServiceRequest.getIncludeLocation() && mEvt.getLocation() != null) { if (calendarServiceRequest.getIncludeLocation() && mEvt.getLocation() != null) {
event.setLocation(mEvt.getLocation().substring(0, Math.min(mEvt.getLocation().length(), calendarServiceRequest.getMaxLocationLength()))); event.setLocation(mEvt.getLocation().substring(0, Math.min(mEvt.getLocation().length(), calendarServiceRequest.getMaxLocationLength())));

View File

@ -140,8 +140,8 @@ public class DateTimeUtils {
return ret; return ret;
} }
public static Date dayStart(final LocalDate date) { public static Date dayStartUtc(final LocalDate date) {
final Calendar calendar = Calendar.getInstance(); final Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
calendar.set(Calendar.YEAR, date.getYear()); calendar.set(Calendar.YEAR, date.getYear());
calendar.set(Calendar.MONTH, date.getMonthValue() - 1); calendar.set(Calendar.MONTH, date.getMonthValue() - 1);
calendar.set(Calendar.DAY_OF_MONTH, date.getDayOfMonth()); calendar.set(Calendar.DAY_OF_MONTH, date.getDayOfMonth());
@ -152,6 +152,20 @@ public class DateTimeUtils {
return calendar.getTime(); 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) { public static Date dayEnd(final Date date) {
final Calendar calendar = Calendar.getInstance(); final Calendar calendar = Calendar.getInstance();
calendar.setTime(date); calendar.setTime(date);

View File

@ -35,11 +35,13 @@ import java.time.format.DateTimeParseException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Calendar; import java.util.Calendar;
import java.util.Comparator; import java.util.Comparator;
import java.util.Date;
import java.util.GregorianCalendar; import java.util.GregorianCalendar;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.TimeZone;
import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.R;
@ -212,9 +214,13 @@ public class CalendarManager {
continue; 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( birthdays.add(new CalendarEvent(
DateTimeUtils.dayStart(birthday).getTime(), startTimestampUtc,
DateTimeUtils.dayStart(birthday).getTime() + 86400000L - 1L, startTimestampUtc + 86400000L - 1L,
contactId.hashCode(), contactId.hashCode(),
mContext.getString(R.string.contact_birthday, displayName), mContext.getString(R.string.contact_birthday, displayName),
null, null,