diff --git a/app/src/commonTest/java/org/tasks/TestUtilities.kt b/app/src/commonTest/java/org/tasks/TestUtilities.kt index bbace59e9..dd306da37 100644 --- a/app/src/commonTest/java/org/tasks/TestUtilities.kt +++ b/app/src/commonTest/java/org/tasks/TestUtilities.kt @@ -4,7 +4,7 @@ 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.caldav.iCalendar.Companion.applyRemote import org.tasks.data.CaldavTask import org.tasks.preferences.Preferences import org.tasks.time.DateTime @@ -40,7 +40,7 @@ object TestUtilities { fun vtodo(path: String): Task { val task = Task() - CaldavConverter.apply(task, fromResource(path)) + task.applyRemote(fromResource(path)) return task } @@ -48,7 +48,7 @@ object TestUtilities { val task = Task() val vtodo = readFile(path) val remote = fromString(vtodo) - CaldavConverter.apply(task, remote) + task.applyRemote(remote) return Triple(task, CaldavTask().apply { this.vtodo = vtodo }, remote) } diff --git a/app/src/main/java/org/tasks/caldav/CaldavConverter.java b/app/src/main/java/org/tasks/caldav/CaldavConverter.java deleted file mode 100644 index 8ecc5df03..000000000 --- a/app/src/main/java/org/tasks/caldav/CaldavConverter.java +++ /dev/null @@ -1,138 +0,0 @@ -package org.tasks.caldav; - -import static com.todoroo.andlib.utility.DateUtilities.now; -import static com.todoroo.astrid.data.Task.withoutRRULE; -import static org.tasks.caldav.iCalendar.getLocal; -import static org.tasks.date.DateTimeUtils.newDateTime; -import static org.tasks.time.DateTime.UTC; -import static org.tasks.time.DateTimeUtils.startOfDay; - -import at.bitfire.ical4android.DateUtils; -import com.todoroo.astrid.data.Task; -import com.todoroo.astrid.data.Task.Priority; -import java.text.ParseException; -import java.util.TimeZone; -import net.fortuna.ical4j.model.Date; -import net.fortuna.ical4j.model.DateTime; -import net.fortuna.ical4j.model.property.Completed; -import net.fortuna.ical4j.model.property.DtStart; -import net.fortuna.ical4j.model.property.Due; -import net.fortuna.ical4j.model.property.RRule; -import net.fortuna.ical4j.model.property.Status; -import org.tasks.data.CaldavTask; -import timber.log.Timber; - -public class CaldavConverter { - - public static void apply(Task local, at.bitfire.ical4android.Task remote) { - Completed completedAt = remote.getCompletedAt(); - if (completedAt != null) { - local.setCompletionDate(getLocal(completedAt)); - } else if (remote.getStatus() == Status.VTODO_COMPLETED) { - if (!local.isCompleted()) { - local.setCompletionDate(now()); - } - } else { - local.setCompletionDate(0L); - } - Long createdAt = remote.getCreatedAt(); - if (createdAt != null) { - local.setCreationDate(newDateTime(createdAt, UTC).toLocal().getMillis()); - } - local.setTitle(remote.getSummary()); - local.setNotes(remote.getDescription()); - local.setPriority(fromRemote(remote.getPriority())); - local.setRecurrence(remote.getRRule()); - iCalendar.Companion.apply(remote.getDue(), local); - iCalendar.Companion.apply(remote.getDtStart(), local); - } - - public static @Priority int fromRemote(int remotePriority) { - // https://tools.ietf.org/html/rfc5545#section-3.8.1.9 - if (remotePriority == 0) { - return Priority.NONE; - } - if (remotePriority == 5) { - return Priority.MEDIUM; - } - return remotePriority < 5 ? Priority.HIGH : Priority.LOW; - } - - public static int toRemote(int remotePriority, int localPriority) { - switch (localPriority) { - case Priority.NONE: - return 0; - - case Priority.MEDIUM: - return 5; - - case Priority.HIGH: - return remotePriority < 5 ? Math.max(1, remotePriority) : 1; - - default: - return remotePriority > 5 ? Math.min(9, remotePriority) : 9; - } - } - - public static void toCaldav(CaldavTask caldavTask, Task task, at.bitfire.ical4android.Task remote) { - remote.setCreatedAt(newDateTime(task.getCreationDate()).toUTC().getMillis()); - remote.setSummary(task.getTitle()); - remote.setDescription(task.getNotes()); - boolean allDay = !task.hasDueTime() && !task.hasStartTime(); - long dueDate = task.hasDueTime() ? task.getDueDate() : startOfDay(task.getDueDate()); - long startDate = task.hasStartTime() ? task.getHideUntil() : startOfDay(task.getHideUntil()); - if (dueDate > 0) { - startDate = Math.min(dueDate, startDate); - remote.setDue(new Due(allDay ? getDate(dueDate) : getDateTime(dueDate))); - } else { - remote.setDue(null); - } - if (startDate > 0) { - remote.setDtStart(new DtStart(allDay ? getDate(startDate) : getDateTime(startDate))); - } else { - remote.setDtStart(null); - } - if (task.isCompleted()) { - remote.setCompletedAt(new Completed(new DateTime(task.getCompletionDate()))); - remote.setStatus(Status.VTODO_COMPLETED); - remote.setPercentComplete(100); - } else if (remote.getCompletedAt() != null) { - remote.setCompletedAt(null); - remote.setStatus(null); - remote.setPercentComplete(null); - } - if (task.isRecurring()) { - try { - RRule rrule = new RRule(withoutRRULE(task.getRecurrenceWithoutFrom())); - 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)); - } catch (ParseException e) { - Timber.e(e); - } - } else { - remote.setRRule(null); - } - remote.setLastModified(newDateTime(task.getModificationDate()).toUTC().getMillis()); - remote.setPriority(toRemote(remote.getPriority(), task.getPriority())); - 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()); - DateTime dateTime = new DateTime(tz != null - ? timestamp - : new org.tasks.time.DateTime(timestamp).toUTC().getMillis()); - dateTime.setTimeZone(tz); - return dateTime; - } -} diff --git a/app/src/main/java/org/tasks/caldav/iCalendar.kt b/app/src/main/java/org/tasks/caldav/iCalendar.kt index 114345a09..40c5f3ba2 100644 --- a/app/src/main/java/org/tasks/caldav/iCalendar.kt +++ b/app/src/main/java/org/tasks/caldav/iCalendar.kt @@ -1,14 +1,19 @@ package org.tasks.caldav +import at.bitfire.ical4android.DateUtils.ical4jTimeZone import at.bitfire.ical4android.Task import at.bitfire.ical4android.Task.Companion.tasksFromReader +import com.todoroo.andlib.utility.DateUtilities import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.data.Task.Companion.HIDE_UNTIL_SPECIFIC_DAY import com.todoroo.astrid.data.Task.Companion.HIDE_UNTIL_SPECIFIC_DAY_TIME import com.todoroo.astrid.data.Task.Companion.URGENCY_SPECIFIC_DAY import com.todoroo.astrid.data.Task.Companion.URGENCY_SPECIFIC_DAY_TIME +import com.todoroo.astrid.data.Task.Companion.sanitizeRRule +import com.todoroo.astrid.data.Task.Companion.withoutRRULE import com.todoroo.astrid.helper.UUIDHelper import com.todoroo.astrid.service.TaskCreator +import net.fortuna.ical4j.model.Date import net.fortuna.ical4j.model.DateTime import net.fortuna.ical4j.model.Parameter import net.fortuna.ical4j.model.Property @@ -19,15 +24,20 @@ import org.tasks.caldav.GeoUtils.equalish import org.tasks.caldav.GeoUtils.toGeo import org.tasks.caldav.GeoUtils.toLikeString import org.tasks.data.* +import org.tasks.date.DateTimeUtils.newDateTime import org.tasks.jobs.WorkManager import org.tasks.location.GeofenceApi import org.tasks.preferences.Preferences import org.tasks.time.DateTime.UTC +import org.tasks.time.DateTimeUtils.startOfDay import timber.log.Timber import java.io.ByteArrayOutputStream import java.io.StringReader +import java.text.ParseException import java.util.* import javax.inject.Inject +import kotlin.math.max +import kotlin.math.min @Suppress("ClassName") class iCalendar @Inject constructor( @@ -107,7 +117,7 @@ class iCalendar @Inject constructor( } suspend fun toVtodo(caldavTask: CaldavTask, task: com.todoroo.astrid.data.Task, remoteModel: Task) { - CaldavConverter.toCaldav(caldavTask, task, remoteModel) + remoteModel.applyLocal(caldavTask, task) remoteModel.order = caldavTask.order val categories = remoteModel.categories categories.clear() @@ -139,7 +149,7 @@ class iCalendar @Inject constructor( existing?.task = id } val caldavTask = existing ?: CaldavTask(task.id, calendar.uuid, remote.uid, obj) - CaldavConverter.apply(task, remote) + task.applyRemote(remote) setPlace(task.id, remote.geoPosition) tagDao.applyTags(task, tagDataDao, getTags(remote.categories)) task.suppressSync() @@ -260,5 +270,97 @@ class iCalendar @Inject constructor( } } } + + fun com.todoroo.astrid.data.Task.applyRemote(remote: Task) { + val completedAt = remote.completedAt + if (completedAt != null) { + completionDate = getLocal(completedAt) + } else if (remote.status === Status.VTODO_COMPLETED) { + if (!isCompleted) { + completionDate = DateUtilities.now() + } + } else { + completionDate = 0L + } + remote.createdAt?.let { + creationDate = newDateTime(it, UTC).toLocal().millis + } + title = remote.summary + notes = remote.description + priority = when (remote.priority) { + // https://tools.ietf.org/html/rfc5545#section-3.8.1.9 + in 1..4 -> com.todoroo.astrid.data.Task.Priority.HIGH + 5 -> com.todoroo.astrid.data.Task.Priority.MEDIUM + in 6..9 -> com.todoroo.astrid.data.Task.Priority.LOW + else -> com.todoroo.astrid.data.Task.Priority.NONE + } + setRecurrence(remote.rRule) + remote.due.apply(this) + remote.dtStart.apply(this) + } + + fun Task.applyLocal(caldavTask: CaldavTask, task: com.todoroo.astrid.data.Task) { + createdAt = newDateTime(task.creationDate).toUTC().millis + summary = task.title + description = task.notes + val allDay = !task.hasDueTime() && !task.hasStartTime() + val dueDate = if (task.hasDueTime()) task.dueDate else task.dueDate.startOfDay() + var startDate = if (task.hasStartTime()) task.hideUntil else task.hideUntil.startOfDay() + due = if (dueDate > 0) { + startDate = min(dueDate, startDate) + Due(if (allDay) getDate(dueDate) else getDateTime(dueDate)) + } else { + null + } + dtStart = if (startDate > 0) { + DtStart(if (allDay) getDate(startDate) else getDateTime(startDate)) + } else { + null + } + if (task.isCompleted) { + completedAt = Completed(DateTime(task.completionDate)) + status = Status.VTODO_COMPLETED + percentComplete = 100 + } else if (completedAt != null) { + completedAt = null + status = null + percentComplete = null + } + rRule = if (task.isRecurring) { + try { + val rrule = RRule(task.getRecurrenceWithoutFrom().withoutRRULE()) + val repeatUntil = task.repeatUntil + rrule + .recur.until = if (repeatUntil > 0) DateTime(newDateTime(repeatUntil).toUTC().millis) else null + val sanitized: String = rrule.value.sanitizeRRule()!! // ical4j adds COUNT=-1 if there is an UNTIL value + RRule(sanitized) + } catch (e: ParseException) { + Timber.e(e) + null + } + } else { + null + } + lastModified = newDateTime(task.modificationDate).toUTC().millis + priority = when (task.priority) { + com.todoroo.astrid.data.Task.Priority.NONE -> 0 + com.todoroo.astrid.data.Task.Priority.MEDIUM -> 5 + com.todoroo.astrid.data.Task.Priority.HIGH -> + if (priority < 5) max(1, priority) else 1 + else -> if (priority > 5) min(9, priority) else 9 + } + setParent(if (task.parent == 0L) null else caldavTask.remoteParent) + } + + private fun getDate(timestamp: Long): Date { + return Date(timestamp + newDateTime(timestamp).offset) + } + + private fun getDateTime(timestamp: Long): DateTime { + val tz = ical4jTimeZone(TimeZone.getDefault().id) + val dateTime = DateTime(if (tz != null) timestamp else org.tasks.time.DateTime(timestamp).toUTC().millis) + dateTime.timeZone = tz + return dateTime + } } } \ No newline at end of file diff --git a/app/src/test/java/org/tasks/caldav/ThunderbirdTests.kt b/app/src/test/java/org/tasks/caldav/ThunderbirdTests.kt index 9608a0ecc..3fbe35737 100644 --- a/app/src/test/java/org/tasks/caldav/ThunderbirdTests.kt +++ b/app/src/test/java/org/tasks/caldav/ThunderbirdTests.kt @@ -8,6 +8,7 @@ import org.junit.Ignore import org.junit.Test import org.tasks.TestUtilities.setup import org.tasks.TestUtilities.vtodo +import org.tasks.caldav.iCalendar.Companion.applyLocal import org.tasks.time.DateTime import java.util.* @@ -96,7 +97,7 @@ class ThunderbirdTests { @Test fun dontTruncateTimeFromUntil() { val (task, caldavTask, remote) = setup("thunderbird/repeat_until_date_time.txt") - CaldavConverter.toCaldav(caldavTask, task, remote) + remote.applyLocal(caldavTask, task) assertEquals( "FREQ=WEEKLY;UNTIL=20200731T160000Z;BYDAY=MO,TU,WE,TH,FR", remote.rRule!!.value)