diff --git a/app/src/androidTest/java/org/tasks/opentasks/OpenTasksSynchronizerTest.kt b/app/src/androidTest/java/org/tasks/opentasks/OpenTasksSynchronizerTest.kt index 84ecb8056..0040fcddb 100644 --- a/app/src/androidTest/java/org/tasks/opentasks/OpenTasksSynchronizerTest.kt +++ b/app/src/androidTest/java/org/tasks/opentasks/OpenTasksSynchronizerTest.kt @@ -9,6 +9,7 @@ import org.junit.Assert.* import org.junit.Before import org.junit.Test import org.tasks.R +import org.tasks.TestUtilities.withTZ import org.tasks.caldav.iCalendar.Companion.getParent import org.tasks.data.CaldavAccount import org.tasks.data.CaldavAccount.Companion.TYPE_OPENTASKS @@ -22,10 +23,13 @@ import org.tasks.makers.CaldavTaskMaker.REMOTE_ID import org.tasks.makers.CaldavTaskMaker.REMOTE_PARENT import org.tasks.makers.CaldavTaskMaker.TASK import org.tasks.makers.CaldavTaskMaker.newCaldavTask +import org.tasks.makers.TaskMaker import org.tasks.makers.TaskMaker.PARENT import org.tasks.makers.TaskMaker.RRULE import org.tasks.makers.TaskMaker.newTask import org.tasks.preferences.Preferences +import org.tasks.time.DateTime +import java.util.* import javax.inject.Inject @UninstallModules(ProductionModule::class) @@ -169,7 +173,121 @@ class OpenTasksSynchronizerTest : InjectingTestCase() { assertEquals("1234", openTaskDao.getTask(listId.toLong(), "abcd")?.task?.getParent()) } + @Test + fun readDueDatePositiveOffset() = runBlocking { + val (listId, list) = openTaskDao.insertList() + openTaskDao.insertTask(listId, ALL_DAY_DUE) + + withTZ(BERLIN) { + synchronizer.sync() + } + + val caldavTask = caldavDao.getTaskByRemoteId(list.uuid!!, "3863299529704302692") + val task = taskDao.fetch(caldavTask!!.task) + assertEquals( + DateTime(2021, 2, 1, 12, 0, 0, 0, BERLIN).millis, + task?.dueDate + ) + } + + @Test + fun writeDueDatePositiveOffset() = withTZ(BERLIN) { + val (listId, list) = openTaskDao.insertList() + val taskId = taskDao.createNew(newTask( + with(TaskMaker.DUE_DATE, DateTime(2021, 2, 1)) + )) + caldavDao.insert(newCaldavTask( + with(CALENDAR, list.uuid), + with(REMOTE_ID, "1234"), + with(TASK, taskId) + )) + + synchronizer.sync() + + assertEquals( + 1612137600000, + openTaskDao.getTask(listId.toLong(), "1234")?.task?.due?.date?.time + ) + } + + @Test + fun readDueDateNoOffset() = runBlocking { + val (listId, list) = openTaskDao.insertList() + openTaskDao.insertTask(listId, ALL_DAY_DUE) + + withTZ(LONDON) { + synchronizer.sync() + } + + val caldavTask = caldavDao.getTaskByRemoteId(list.uuid!!, "3863299529704302692") + val task = taskDao.fetch(caldavTask!!.task) + assertEquals( + DateTime(2021, 2, 1, 12, 0, 0, 0, LONDON).millis, + task?.dueDate + ) + } + + @Test + fun writeDueDateNoOffset() = withTZ(LONDON) { + val (listId, list) = openTaskDao.insertList() + val taskId = taskDao.createNew(newTask( + with(TaskMaker.DUE_DATE, DateTime(2021, 2, 1)) + )) + caldavDao.insert(newCaldavTask( + with(CALENDAR, list.uuid), + with(REMOTE_ID, "1234"), + with(TASK, taskId) + )) + + synchronizer.sync() + + assertEquals( + 1612137600000, + openTaskDao.getTask(listId.toLong(), "1234")?.task?.due?.date?.time + ) + } + + @Test + fun readDueDateNegativeOffset() = runBlocking { + val (listId, list) = openTaskDao.insertList() + openTaskDao.insertTask(listId, ALL_DAY_DUE) + + withTZ(NEW_YORK) { + synchronizer.sync() + } + + val caldavTask = caldavDao.getTaskByRemoteId(list.uuid!!, "3863299529704302692") + val task = taskDao.fetch(caldavTask!!.task) + assertEquals( + DateTime(2021, 2, 1, 12, 0, 0, 0, NEW_YORK).millis, + task?.dueDate + ) + } + + @Test + fun writeDueDateNegativeOffset() = withTZ(NEW_YORK) { + val (listId, list) = openTaskDao.insertList() + val taskId = taskDao.createNew(newTask( + with(TaskMaker.DUE_DATE, DateTime(2021, 2, 1)) + )) + caldavDao.insert(newCaldavTask( + with(CALENDAR, list.uuid), + with(REMOTE_ID, "1234"), + with(TASK, taskId) + )) + + synchronizer.sync() + + assertEquals( + 1612137600000, + openTaskDao.getTask(listId.toLong(), "1234")?.task?.due?.date?.time + ) + } + companion object { + val BERLIN = TimeZone.getTimeZone("Europe/Berlin") + val LONDON = TimeZone.getTimeZone("Europe/London") + val NEW_YORK = TimeZone.getTimeZone("America/New_York") val SUBTASK = """ BEGIN:VCALENDAR VERSION:2.0 @@ -184,5 +302,20 @@ class OpenTasksSynchronizerTest : InjectingTestCase() { END:VTODO END:VCALENDAR """.trimIndent() + + val ALL_DAY_DUE = """ + BEGIN:VCALENDAR + VERSION:2.0 + PRODID:+//IDN tasks.org//android-110304//EN + BEGIN:VTODO + DTSTAMP:20210129T155402Z + UID:3863299529704302692 + CREATED:20210129T155318Z + LAST-MODIFIED:20210129T155329Z + SUMMARY:Due date + DUE;VALUE=DATE:20210201 + END:VTODO + END:VCALENDAR + """.trimIndent() } } \ No newline at end of file diff --git a/app/src/commonTest/java/org/tasks/TestUtilities.kt b/app/src/commonTest/java/org/tasks/TestUtilities.kt index 4f2e07fda..bbace59e9 100644 --- a/app/src/commonTest/java/org/tasks/TestUtilities.kt +++ b/app/src/commonTest/java/org/tasks/TestUtilities.kt @@ -3,6 +3,7 @@ package org.tasks import android.content.Context import at.bitfire.ical4android.Task.Companion.tasksFromReader import com.todoroo.astrid.data.Task +import kotlinx.coroutines.runBlocking import org.tasks.caldav.CaldavConverter import org.tasks.data.CaldavTask import org.tasks.preferences.Preferences @@ -13,13 +14,15 @@ import java.nio.file.Paths import java.util.* object TestUtilities { - fun withTZ(id: String, runnable: () -> Unit) = withTZ(TimeZone.getTimeZone(id), runnable) + fun withTZ(id: String, runnable: suspend () -> Unit) = withTZ(TimeZone.getTimeZone(id), runnable) - fun withTZ(tz: TimeZone, runnable: () -> Unit) { + fun withTZ(tz: TimeZone, runnable: suspend () -> Unit) { val def = TimeZone.getDefault() try { TimeZone.setDefault(tz) - runnable() + runBlocking { + runnable() + } } finally { TimeZone.setDefault(def) } diff --git a/app/src/main/java/org/tasks/caldav/CaldavConverter.java b/app/src/main/java/org/tasks/caldav/CaldavConverter.java index 80d014652..d4fed037d 100644 --- a/app/src/main/java/org/tasks/caldav/CaldavConverter.java +++ b/app/src/main/java/org/tasks/caldav/CaldavConverter.java @@ -82,12 +82,12 @@ public class CaldavConverter { long startDate = task.hasStartTime() ? task.getHideUntil() : startOfDay(task.getHideUntil()); if (dueDate > 0) { startDate = Math.min(dueDate, startDate); - remote.setDue(new Due(allDay ? new Date(dueDate) : getDateTime(dueDate))); + remote.setDue(new Due(allDay ? getDate(dueDate) : getDateTime(dueDate))); } else { remote.setDue(null); } if (startDate > 0) { - remote.setDtStart(new DtStart(allDay ? new Date(startDate) : getDateTime(startDate))); + remote.setDtStart(new DtStart(allDay ? getDate(startDate) : getDateTime(startDate))); } else { remote.setDtStart(null); } @@ -121,6 +121,10 @@ public class CaldavConverter { iCalendar.Companion.setParent(remote, task.getParent() == 0 ? null : caldavTask.getRemoteParent()); } + private static Date getDate(long timestamp) { + return new Date(timestamp + newDateTime(timestamp).getOffset()); + } + private static DateTime getDateTime(long timestamp) { net.fortuna.ical4j.model.TimeZone tz = DateUtils.INSTANCE.ical4jTimeZone(TimeZone.getDefault().getID()); diff --git a/app/src/main/java/org/tasks/caldav/iCalendar.kt b/app/src/main/java/org/tasks/caldav/iCalendar.kt index 6ed408276..992117d25 100644 --- a/app/src/main/java/org/tasks/caldav/iCalendar.kt +++ b/app/src/main/java/org/tasks/caldav/iCalendar.kt @@ -26,8 +26,6 @@ import org.tasks.time.DateTime.UTC import timber.log.Timber import java.io.ByteArrayOutputStream import java.io.StringReader -import java.text.ParseException -import java.text.SimpleDateFormat import java.util.* import javax.inject.Inject @@ -162,7 +160,6 @@ class iCalendar @Inject constructor( } companion object { - private val DUE_DATE_FORMAT = SimpleDateFormat("yyyyMMdd", Locale.US) private const val APPLE_SORT_ORDER = "X-APPLE-SORT-ORDER" private val IS_PARENT = { r: RelatedTo -> r.parameters.getParameter(Parameter.RELTYPE).let { @@ -198,21 +195,14 @@ class iCalendar @Inject constructor( @JvmStatic fun getLocal(property: DateProperty): Long { - val dateTime = if (property.date is DateTime) { + val dateTime: org.tasks.time.DateTime? = if (property.date is DateTime) { val dt = property.date as DateTime org.tasks.time.DateTime( dt.time, dt.timeZone ?: if (dt.isUtc) UTC else TimeZone.getDefault() ) } else { - try { - DUE_DATE_FORMAT.parse(property.value)?.let { - org.tasks.time.DateTime(it) - } - } catch (e: ParseException) { - Timber.e(e) - null - } + org.tasks.time.DateTime(property.date.time).let { it.minusMillis(it.offset) } } return dateTime?.toLocal()?.millis ?: 0 } diff --git a/app/src/main/java/org/tasks/time/DateTime.java b/app/src/main/java/org/tasks/time/DateTime.java index c32a1c29f..9f62c1fe0 100644 --- a/app/src/main/java/org/tasks/time/DateTime.java +++ b/app/src/main/java/org/tasks/time/DateTime.java @@ -277,7 +277,7 @@ public class DateTime { return subtract(Calendar.MINUTE, minutes); } - public DateTime minusMillis(int millis) { + public DateTime minusMillis(long millis) { return new DateTime(timestamp - millis, timeZone); }