Skip to content

Commit f0cdf41

Browse files
committed
feat(ical-v4)
Signed-off-by: alperozturk96 <alper_ozturk@proton.me>
1 parent 2b7b053 commit f0cdf41

4 files changed

Lines changed: 182 additions & 125 deletions

File tree

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Nextcloud - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
8+
package com.nextcloud.utils.extensions
9+
10+
import java.time.*
11+
import java.time.temporal.Temporal
12+
13+
fun Temporal?.toInstant(): Instant = when (this) {
14+
is Instant -> this
15+
is ZonedDateTime -> toInstant()
16+
is OffsetDateTime -> toInstant()
17+
is LocalDateTime -> atZone(ZoneOffset.UTC).toInstant()
18+
is LocalDate -> atStartOfDay(ZoneOffset.UTC).toInstant()
19+
else -> throw IllegalArgumentException("Unsupported temporal type")
20+
}
21+
22+
fun Temporal?.toEpochMilli(): Long = toInstant().toEpochMilli()

app/src/main/java/thirdparties/sufficientlysecure/ProcessVEvent.java

Lines changed: 63 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/*
22
* Nextcloud - Android Client
33
*
4+
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
45
* SPDX-FileCopyrightText: 2015 Jon Griffiths (jon_p_griffiths@yahoo.com)
56
* SPDX-FileCopyrightText: 2013 Dominik Schürmann <dominik@dominikschuermann.de>
67
* SPDX-FileCopyrightText: 2010-2011 Lukas Aichbauer
@@ -23,11 +24,11 @@
2324
import android.text.format.DateUtils;
2425

2526
import com.nextcloud.client.preferences.AppPreferences;
27+
import com.nextcloud.utils.extensions.TemporalExtensionsKt;
2628
import com.owncloud.android.R;
2729
import com.owncloud.android.lib.common.utils.Log_OC;
2830

2931
import net.fortuna.ical4j.model.Calendar;
30-
import net.fortuna.ical4j.model.DateTime;
3132
import net.fortuna.ical4j.model.Parameter;
3233
import net.fortuna.ical4j.model.Property;
3334
import net.fortuna.ical4j.model.component.CalendarComponent;
@@ -37,17 +38,25 @@
3738
import net.fortuna.ical4j.model.parameter.Related;
3839
import net.fortuna.ical4j.model.property.Action;
3940
import net.fortuna.ical4j.model.property.DateProperty;
41+
import net.fortuna.ical4j.model.property.DtEnd;
42+
import net.fortuna.ical4j.model.property.DtStart;
4043
import net.fortuna.ical4j.model.property.Duration;
4144
import net.fortuna.ical4j.model.property.FreeBusy;
4245
import net.fortuna.ical4j.model.property.Transp;
4346
import net.fortuna.ical4j.model.property.Trigger;
47+
import net.fortuna.ical4j.model.property.immutable.ImmutableAction;
48+
import net.fortuna.ical4j.model.property.immutable.ImmutableTransp;
4449

4550
import java.net.URI;
51+
import java.time.LocalDate;
52+
import java.time.ZonedDateTime;
53+
import java.time.temporal.Temporal;
4654
import java.time.temporal.TemporalAmount;
4755
import java.time.temporal.TemporalUnit;
4856
import java.util.ArrayList;
4957
import java.util.List;
5058
import java.util.Objects;
59+
import java.util.Optional;
5160
import java.util.UUID;
5261

5362
import javax.inject.Inject;
@@ -110,13 +119,8 @@ private boolean getTestFileSupport() {
110119
}
111120

112121
public DuplicateHandlingEnum getDuplicateHandling() {
113-
// return DuplicateHandlingEnum.values()[getEnumInt(PREF_DUPLICATE_HANDLING, 0)];
114122
return DuplicateHandlingEnum.values()[0]; // TODO is option needed?
115123
}
116-
117-
// private int getEnumInt(final String key, final int def) {
118-
// return Integer.parseInt(getString(key, String.valueOf(def)));
119-
// }
120124
}
121125

122126
public ProcessVEvent(Context context, Calendar iCalCalendar, AndroidCalendar selectedCal, boolean isInserter) {
@@ -275,7 +279,8 @@ private ContentValues convertToDB(VEvent e, Options options,
275279
reminders.clear();
276280

277281
boolean allDay = false;
278-
boolean startIsDate = !(e.getStartDate().getDate() instanceof DateTime);
282+
Temporal startTemporal = e.<Temporal>getStartDate().map(DtStart::getDate).orElse(null);
283+
boolean startIsDate = startTemporal instanceof LocalDate;
279284
boolean isRecurring = hasProperty(e, Property.RRULE) || hasProperty(e, Property.RDATE);
280285

281286
if (startIsDate) {
@@ -288,25 +293,27 @@ private ContentValues convertToDB(VEvent e, Options options,
288293
// No end date or duration given.
289294
// Since we added a duration above when the start date is a DATE:
290295
// - The start date is a DATETIME, the event lasts no time at all (RFC 2445).
291-
e.getProperties().add(ZERO_SECONDS);
296+
e.add(ZERO_SECONDS);
292297
// Zero time events are always free (RFC 2445), so override/set TRANSP accordingly.
293298
removeProperty(e, Property.TRANSP);
294-
e.getProperties().add(Transp.TRANSPARENT);
299+
e.add(ImmutableTransp.TRANSPARENT);
295300
}
296301

297302
if (isRecurring) {
298303
// Recurring event. Android insists on a duration.
299304
if (!hasProperty(e, Property.DURATION)) {
300305
// Calculate duration from start to end date
301-
Duration d = new Duration(e.getStartDate().getDate(), e.getEndDate().getDate());
302-
e.getProperties().add(d);
306+
Temporal endTemporal = e.<Temporal>getEndDate().map(DtEnd::getDate).orElse(null);
307+
Duration d = new Duration(startTemporal, endTemporal);
308+
e.add(d);
303309
}
304310
removeProperty(e, Property.DTEND);
305311
} else {
306312
// Non-recurring event. Android insists on an end date.
307313
if (!hasProperty(e, Property.DTEND)) {
308314
// Calculate end date from duration, set it and remove the duration.
309-
e.getProperties().add(e.getEndDate());
315+
Optional<DtEnd<Temporal>> derivedEnd = e.getEndDate();
316+
derivedEnd.ifPresent(e::add);
310317
}
311318
removeProperty(e, Property.DURATION);
312319
}
@@ -331,8 +338,9 @@ private ContentValues convertToDB(VEvent e, Options options,
331338

332339
copyProperty(c, Events.EVENT_LOCATION, e, Property.LOCATION);
333340

334-
if (hasProperty(e, Property.STATUS)) {
335-
String status = e.getProperty(Property.STATUS).getValue();
341+
Optional<Property> statusProperty = e.getProperty(Property.STATUS);
342+
if (statusProperty.isPresent()) {
343+
String status = statusProperty.get().getValue();
336344
switch (status) {
337345
case "TENTATIVE":
338346
c.put(Events.STATUS, Events.STATUS_TENTATIVE);
@@ -352,13 +360,14 @@ private ContentValues convertToDB(VEvent e, Options options,
352360
c.put(Events.ALL_DAY, 1);
353361
}
354362

355-
copyDateProperty(c, Events.DTSTART, Events.EVENT_TIMEZONE, e.getStartDate());
363+
e.<Temporal>getStartDate().ifPresent(dtStart -> copyDateProperty(c, Events.DTSTART, Events.EVENT_TIMEZONE, dtStart));
356364
if (hasProperty(e, Property.DTEND)) {
357-
copyDateProperty(c, Events.DTEND, Events.EVENT_END_TIMEZONE, e.getEndDate());
365+
e.<Temporal>getEndDate().ifPresent(dtEnd -> copyDateProperty(c, Events.DTEND, Events.EVENT_END_TIMEZONE, dtEnd));
358366
}
359367

360-
if (hasProperty(e, Property.CLASS)) {
361-
String access = e.getProperty(Property.CLASS).getValue();
368+
Optional<Property> classProperty = e.getProperty(Property.CLASS);
369+
if (classProperty.isPresent()) {
370+
String access = classProperty.get().getValue();
362371
int accessLevel = switch (access) {
363372
case "CONFIDENTIAL" -> Events.ACCESS_CONFIDENTIAL;
364373
case "PRIVATE" -> Events.ACCESS_PRIVATE;
@@ -372,16 +381,17 @@ private ContentValues convertToDB(VEvent e, Options options,
372381
// Work out availability. This is confusing as FREEBUSY and TRANSP overlap.
373382
int availability = Events.AVAILABILITY_BUSY;
374383
if (hasProperty(e, Property.TRANSP)) {
375-
if (e.getTransparency() == Transp.TRANSPARENT) {
384+
Optional<Transp> transparency = e.getTransparency();
385+
if (transparency.isPresent() && Transp.VALUE_TRANSPARENT.equals(transparency.get().getValue())) {
376386
availability = Events.AVAILABILITY_FREE;
377387
}
378388

379389
} else if (hasProperty(e, Property.FREEBUSY)) {
380-
FreeBusy fb = e.getProperty(Property.FREEBUSY);
381-
FbType fbType = fb.getParameter(Parameter.FBTYPE);
382-
if (fbType != null && fbType == FbType.FREE) {
390+
Optional<FreeBusy> fb = e.getProperty(Property.FREEBUSY);
391+
Optional<FbType> fbType = fb.isPresent() ? fb.get().getParameter(Parameter.FBTYPE) : Optional.empty();
392+
if (fbType.isPresent() && fbType.get().equals(FbType.FREE)) {
383393
availability = Events.AVAILABILITY_FREE;
384-
} else if (fbType != null && fbType == FbType.BUSY_TENTATIVE) {
394+
} else if (fbType.isPresent() && fbType.get().equals(FbType.BUSY_TENTATIVE)) {
385395
availability = Events.AVAILABILITY_TENTATIVE;
386396
}
387397
}
@@ -400,23 +410,31 @@ private ContentValues convertToDB(VEvent e, Options options,
400410

401411
for (VAlarm a : e.getAlarms()) {
402412

403-
if (a.getAction() != Action.AUDIO && a.getAction() != Action.DISPLAY) {
413+
Optional<Action> action = a.getAction();
414+
if (action.isEmpty() || (!ImmutableAction.AUDIO.equals(action.get()) && !ImmutableAction.DISPLAY.equals(action.get()))) {
404415
continue; // Ignore email and procedure alarms
405416
}
406417

407-
Trigger t = a.getTrigger();
408-
final long startMs = e.getStartDate().getDate().getTime();
418+
Optional<Trigger> triggerProperty = a.getTrigger();
419+
if (triggerProperty.isEmpty()) {
420+
continue;
421+
}
422+
Trigger t = triggerProperty.get();
423+
424+
final long startMs = TemporalExtensionsKt.toEpochMilli(startTemporal);
409425
long alarmStartMs = startMs;
410426
long alarmMs;
411427

412428
// FIXME: - Support for repeating alarms
413429
// - Check the calendars max number of alarms
414-
if (t.getDateTime() != null)
415-
alarmMs = t.getDateTime().getTime(); // Absolute
430+
if (t.isAbsolute())
431+
alarmMs = t.getDate().toEpochMilli(); // Absolute
416432
else if (t.getDuration() != null) {
417-
Related rel = t.getParameter(Parameter.RELATED);
418-
if (rel != null && rel == Related.END)
419-
alarmStartMs = e.getEndDate().getDate().getTime();
433+
Optional<Related> rel = t.getParameter(Parameter.RELATED);
434+
if (rel.isPresent() && rel.get().equals(Related.END)) {
435+
Temporal endTemporal = e.<Temporal>getEndDate().map(DtEnd::getDate).orElse(null);
436+
alarmStartMs = TemporalExtensionsKt.toEpochMilli(endTemporal);
437+
}
420438
alarmMs = alarmStartMs + durationToMs(t.getDuration());
421439
} else {
422440
continue;
@@ -452,33 +470,31 @@ private static long durationToMs(TemporalAmount d) {
452470
}
453471

454472
private boolean hasProperty(VEvent e, String name) {
455-
return e.getProperty(name) != null;
473+
return e.getProperty(name).isPresent();
456474
}
457475

458476
private void removeProperty(VEvent e, String name) {
459-
Property p = e.getProperty(name);
460-
if (p != null) {
461-
e.getProperties().remove(p);
462-
}
477+
e.removeAll(name);
463478
}
464479

465480
private void copyProperty(ContentValues c, String dbName, VEvent e, String evName) {
466481
if (dbName != null) {
467-
Property p = e.getProperty(evName);
468-
if (p != null) {
469-
c.put(dbName, p.getValue());
482+
Optional<Property> p = e.getProperty(evName);
483+
if (p.isPresent()) {
484+
c.put(dbName, p.get().getValue());
470485
}
471486
}
472487
}
473488

474-
private void copyDateProperty(ContentValues c, String dbName, String dbTzName, DateProperty date) {
475-
if (dbName != null && date.getDate() != null) {
476-
c.put(dbName, date.getDate().getTime()); // ms since epoc in GMT
489+
private void copyDateProperty(ContentValues c, String dbName, String dbTzName, DateProperty<? extends Temporal> date) {
490+
Temporal temporal = date.getDate();
491+
if (dbName != null && temporal != null) {
492+
c.put(dbName, TemporalExtensionsKt.toEpochMilli(temporal)); // ms since epoc in GMT
477493
if (dbTzName != null) {
478-
if (date.isUtc() || date.getTimeZone() == null) {
494+
if (date.isUtc() || !(temporal instanceof ZonedDateTime)) {
479495
c.put(dbTzName, "UTC");
480496
} else {
481-
c.put(dbTzName, date.getTimeZone().getID());
497+
c.put(dbTzName, ((ZonedDateTime) temporal).getZone().getId());
482498
}
483499
}
484500
}
@@ -566,10 +582,11 @@ private void checkTestValue(ContentValues c, String keyValue, String testName) {
566582

567583
private void processEventTests(VEvent e, ContentValues c, List<Integer> reminders) {
568584

569-
Property testName = e.getProperty("X-TEST-NAME");
570-
if (testName == null) {
585+
Optional<Property> testNameProperty = e.getProperty("X-TEST-NAME");
586+
if (testNameProperty.isEmpty()) {
571587
return; // Not a test case
572588
}
589+
Property testName = testNameProperty.get();
573590

574591
// This is a test event. Verify it using the embedded meta data.
575592
Log_OC.i(TAG, "Processing test case " + testName.getValue() + "...");

0 commit comments

Comments
 (0)