diff --git a/app/src/main/java/com/nextcloud/utils/extensions/TemporalExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/TemporalExtensions.kt new file mode 100644 index 000000000000..6260194363f3 --- /dev/null +++ b/app/src/main/java/com/nextcloud/utils/extensions/TemporalExtensions.kt @@ -0,0 +1,22 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.utils.extensions + +import java.time.* +import java.time.temporal.Temporal + +fun Temporal?.toInstant(): Instant = when (this) { + is Instant -> this + is ZonedDateTime -> toInstant() + is OffsetDateTime -> toInstant() + is LocalDateTime -> atZone(ZoneOffset.UTC).toInstant() + is LocalDate -> atStartOfDay(ZoneOffset.UTC).toInstant() + else -> throw IllegalArgumentException("Unsupported temporal type") +} + +fun Temporal?.toEpochMilli(): Long = toInstant().toEpochMilli() diff --git a/app/src/main/java/third_parties/sufficientlysecure/ProcessVEvent.java b/app/src/main/java/third_parties/sufficientlysecure/ProcessVEvent.java index ad799ffe971e..0e85637607fa 100644 --- a/app/src/main/java/third_parties/sufficientlysecure/ProcessVEvent.java +++ b/app/src/main/java/third_parties/sufficientlysecure/ProcessVEvent.java @@ -1,6 +1,7 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2015 Jon Griffiths (jon_p_griffiths@yahoo.com) * SPDX-FileCopyrightText: 2013 Dominik Schürmann * SPDX-FileCopyrightText: 2010-2011 Lukas Aichbauer @@ -23,11 +24,11 @@ import android.text.format.DateUtils; import com.nextcloud.client.preferences.AppPreferences; +import com.nextcloud.utils.extensions.TemporalExtensionsKt; import com.owncloud.android.R; import com.owncloud.android.lib.common.utils.Log_OC; import net.fortuna.ical4j.model.Calendar; -import net.fortuna.ical4j.model.DateTime; import net.fortuna.ical4j.model.Parameter; import net.fortuna.ical4j.model.Property; import net.fortuna.ical4j.model.component.CalendarComponent; @@ -37,17 +38,25 @@ import net.fortuna.ical4j.model.parameter.Related; import net.fortuna.ical4j.model.property.Action; import net.fortuna.ical4j.model.property.DateProperty; +import net.fortuna.ical4j.model.property.DtEnd; +import net.fortuna.ical4j.model.property.DtStart; import net.fortuna.ical4j.model.property.Duration; import net.fortuna.ical4j.model.property.FreeBusy; import net.fortuna.ical4j.model.property.Transp; import net.fortuna.ical4j.model.property.Trigger; +import net.fortuna.ical4j.model.property.immutable.ImmutableAction; +import net.fortuna.ical4j.model.property.immutable.ImmutableTransp; import java.net.URI; +import java.time.LocalDate; +import java.time.ZonedDateTime; +import java.time.temporal.Temporal; import java.time.temporal.TemporalAmount; import java.time.temporal.TemporalUnit; import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.UUID; import javax.inject.Inject; @@ -110,13 +119,8 @@ private boolean getTestFileSupport() { } public DuplicateHandlingEnum getDuplicateHandling() { -// return DuplicateHandlingEnum.values()[getEnumInt(PREF_DUPLICATE_HANDLING, 0)]; return DuplicateHandlingEnum.values()[0]; // TODO is option needed? } - -// private int getEnumInt(final String key, final int def) { -// return Integer.parseInt(getString(key, String.valueOf(def))); -// } } public ProcessVEvent(Context context, Calendar iCalCalendar, AndroidCalendar selectedCal, boolean isInserter) { @@ -275,7 +279,8 @@ private ContentValues convertToDB(VEvent e, Options options, reminders.clear(); boolean allDay = false; - boolean startIsDate = !(e.getStartDate().getDate() instanceof DateTime); + Temporal startTemporal = e.getStartDate().map(DtStart::getDate).orElse(null); + boolean startIsDate = startTemporal instanceof LocalDate; boolean isRecurring = hasProperty(e, Property.RRULE) || hasProperty(e, Property.RDATE); if (startIsDate) { @@ -288,25 +293,27 @@ private ContentValues convertToDB(VEvent e, Options options, // No end date or duration given. // Since we added a duration above when the start date is a DATE: // - The start date is a DATETIME, the event lasts no time at all (RFC 2445). - e.getProperties().add(ZERO_SECONDS); + e.add(ZERO_SECONDS); // Zero time events are always free (RFC 2445), so override/set TRANSP accordingly. removeProperty(e, Property.TRANSP); - e.getProperties().add(Transp.TRANSPARENT); + e.add(ImmutableTransp.TRANSPARENT); } if (isRecurring) { // Recurring event. Android insists on a duration. if (!hasProperty(e, Property.DURATION)) { // Calculate duration from start to end date - Duration d = new Duration(e.getStartDate().getDate(), e.getEndDate().getDate()); - e.getProperties().add(d); + Temporal endTemporal = e.getEndDate().map(DtEnd::getDate).orElse(null); + Duration d = new Duration(startTemporal, endTemporal); + e.add(d); } removeProperty(e, Property.DTEND); } else { // Non-recurring event. Android insists on an end date. if (!hasProperty(e, Property.DTEND)) { // Calculate end date from duration, set it and remove the duration. - e.getProperties().add(e.getEndDate()); + Optional> derivedEnd = e.getEndDate(); + derivedEnd.ifPresent(e::add); } removeProperty(e, Property.DURATION); } @@ -331,8 +338,9 @@ private ContentValues convertToDB(VEvent e, Options options, copyProperty(c, Events.EVENT_LOCATION, e, Property.LOCATION); - if (hasProperty(e, Property.STATUS)) { - String status = e.getProperty(Property.STATUS).getValue(); + Optional statusProperty = e.getProperty(Property.STATUS); + if (statusProperty.isPresent()) { + String status = statusProperty.get().getValue(); switch (status) { case "TENTATIVE": c.put(Events.STATUS, Events.STATUS_TENTATIVE); @@ -352,13 +360,14 @@ private ContentValues convertToDB(VEvent e, Options options, c.put(Events.ALL_DAY, 1); } - copyDateProperty(c, Events.DTSTART, Events.EVENT_TIMEZONE, e.getStartDate()); + e.getStartDate().ifPresent(dtStart -> copyDateProperty(c, Events.DTSTART, Events.EVENT_TIMEZONE, dtStart)); if (hasProperty(e, Property.DTEND)) { - copyDateProperty(c, Events.DTEND, Events.EVENT_END_TIMEZONE, e.getEndDate()); + e.getEndDate().ifPresent(dtEnd -> copyDateProperty(c, Events.DTEND, Events.EVENT_END_TIMEZONE, dtEnd)); } - if (hasProperty(e, Property.CLASS)) { - String access = e.getProperty(Property.CLASS).getValue(); + Optional classProperty = e.getProperty(Property.CLASS); + if (classProperty.isPresent()) { + String access = classProperty.get().getValue(); int accessLevel = switch (access) { case "CONFIDENTIAL" -> Events.ACCESS_CONFIDENTIAL; case "PRIVATE" -> Events.ACCESS_PRIVATE; @@ -372,16 +381,17 @@ private ContentValues convertToDB(VEvent e, Options options, // Work out availability. This is confusing as FREEBUSY and TRANSP overlap. int availability = Events.AVAILABILITY_BUSY; if (hasProperty(e, Property.TRANSP)) { - if (e.getTransparency() == Transp.TRANSPARENT) { + Optional transparency = e.getTransparency(); + if (transparency.isPresent() && Transp.VALUE_TRANSPARENT.equals(transparency.get().getValue())) { availability = Events.AVAILABILITY_FREE; } } else if (hasProperty(e, Property.FREEBUSY)) { - FreeBusy fb = e.getProperty(Property.FREEBUSY); - FbType fbType = fb.getParameter(Parameter.FBTYPE); - if (fbType != null && fbType == FbType.FREE) { + Optional fb = e.getProperty(Property.FREEBUSY); + Optional fbType = fb.isPresent() ? fb.get().getParameter(Parameter.FBTYPE) : Optional.empty(); + if (fbType.isPresent() && fbType.get().equals(FbType.FREE)) { availability = Events.AVAILABILITY_FREE; - } else if (fbType != null && fbType == FbType.BUSY_TENTATIVE) { + } else if (fbType.isPresent() && fbType.get().equals(FbType.BUSY_TENTATIVE)) { availability = Events.AVAILABILITY_TENTATIVE; } } @@ -400,23 +410,31 @@ private ContentValues convertToDB(VEvent e, Options options, for (VAlarm a : e.getAlarms()) { - if (a.getAction() != Action.AUDIO && a.getAction() != Action.DISPLAY) { + Optional action = a.getAction(); + if (action.isEmpty() || (!ImmutableAction.AUDIO.equals(action.get()) && !ImmutableAction.DISPLAY.equals(action.get()))) { continue; // Ignore email and procedure alarms } - Trigger t = a.getTrigger(); - final long startMs = e.getStartDate().getDate().getTime(); + Optional triggerProperty = a.getTrigger(); + if (triggerProperty.isEmpty()) { + continue; + } + Trigger t = triggerProperty.get(); + + final long startMs = TemporalExtensionsKt.toEpochMilli(startTemporal); long alarmStartMs = startMs; long alarmMs; // FIXME: - Support for repeating alarms // - Check the calendars max number of alarms - if (t.getDateTime() != null) - alarmMs = t.getDateTime().getTime(); // Absolute + if (t.isAbsolute()) + alarmMs = t.getDate().toEpochMilli(); // Absolute else if (t.getDuration() != null) { - Related rel = t.getParameter(Parameter.RELATED); - if (rel != null && rel == Related.END) - alarmStartMs = e.getEndDate().getDate().getTime(); + Optional rel = t.getParameter(Parameter.RELATED); + if (rel.isPresent() && rel.get().equals(Related.END)) { + Temporal endTemporal = e.getEndDate().map(DtEnd::getDate).orElse(null); + alarmStartMs = TemporalExtensionsKt.toEpochMilli(endTemporal); + } alarmMs = alarmStartMs + durationToMs(t.getDuration()); } else { continue; @@ -452,33 +470,31 @@ private static long durationToMs(TemporalAmount d) { } private boolean hasProperty(VEvent e, String name) { - return e.getProperty(name) != null; + return e.getProperty(name).isPresent(); } private void removeProperty(VEvent e, String name) { - Property p = e.getProperty(name); - if (p != null) { - e.getProperties().remove(p); - } + e.removeAll(name); } private void copyProperty(ContentValues c, String dbName, VEvent e, String evName) { if (dbName != null) { - Property p = e.getProperty(evName); - if (p != null) { - c.put(dbName, p.getValue()); + Optional p = e.getProperty(evName); + if (p.isPresent()) { + c.put(dbName, p.get().getValue()); } } } - private void copyDateProperty(ContentValues c, String dbName, String dbTzName, DateProperty date) { - if (dbName != null && date.getDate() != null) { - c.put(dbName, date.getDate().getTime()); // ms since epoc in GMT + private void copyDateProperty(ContentValues c, String dbName, String dbTzName, DateProperty date) { + Temporal temporal = date.getDate(); + if (dbName != null && temporal != null) { + c.put(dbName, TemporalExtensionsKt.toEpochMilli(temporal)); // ms since epoc in GMT if (dbTzName != null) { - if (date.isUtc() || date.getTimeZone() == null) { + if (date.isUtc() || !(temporal instanceof ZonedDateTime)) { c.put(dbTzName, "UTC"); } else { - c.put(dbTzName, date.getTimeZone().getID()); + c.put(dbTzName, ((ZonedDateTime) temporal).getZone().getId()); } } } @@ -566,10 +582,11 @@ private void checkTestValue(ContentValues c, String keyValue, String testName) { private void processEventTests(VEvent e, ContentValues c, List reminders) { - Property testName = e.getProperty("X-TEST-NAME"); - if (testName == null) { + Optional testNameProperty = e.getProperty("X-TEST-NAME"); + if (testNameProperty.isEmpty()) { return; // Not a test case } + Property testName = testNameProperty.get(); // This is a test event. Verify it using the embedded meta data. Log_OC.i(TAG, "Processing test case " + testName.getValue() + "..."); diff --git a/app/src/main/java/third_parties/sufficientlysecure/SaveCalendar.java b/app/src/main/java/third_parties/sufficientlysecure/SaveCalendar.java index cfa537d11e74..b83a51d5a76f 100644 --- a/app/src/main/java/third_parties/sufficientlysecure/SaveCalendar.java +++ b/app/src/main/java/third_parties/sufficientlysecure/SaveCalendar.java @@ -1,6 +1,7 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2015 Jon Griffiths (jon_p_griffiths@yahoo.com) * SPDX-FileCopyrightText: 2013 Dominik Schürmann * SPDX-FileCopyrightText: 2010-2011 Lukas Aichbauer @@ -24,7 +25,6 @@ import android.provider.CalendarContract.Reminders; import android.text.TextUtils; import android.text.format.DateFormat; -import android.text.format.DateUtils; import android.view.WindowManager; import android.widget.EditText; @@ -36,6 +36,7 @@ import com.nextcloud.client.jobs.upload.PostUploadAction; import com.nextcloud.client.jobs.upload.UploadTrigger; import com.nextcloud.client.preferences.AppPreferences; +import com.nextcloud.utils.extensions.TemporalExtensionsKt; import com.owncloud.android.R; import com.owncloud.android.datamodel.OCFile; import com.owncloud.android.files.services.NameCollisionPolicy; @@ -43,9 +44,7 @@ import net.fortuna.ical4j.data.CalendarOutputter; import net.fortuna.ical4j.model.Calendar; -import net.fortuna.ical4j.model.Date; -import net.fortuna.ical4j.model.DateTime; -import net.fortuna.ical4j.model.Period; +import net.fortuna.ical4j.model.ParameterList; import net.fortuna.ical4j.model.Property; import net.fortuna.ical4j.model.PropertyFactoryImpl; import net.fortuna.ical4j.model.PropertyFactoryRegistry; @@ -56,29 +55,36 @@ import net.fortuna.ical4j.model.component.VAlarm; import net.fortuna.ical4j.model.component.VEvent; import net.fortuna.ical4j.model.parameter.FbType; -import net.fortuna.ical4j.model.property.Action; -import net.fortuna.ical4j.model.property.CalScale; import net.fortuna.ical4j.model.property.Description; import net.fortuna.ical4j.model.property.DtEnd; import net.fortuna.ical4j.model.property.DtStamp; import net.fortuna.ical4j.model.property.DtStart; import net.fortuna.ical4j.model.property.Duration; import net.fortuna.ical4j.model.property.FreeBusy; -import net.fortuna.ical4j.model.property.Method; import net.fortuna.ical4j.model.property.Organizer; import net.fortuna.ical4j.model.property.ProdId; -import net.fortuna.ical4j.model.property.Transp; -import net.fortuna.ical4j.model.property.Version; import net.fortuna.ical4j.model.property.XProperty; +import net.fortuna.ical4j.model.property.immutable.ImmutableAction; +import net.fortuna.ical4j.model.property.immutable.ImmutableCalScale; +import net.fortuna.ical4j.model.property.immutable.ImmutableMethod; +import net.fortuna.ical4j.model.property.immutable.ImmutableTransp; +import net.fortuna.ical4j.model.property.immutable.ImmutableVersion; import net.fortuna.ical4j.util.CompatibilityHints; +import org.threeten.extra.Interval; + import java.io.File; import java.io.FileOutputStream; -import java.io.IOException; -import java.net.URISyntaxException; -import java.text.ParseException; +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.temporal.Temporal; +import java.time.temporal.TemporalAmount; +import java.time.temporal.TemporalUnit; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -153,16 +159,16 @@ public void start() throws Exception { String prodId = "-//" + selectedCal.mOwner + "//iCal Import/Export " + ver + "//EN"; Calendar cal = new Calendar(); - cal.getProperties().add(new ProdId(prodId)); - cal.getProperties().add(Version.VERSION_2_0); - cal.getProperties().add(Method.PUBLISH); - cal.getProperties().add(CalScale.GREGORIAN); + cal.add(new ProdId(prodId)); + cal.add(ImmutableVersion.VERSION_2_0); + cal.add(ImmutableMethod.PUBLISH); + cal.add(ImmutableCalScale.GREGORIAN); if (selectedCal.mTimezone != null) { // We don't write any events with floating times, but export this // anyway so the default timezone for new events is correct when // the file is imported into a system that supports it. - cal.getProperties().add(new XProperty("X-WR-TIMEZONE", selectedCal.mTimezone)); + cal.add(new XProperty("X-WR-TIMEZONE", selectedCal.mTimezone)); } // query events @@ -176,7 +182,7 @@ public void start() throws Exception { List events = getEvents(resolver, selectedCal, cal); for (VEvent v : events) { - cal.getComponents().add(v); + cal.add(v); } if (!cal.getComponents().isEmpty()) { @@ -298,7 +304,7 @@ private VEvent convertFromDb(Cursor cur, Calendar cal, DtStamp timestamp) { return null; } - PropertyList l = new PropertyList<>(); + List l = new ArrayList<>(); l.add(timestamp); copyProperty(l, Property.UID, cur, Events.UID_2445); @@ -314,7 +320,7 @@ private VEvent convertFromDb(Cursor cur, Calendar cal, DtStamp timestamp) { } try { l.add(new Organizer(organizer)); - } catch (URISyntaxException ignored) { + } catch (IllegalArgumentException ignored) { if (!mFailedOrganisers.contains(organizer)) { Log_OC.e(TAG, "Failed to create mailTo for organizer " + organizer); mFailedOrganisers.add(organizer); @@ -327,41 +333,50 @@ private VEvent convertFromDb(Cursor cur, Calendar cal, DtStamp timestamp) { boolean allDay = TextUtils.equals(getString(cur, Events.ALL_DAY), "1"); boolean isTransparent; - DtEnd dtEnd = null; + DtEnd dtEnd = null; + Temporal dtStartValue; + TemporalAmount durationValue = null; if (allDay) { // All day event isTransparent = true; - Date start = getDateTime(cur, Events.DTSTART, null, null); - Date end = getDateTime(cur, Events.DTEND, null, null); - l.add(new DtStart(new Date(start))); + LocalDate start = (LocalDate) getDateTime(cur, Events.DTSTART, null, null); + LocalDate end = (LocalDate) getDateTime(cur, Events.DTEND, null, null); + dtStartValue = start; + l.add(new DtStart<>(start)); if (end != null) { - dtEnd = new DtEnd(new Date(end)); + dtEnd = new DtEnd<>(end); } else { - dtEnd = new DtEnd(utcDateFromMs(start.getTime() + DateUtils.DAY_IN_MILLIS)); + dtEnd = new DtEnd<>(start.plusDays(1)); } l.add(dtEnd); } else { // Regular or zero-time event. Start date must be a date-time - Date startDate = getDateTime(cur, Events.DTSTART, Events.EVENT_TIMEZONE, cal); - l.add(new DtStart(startDate)); + Temporal startDate = getDateTime(cur, Events.DTSTART, Events.EVENT_TIMEZONE, cal); + dtStartValue = startDate; + l.add(new DtStart<>(startDate)); // Use duration if we have one, otherwise end date if (hasStringValue(cur, Events.DURATION)) { - isTransparent = getString(cur, Events.DURATION).equals("PT0S"); + String durationString = getString(cur, Events.DURATION); + isTransparent = "PT0S".equals(durationString); if (!isTransparent) { - copyProperty(l, Property.DURATION, cur, Events.DURATION); + Duration durationProperty = new Duration(durationString); + durationValue = durationProperty.getDuration(); + l.add(durationProperty); } } else { String endTz = Events.EVENT_END_TIMEZONE; if (endTz == null) { endTz = Events.EVENT_TIMEZONE; } - Date end = getDateTime(cur, Events.DTEND, endTz, cal); - dtEnd = new DtEnd(end); - isTransparent = startDate.getTime() == end.getTime(); + Temporal end = getDateTime(cur, Events.DTEND, endTz, cal); + dtEnd = new DtEnd<>(end); + final var startEpochMilli = TemporalExtensionsKt.toEpochMilli(startDate); + final var endEpochMilli = TemporalExtensionsKt.toEpochMilli(end); + isTransparent = startEpochMilli == endEpochMilli; if (!isTransparent) { l.add(dtEnd); } @@ -379,22 +394,23 @@ private VEvent convertFromDb(Cursor cur, Calendar cal, DtStamp timestamp) { // This event is ordinarily transparent. If availability shows that its // not free, then mark it opaque. if (availability >= 0 && availability != Events.AVAILABILITY_FREE) { - l.add(Transp.OPAQUE); + l.add(ImmutableTransp.OPAQUE); } } else if (availability > Events.AVAILABILITY_BUSY) { // This event is ordinarily busy but differs, so output a FREEBUSY // period covering the time of the event - FreeBusy fb = new FreeBusy(); - fb.getParameters().add(new FbType(AVAIL_ENUM.get(availability))); - DateTime start = new DateTime(((DtStart) l.getProperty(Property.DTSTART)).getDate()); + Instant startInstant = TemporalExtensionsKt.toInstant(dtStartValue); + Interval interval; if (dtEnd != null) { - fb.getPeriods().add(new Period(start, new DateTime(dtEnd.getDate()))); + interval = Interval.of(startInstant, TemporalExtensionsKt.toInstant(dtEnd.getDate())); } else { - Duration d = (Duration) l.getProperty(Property.DURATION); - fb.getPeriods().add(new Period(start, d.getDuration())); + interval = Interval.of(startInstant, toJavaDuration(durationValue)); } + FbType fbType = new FbType(AVAIL_ENUM.get(availability)); + ParameterList fbParams = new ParameterList(Collections.singletonList(fbType)); + FreeBusy fb = new FreeBusy(fbParams, Collections.singletonList(interval)); l.add(fb); } @@ -407,7 +423,7 @@ private VEvent convertFromDb(Cursor cur, Calendar cal, DtStamp timestamp) { copyProperty(l, Property.URL, cur, Events.CUSTOM_APP_URI); } - VEvent e = new VEvent(l); + VEvent e = new VEvent(new PropertyList(l)); if (getInt(cur, Events.HAS_ALARM) == 1) { // Add alarms @@ -429,9 +445,9 @@ private VEvent convertFromDb(Cursor cur, Calendar cal, DtStamp timestamp) { int method = getInt(alarmCur, Reminders.METHOD); if (method == Reminders.METHOD_DEFAULT || method == Reminders.METHOD_ALERT) { VAlarm alarm = new VAlarm(java.time.Duration.ofMinutes(-mins)); - alarm.getProperties().add(Action.DISPLAY); - alarm.getProperties().add(desc); - e.getAlarms().add(alarm); + alarm.add(ImmutableAction.DISPLAY); + alarm.add(desc); + e.add(alarm); } } alarmCur.close(); @@ -464,12 +480,6 @@ private boolean hasStringValue(Cursor cur, String dbName) { return i != -1 && !TextUtils.isEmpty(cur.getString(i)); } - private Date utcDateFromMs(long ms) { - // This date will be UTC provided the default false value of the iCal4j property - // "net.fortuna.ical4j.timezone.date.floating" has not been changed. - return new Date(ms); - } - private boolean isUtcTimeZone(final String tz) { if (TextUtils.isEmpty(tz)) { return true; @@ -478,15 +488,19 @@ private boolean isUtcTimeZone(final String tz) { return "UTC".equals(utz) || "UTC-0".equals(utz) || "UTC+0".equals(utz) || utz.endsWith("/UTC"); } - private Date getDateTime(Cursor cur, String dbName, String dbTzName, Calendar cal) { + private Temporal getDateTime(Cursor cur, String dbName, String dbTzName, Calendar cal) { int i = getColumnIndex(cur, dbName); if (i == -1 || cur.isNull(i)) { Log_OC.e(TAG, "No valid " + dbName + " column found, index: " + Integer.toString(i)); return null; } + final long millis = cur.getLong(i); + if (cal == null) { - return utcDateFromMs(cur.getLong(i)); // Ignore timezone for date-only dates + // Ignore timezone for date-only dates. This date will be UTC provided the default false + // value of the iCal4j property "net.fortuna.ical4j.timezone.date.floating" has not been changed. + return Instant.ofEpochMilli(millis).atZone(ZoneOffset.UTC).toLocalDate(); } else if (dbTzName == null) { Log_OC.e(TAG, "No valid tz " + dbName + " column given"); } @@ -494,37 +508,39 @@ private Date getDateTime(Cursor cur, String dbName, String dbTzName, Calendar ca String tz = getString(cur, dbTzName); final boolean isUtc = isUtcTimeZone(tz); - DateTime dt = new DateTime(isUtc); - if (dt.isUtc() != isUtc) { - throw new RuntimeException("UTC mismatch after construction"); - } - dt.setTime(cur.getLong(i)); - if (dt.isUtc() != isUtc) { - throw new RuntimeException("UTC mismatch after setTime"); + if (isUtc) { + return Instant.ofEpochMilli(millis); } - if (!isUtc) { + if (mTzRegistry == null) { + mTzRegistry = TimeZoneRegistryFactory.getInstance().createRegistry(); if (mTzRegistry == null) { - mTzRegistry = TimeZoneRegistryFactory.getInstance().createRegistry(); - if (mTzRegistry == null) { - throw new RuntimeException("Failed to create TZ registry"); - } - } - TimeZone t = mTzRegistry.getTimeZone(tz); - if (t == null) { - Log_OC.e(TAG, "Unknown TZ " + tz + ", assuming UTC"); - } else { - dt.setTimeZone(t); - if (!mInsertedTimeZones.contains(t)) { - cal.getComponents().add(t.getVTimeZone()); - mInsertedTimeZones.add(t); - } + throw new RuntimeException("Failed to create TZ registry"); } } - return dt; + TimeZone t = mTzRegistry.getTimeZone(tz); + if (t == null) { + Log_OC.e(TAG, "Unknown TZ " + tz + ", assuming UTC"); + return Instant.ofEpochMilli(millis); + } + + ZoneId zoneId = t.toZoneId(); + if (!mInsertedTimeZones.contains(t)) { + cal.add(t.getVTimeZone()); + mInsertedTimeZones.add(t); + } + return Instant.ofEpochMilli(millis).atZone(zoneId); + } + + private static java.time.Duration toJavaDuration(TemporalAmount amount) { + long ms = 0; + for (TemporalUnit unit : amount.getUnits()) { + ms += amount.get(unit) * unit.getDuration().toMillis(); + } + return java.time.Duration.ofMillis(ms); } - private String copyProperty(PropertyList l, String evName, Cursor cur, String dbName) { + private String copyProperty(List l, String evName, Cursor cur, String dbName) { // None of the exceptions caught below should be able to be thrown AFAICS. try { String value = getString(cur, dbName); @@ -534,12 +550,12 @@ private String copyProperty(PropertyList l, String evName, Cursor cur, l.add(p); return value; } - } catch (IOException | URISyntaxException | ParseException ignored) { + } catch (RuntimeException ignored) { } return null; } - private void copyEnumProperty(PropertyList l, String evName, Cursor cur, String dbName, + private void copyEnumProperty(List l, String evName, Cursor cur, String dbName, List vals) { // None of the exceptions caught below should be able to be thrown AFAICS. try { @@ -552,7 +568,7 @@ private void copyEnumProperty(PropertyList l, String evName, Cursor cu l.add(p); } } - } catch (IOException | URISyntaxException | ParseException ignored) { + } catch (RuntimeException ignored) { } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4e3b52354dcb..d97f4f897237 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -42,7 +42,7 @@ flexboxVersion = "3.0.0" fragmentKtxVersion = "1.8.9" glide = "5.0.7" gsonVersion = "2.14.0" -ical4jVersion = "3.2.19" +ical4jVersion = "4.3.0" jackrabbitWebdavVersion = "2.13.5" jacoco = "0.8.14" jsonVersion = "20250517" diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index b6a0012a381e..6b4332a6c49c 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -198,6 +198,7 @@ + @@ -440,6 +441,7 @@ +