diff --git a/app/src/androidTest/java/org/tasks/repeats/RepeatRuleToStringTest.kt b/app/src/androidTest/java/org/tasks/repeats/RepeatRuleToStringTest.kt index a428838b8..a58ba9489 100644 --- a/app/src/androidTest/java/org/tasks/repeats/RepeatRuleToStringTest.kt +++ b/app/src/androidTest/java/org/tasks/repeats/RepeatRuleToStringTest.kt @@ -11,6 +11,11 @@ import java.text.ParseException @RunWith(AndroidJUnit4::class) class RepeatRuleToStringTest { + @Test + fun daily() { + assertEquals("Repeats daily", toString("RRULE:FREQ=DAILY")) + } + @Test fun weekly() { assertEquals("Repeats weekly", toString("RRULE:FREQ=WEEKLY;INTERVAL=1")) diff --git a/app/src/commonTest/java/org/tasks/TestUtilities.kt b/app/src/commonTest/java/org/tasks/TestUtilities.kt index a5ecfe272..bbe341a2c 100644 --- a/app/src/commonTest/java/org/tasks/TestUtilities.kt +++ b/app/src/commonTest/java/org/tasks/TestUtilities.kt @@ -4,6 +4,7 @@ import android.content.Context import at.bitfire.ical4android.Task.Companion.tasksFromReader import com.todoroo.astrid.data.Task import org.tasks.caldav.CaldavConverter +import org.tasks.data.CaldavTask import org.tasks.preferences.Preferences import java.io.StringReader import java.nio.file.Files @@ -20,11 +21,22 @@ object TestUtilities { return task } - private fun fromResource(path: String): at.bitfire.ical4android.Task { + fun setup(path: String): Pair { + val task = Task() + val vtodo = readFile(path) + CaldavConverter.apply(task, fromString(vtodo)) + return Pair(task, CaldavTask().apply { this.vtodo = vtodo }) + } + + private fun fromResource(path: String): at.bitfire.ical4android.Task = + fromString(readFile(path)) + + fun readFile(path: String): String { val url = javaClass.classLoader!!.getResource(path) val paths = Paths.get(url!!.toURI()) - return fromString(String(Files.readAllBytes(paths), Charsets.UTF_8)) + return String(Files.readAllBytes(paths), Charsets.UTF_8) } - private fun fromString(task: String) = tasksFromReader(StringReader(task))[0] + fun fromString(task: String): at.bitfire.ical4android.Task = + task.let { tasksFromReader(StringReader(it))[0] } } \ No newline at end of file diff --git a/app/src/main/java/com/todoroo/astrid/data/Task.kt b/app/src/main/java/com/todoroo/astrid/data/Task.kt index ffb5b71d4..ccdf5cfcb 100644 --- a/app/src/main/java/com/todoroo/astrid/data/Task.kt +++ b/app/src/main/java/com/todoroo/astrid/data/Task.kt @@ -234,7 +234,7 @@ class Task : Parcelable { fun repeatAfterCompletion(): Boolean = recurrence.isRepeatAfterCompletion() - fun sanitizedRecurrence(): String? = getRecurrenceWithoutFrom()?.replace("BYDAY=;".toRegex(), "") // $NON-NLS-1$//$NON-NLS-2$ + fun sanitizedRecurrence(): String? = getRecurrenceWithoutFrom()?.sanitizeRRule() fun getRecurrenceWithoutFrom(): String? = recurrence.withoutFrom() @@ -608,6 +608,11 @@ class Task : Parcelable { } } + @JvmStatic + fun String?.sanitizeRRule(): String? = this + ?.replace("BYDAY=;", "") + ?.replace("COUNT=-1;", "") + @JvmStatic fun isUuidEmpty(uuid: String?): Boolean { return NO_UUID == uuid || Strings.isNullOrEmpty(uuid) } diff --git a/app/src/main/java/org/tasks/caldav/CaldavConverter.java b/app/src/main/java/org/tasks/caldav/CaldavConverter.java index fe0052d5b..906ce4ff8 100644 --- a/app/src/main/java/org/tasks/caldav/CaldavConverter.java +++ b/app/src/main/java/org/tasks/caldav/CaldavConverter.java @@ -53,11 +53,10 @@ public class CaldavConverter { local.setRecurrence(""); } else { Recur recur = repeatRule.getRecur(); - if (recur.getInterval() <= 0) { - recur.setInterval(1); - } + Date until = recur.getUntil(); + local.setRepeatUntil(until == null ? 0 : org.tasks.time.DateTime.from(until).getMillis()); local.setRecurrence( - "RRULE:" + recur.toString() + (local.repeatAfterCompletion() ? ";FROM=COMPLETION" : "")); + "RRULE:" + Task.sanitizeRRule(recur.toString()) + (local.repeatAfterCompletion() ? ";FROM=COMPLETION" : "")); } Due due = remote.getDue(); if (due == null) { @@ -142,8 +141,14 @@ public class CaldavConverter { } if (task.isRecurring()) { try { - String rrule = task.getRecurrenceWithoutFrom().replace("RRULE:", ""); - remote.setRRule(new RRule(rrule)); + RRule rrule = new RRule(task.getRecurrenceWithoutFrom().replace("RRULE:", "")); + long repeatUntil = task.getRepeatUntil(); + rrule + .getRecur() + .setUntil( + repeatUntil > 0 ? new DateTime(newDateTime(repeatUntil).toUTC().getMillis()) : null); + String sanitized = Task.sanitizeRRule(rrule.getValue()); // ical4j adds COUNT=-1 if there is an UNTIL value + remote.setRRule(new RRule(sanitized)); if (remote.getDtStart() == null) { Date date = remote.getDue() != null ? remote.getDue().getDate() : new Date(); remote.setDtStart(new DtStart(date)); diff --git a/app/src/main/java/org/tasks/repeats/RepeatRuleToString.java b/app/src/main/java/org/tasks/repeats/RepeatRuleToString.java index 1c9eadc17..c5b4b6c15 100644 --- a/app/src/main/java/org/tasks/repeats/RepeatRuleToString.java +++ b/app/src/main/java/org/tasks/repeats/RepeatRuleToString.java @@ -40,7 +40,7 @@ public class RepeatRuleToString { int count = rrule.getCount(); String countString = count > 0 ? context.getResources().getQuantityString(R.plurals.repeat_times, count) : ""; - if (interval == 1) { + if (interval <= 1) { String frequencyString = context.getString(getSingleFrequencyResource(frequency)); if ((frequency == WEEKLY || frequency == MONTHLY) && !rrule.getByDay().isEmpty()) { String dayString = getDayString(rrule); diff --git a/app/src/main/java/org/tasks/time/DateTime.java b/app/src/main/java/org/tasks/time/DateTime.java index 6d458c46f..c91be7482 100644 --- a/app/src/main/java/org/tasks/time/DateTime.java +++ b/app/src/main/java/org/tasks/time/DateTime.java @@ -15,14 +15,15 @@ import com.google.ical.values.DateValue; import com.google.ical.values.DateValueImpl; import com.google.ical.values.Weekday; import java.text.SimpleDateFormat; +import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.Calendar; import java.util.GregorianCalendar; import java.util.Objects; import java.util.TimeZone; import java.util.concurrent.TimeUnit; +import net.fortuna.ical4j.model.Date; import org.tasks.locale.Locale; -import java.time.LocalDate; -import java.time.LocalDateTime; public class DateTime { @@ -95,6 +96,10 @@ public class DateTime { return new DateTime(dateValue.year(), dateValue.month(), dateValue.day()); } + public static DateTime from(Date date) { + return new DateTime(date.getTime(), UTC); + } + private DateTime setTime(int hours, int minutes, int seconds, int milliseconds) { Calendar calendar = getCalendar(); calendar.set(Calendar.HOUR_OF_DAY, hours); diff --git a/app/src/test/java/org/tasks/caldav/AppleRemindersTests.kt b/app/src/test/java/org/tasks/caldav/AppleRemindersTests.kt index dbf781ad6..813b4db85 100644 --- a/app/src/test/java/org/tasks/caldav/AppleRemindersTests.kt +++ b/app/src/test/java/org/tasks/caldav/AppleRemindersTests.kt @@ -55,7 +55,7 @@ class AppleRemindersTests { @Test fun repeatDaily() { - assertEquals("RRULE:FREQ=DAILY;INTERVAL=1", vtodo("apple/repeat_daily.txt").recurrence) + assertEquals("RRULE:FREQ=DAILY", vtodo("apple/repeat_daily.txt").recurrence) } @Test diff --git a/app/src/test/java/org/tasks/caldav/ThunderbirdTests.kt b/app/src/test/java/org/tasks/caldav/ThunderbirdTests.kt index 37f8b8310..daec919c5 100644 --- a/app/src/test/java/org/tasks/caldav/ThunderbirdTests.kt +++ b/app/src/test/java/org/tasks/caldav/ThunderbirdTests.kt @@ -5,6 +5,7 @@ import org.junit.After import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test +import org.tasks.TestUtilities.setup import org.tasks.TestUtilities.vtodo import org.tasks.time.DateTime import java.util.* @@ -56,7 +57,7 @@ class ThunderbirdTests { @Test fun repeatDaily() { assertEquals( - "RRULE:FREQ=DAILY;INTERVAL=1", vtodo("thunderbird/repeat_daily.txt").recurrence) + "RRULE:FREQ=DAILY", vtodo("thunderbird/repeat_daily.txt").recurrence) } @Test @@ -83,4 +84,20 @@ class ThunderbirdTests { fun highPriority() { assertEquals(Task.Priority.HIGH, vtodo("thunderbird/priority_high.txt").priority) } + + @Test + fun getRepeatUntil() { + assertEquals( + DateTime(2020, 7, 31, 11, 0, 0, 0).millis, + vtodo("thunderbird/repeat_until_date_time.txt").repeatUntil) + } + + @Test + fun dontTruncateTimeFromUntil() { + val (task, caldavTask) = setup("thunderbird/repeat_until_date_time.txt") + val remote = CaldavConverter.toCaldav(caldavTask, task) + assertEquals( + "FREQ=WEEKLY;UNTIL=20200731T160000Z;BYDAY=MO,TU,WE,TH,FR", + remote.rRule!!.value) + } } \ No newline at end of file diff --git a/app/src/test/resources/thunderbird/repeat_until_date_time.txt b/app/src/test/resources/thunderbird/repeat_until_date_time.txt new file mode 100644 index 000000000..978c1e323 --- /dev/null +++ b/app/src/test/resources/thunderbird/repeat_until_date_time.txt @@ -0,0 +1,30 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN +BEGIN:VTIMEZONE +TZID:America/Chicago +BEGIN:STANDARD +DTSTART:19701101T020000 +RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11 +TZNAME:CST +TZOFFSETFROM:-0500 +TZOFFSETTO:-0600 +END:STANDARD +BEGIN:DAYLIGHT +DTSTART:19700308T020000 +RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3 +TZNAME:CDT +TZOFFSETFROM:-0600 +TZOFFSETTO:-0500 +END:DAYLIGHT +END:VTIMEZONE +BEGIN:VTODO +CREATED:20200717T155259Z +DTSTAMP:20200717T155411Z +DTSTART;TZID=America/Chicago:20200717T110000 +LAST-MODIFIED:20200717T155411Z +RRULE:FREQ=WEEKLY;UNTIL=20200731T160000Z;BYDAY=MO,TU,WE,TH,FR +SUMMARY:Repeat until +UID:3c6ed018-7840-404b-9182-5e2f39967a55 +END:VTODO +END:VCALENDAR