Synchronize start dates via iCalendar & OpenTasks

pull/1295/head
Alex Baker 5 years ago
parent aece05d7e7
commit cf2360d58d

@ -192,6 +192,8 @@ class Task : Parcelable {
val isHidden val isHidden
get() = hideUntil > DateUtilities.now() get() = hideUntil > DateUtilities.now()
fun hasStartTime() = hasDueTime(hideUntil)
fun hasStartDate() = hideUntil > 0 fun hasStartDate() = hideUntil > 0
/** Checks whether task is done. Requires DUE_DATE */ /** Checks whether task is done. Requires DUE_DATE */
@ -209,7 +211,7 @@ class Task : Parcelable {
HIDE_UNTIL_DUE, HIDE_UNTIL_DUE_TIME -> dueDate HIDE_UNTIL_DUE, HIDE_UNTIL_DUE_TIME -> dueDate
HIDE_UNTIL_DAY_BEFORE -> dueDate - DateUtilities.ONE_DAY HIDE_UNTIL_DAY_BEFORE -> dueDate - DateUtilities.ONE_DAY
HIDE_UNTIL_WEEK_BEFORE -> dueDate - DateUtilities.ONE_WEEK HIDE_UNTIL_WEEK_BEFORE -> dueDate - DateUtilities.ONE_WEEK
HIDE_UNTIL_SPECIFIC_DAY, HIDE_UNTIL_SPECIFIC_DAY_TIME -> customDate.startOfDay() HIDE_UNTIL_SPECIFIC_DAY, HIDE_UNTIL_SPECIFIC_DAY_TIME -> customDate
else -> throw IllegalArgumentException("Unknown setting $setting") else -> throw IllegalArgumentException("Unknown setting $setting")
} }
if (date <= 0) { if (date <= 0) {
@ -224,7 +226,7 @@ class Task : Parcelable {
} }
/** Checks whether this due date has a due time or only a date */ /** Checks whether this due date has a due time or only a date */
fun hasDueTime(): Boolean = hasDueDate() && hasDueTime(dueDate) fun hasDueTime(): Boolean = hasDueTime(dueDate)
val isOverdue: Boolean val isOverdue: Boolean
get() { get() {
@ -368,6 +370,7 @@ class Task : Parcelable {
false false
} else title == original.title } else title == original.title
&& priority == original.priority && priority == original.priority
&& hideUntil == original.hideUntil
&& dueDate == original.dueDate && dueDate == original.dueDate
&& completionDate == original.completionDate && completionDate == original.completionDate
&& deletionDate == original.deletionDate && deletionDate == original.deletionDate

@ -5,6 +5,7 @@ import static com.todoroo.astrid.data.Task.URGENCY_SPECIFIC_DAY;
import static com.todoroo.astrid.data.Task.URGENCY_SPECIFIC_DAY_TIME; import static com.todoroo.astrid.data.Task.URGENCY_SPECIFIC_DAY_TIME;
import static org.tasks.Strings.isNullOrEmpty; import static org.tasks.Strings.isNullOrEmpty;
import static org.tasks.date.DateTimeUtils.newDateTime; import static org.tasks.date.DateTimeUtils.newDateTime;
import static org.tasks.time.DateTimeUtils.startOfDay;
import at.bitfire.ical4android.DateUtils; import at.bitfire.ical4android.DateUtils;
import com.todoroo.astrid.data.Task; import com.todoroo.astrid.data.Task;
@ -26,7 +27,7 @@ import timber.log.Timber;
public class CaldavConverter { public class CaldavConverter {
private static final DateFormat DUE_DATE_FORMAT = new SimpleDateFormat("yyyyMMdd", Locale.US); static final DateFormat DUE_DATE_FORMAT = new SimpleDateFormat("yyyyMMdd", Locale.US);
public static void apply(Task local, at.bitfire.ical4android.Task remote) { public static void apply(Task local, at.bitfire.ical4android.Task remote) {
Completed completedAt = remote.getCompletedAt(); Completed completedAt = remote.getCompletedAt();
@ -54,11 +55,10 @@ public class CaldavConverter {
} else { } else {
Date dueDate = due.getDate(); Date dueDate = due.getDate();
if (dueDate instanceof DateTime) { if (dueDate instanceof DateTime) {
local.setDueDateAdjustingHideUntil( local.setDueDate(Task.createDueDate(URGENCY_SPECIFIC_DAY_TIME, dueDate.getTime()));
Task.createDueDate(URGENCY_SPECIFIC_DAY_TIME, dueDate.getTime()));
} else { } else {
try { try {
local.setDueDateAdjustingHideUntil( local.setDueDate(
Task.createDueDate( Task.createDueDate(
URGENCY_SPECIFIC_DAY, DUE_DATE_FORMAT.parse(due.getValue()).getTime())); URGENCY_SPECIFIC_DAY, DUE_DATE_FORMAT.parse(due.getValue()).getTime()));
} catch (ParseException e) { } catch (ParseException e) {
@ -66,6 +66,7 @@ public class CaldavConverter {
} }
} }
} }
iCalendar.Companion.apply(remote.getDtStart(), local);
} }
public static @Priority int fromRemote(int remotePriority) { public static @Priority int fromRemote(int remotePriority) {
@ -110,19 +111,20 @@ public class CaldavConverter {
remote.setCreatedAt(newDateTime(task.getCreationDate()).toUTC().getMillis()); remote.setCreatedAt(newDateTime(task.getCreationDate()).toUTC().getMillis());
remote.setSummary(task.getTitle()); remote.setSummary(task.getTitle());
remote.setDescription(task.getNotes()); remote.setDescription(task.getNotes());
if (task.hasDueTime()) { boolean allDay = !task.hasDueTime() && !task.hasStartTime();
net.fortuna.ical4j.model.TimeZone tz = long dueDate = task.hasDueTime() ? task.getDueDate() : startOfDay(task.getDueDate());
DateUtils.INSTANCE.ical4jTimeZone(TimeZone.getDefault().getID()); long startDate = task.hasStartTime() ? task.getHideUntil() : startOfDay(task.getHideUntil());
DateTime dateTime = new DateTime(tz != null if (dueDate > 0) {
? task.getDueDate() startDate = Math.min(dueDate, startDate);
: new org.tasks.time.DateTime(task.getDueDate()).toUTC().getMillis()); remote.setDue(new Due(allDay ? new Date(dueDate) : getDateTime(dueDate)));
dateTime.setTimeZone(tz);
remote.setDue(new Due(dateTime));
} else if (task.hasDueDate()) {
remote.setDue(new Due(new Date(task.getDueDate())));
} else { } else {
remote.setDue(null); remote.setDue(null);
} }
if (startDate > 0) {
remote.setDtStart(new DtStart(allDay ? new Date(startDate) : getDateTime(startDate)));
} else {
remote.setDtStart(null);
}
if (task.isCompleted()) { if (task.isCompleted()) {
remote.setCompletedAt(new Completed(new DateTime(task.getCompletionDate()))); remote.setCompletedAt(new Completed(new DateTime(task.getCompletionDate())));
remote.setStatus(Status.VTODO_COMPLETED); remote.setStatus(Status.VTODO_COMPLETED);
@ -158,4 +160,14 @@ public class CaldavConverter {
return remote; return remote;
} }
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;
}
} }

@ -3,15 +3,20 @@ package org.tasks.caldav
import at.bitfire.ical4android.Task import at.bitfire.ical4android.Task
import at.bitfire.ical4android.Task.Companion.tasksFromReader import at.bitfire.ical4android.Task.Companion.tasksFromReader
import com.todoroo.astrid.dao.TaskDao 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.helper.UUIDHelper import com.todoroo.astrid.helper.UUIDHelper
import com.todoroo.astrid.service.TaskCreator import com.todoroo.astrid.service.TaskCreator
import net.fortuna.ical4j.model.DateTime
import net.fortuna.ical4j.model.Parameter import net.fortuna.ical4j.model.Parameter
import net.fortuna.ical4j.model.Property import net.fortuna.ical4j.model.Property
import net.fortuna.ical4j.model.parameter.RelType import net.fortuna.ical4j.model.parameter.RelType
import net.fortuna.ical4j.model.property.DtStart
import net.fortuna.ical4j.model.property.Geo import net.fortuna.ical4j.model.property.Geo
import net.fortuna.ical4j.model.property.RelatedTo import net.fortuna.ical4j.model.property.RelatedTo
import net.fortuna.ical4j.model.property.XProperty import net.fortuna.ical4j.model.property.XProperty
import org.tasks.Strings.isNullOrEmpty import org.tasks.Strings.isNullOrEmpty
import org.tasks.caldav.CaldavConverter.DUE_DATE_FORMAT
import org.tasks.caldav.GeoUtils.equalish import org.tasks.caldav.GeoUtils.equalish
import org.tasks.caldav.GeoUtils.toGeo import org.tasks.caldav.GeoUtils.toGeo
import org.tasks.caldav.GeoUtils.toLikeString import org.tasks.caldav.GeoUtils.toLikeString
@ -22,6 +27,7 @@ import org.tasks.preferences.Preferences
import timber.log.Timber import timber.log.Timber
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.StringReader import java.io.StringReader
import java.text.ParseException
import javax.inject.Inject import javax.inject.Inject
@Suppress("ClassName") @Suppress("ClassName")
@ -158,6 +164,23 @@ class iCalendar @Inject constructor(
x?.name.equals(APPLE_SORT_ORDER, true) x?.name.equals(APPLE_SORT_ORDER, true)
} }
fun DtStart?.apply(task: com.todoroo.astrid.data.Task) {
when (this?.date) {
null -> 0
is DateTime -> task.createHideUntil(HIDE_UNTIL_SPECIFIC_DAY_TIME, date.time)
else -> try {
DUE_DATE_FORMAT.parse(value)?.let {
task.createHideUntil(HIDE_UNTIL_SPECIFIC_DAY, it.time)
}
} catch (e: ParseException) {
Timber.e(e)
null
}
}?.let {
task.hideUntil = it
}
}
fun fromVtodo(vtodo: String): Task? { fun fromVtodo(vtodo: String): Task? {
try { try {
val tasks = tasksFromReader(StringReader(vtodo)) val tasks = tasksFromReader(StringReader(vtodo))

@ -8,6 +8,9 @@ class CaldavTaskContainer {
@Embedded lateinit var task: Task @Embedded lateinit var task: Task
@Embedded lateinit var caldavTask: CaldavTask @Embedded lateinit var caldavTask: CaldavTask
val id: Long
get() = task.id
val remoteId: String? val remoteId: String?
get() = caldavTask.remoteId get() = caldavTask.remoteId
@ -20,5 +23,8 @@ class CaldavTaskContainer {
val sortOrder: Long val sortOrder: Long
get() = caldavTask.order ?: DateTime(task.creationDate).toAppleEpoch() get() = caldavTask.order ?: DateTime(task.creationDate).toAppleEpoch()
val startDate: Long
get() = task.hideUntil
override fun toString(): String = "CaldavTaskContainer{task=$task, caldavTask=$caldavTask}" override fun toString(): String = "CaldavTaskContainer{task=$task, caldavTask=$caldavTask}"
} }

@ -7,13 +7,14 @@ import android.database.Cursor
import com.todoroo.andlib.utility.DateUtilities.now import com.todoroo.andlib.utility.DateUtilities.now
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.data.Task import com.todoroo.astrid.data.Task
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
import com.todoroo.astrid.data.Task.Companion.URGENCY_SPECIFIC_DAY_TIME 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.sanitizeRRule
import com.todoroo.astrid.service.TaskCreator import com.todoroo.astrid.service.TaskCreator
import com.todoroo.astrid.service.TaskDeleter import com.todoroo.astrid.service.TaskDeleter
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import net.fortuna.ical4j.model.Date
import net.fortuna.ical4j.model.property.Geo import net.fortuna.ical4j.model.property.Geo
import net.fortuna.ical4j.model.property.RRule import net.fortuna.ical4j.model.property.RRule
import org.dmfs.tasks.contract.TaskContract.Tasks import org.dmfs.tasks.contract.TaskContract.Tasks
@ -34,9 +35,9 @@ import org.tasks.data.OpenTaskDao.Companion.isDavx5
import org.tasks.data.OpenTaskDao.Companion.isDecSync import org.tasks.data.OpenTaskDao.Companion.isDecSync
import org.tasks.data.OpenTaskDao.Companion.isEteSync import org.tasks.data.OpenTaskDao.Companion.isEteSync
import org.tasks.data.OpenTaskDao.Companion.newAccounts import org.tasks.data.OpenTaskDao.Companion.newAccounts
import org.tasks.date.DateTimeUtils.newDateTime
import org.tasks.time.DateTime import org.tasks.time.DateTime
import org.tasks.time.DateTimeUtils.currentTimeMillis import org.tasks.time.DateTimeUtils.currentTimeMillis
import org.tasks.time.DateTimeUtils.startOfDay
import timber.log.Timber import timber.log.Timber
import java.util.* import java.util.*
import javax.inject.Inject import javax.inject.Inject
@ -237,17 +238,23 @@ class OpenTasksSynchronizer @Inject constructor(
} }
RRule(rrule.value.sanitizeRRule()).value RRule(rrule.value.sanitizeRRule()).value
} else null) } else null)
values.put(Tasks.IS_ALLDAY, if (task.hasDueDate() && !task.hasDueTime()) 1 else 0) val allDay = !task.hasDueTime() && !task.hasStartTime()
values.put(Tasks.IS_ALLDAY, if (allDay) 1 else 0)
values.put(Tasks.DUE, when { values.put(Tasks.DUE, when {
task.hasDueTime() -> newDateTime(task.dueDate).toDateTime().time task.hasDueTime() -> task.dueDate
task.hasDueDate() -> Date(task.dueDate).time task.hasDueDate() -> task.dueDate.startOfDay()
else -> null
})
values.put(Tasks.DTSTART, when {
task.hasStartTime() -> task.hideUntil
task.hasStartDate() -> task.hideUntil.startOfDay()
else -> null else -> null
}) })
values.put(Tasks.COMPLETED_IS_ALLDAY, 0) values.put(Tasks.COMPLETED_IS_ALLDAY, 0)
values.put(Tasks.COMPLETED, if (task.isCompleted) task.completionDate else null) values.put(Tasks.COMPLETED, if (task.isCompleted) task.completionDate else null)
values.put(Tasks.STATUS, if (task.isCompleted) Tasks.STATUS_COMPLETED else null) values.put(Tasks.STATUS, if (task.isCompleted) Tasks.STATUS_COMPLETED else null)
values.put(Tasks.PERCENT_COMPLETE, if (task.isCompleted) 100 else null) values.put(Tasks.PERCENT_COMPLETE, if (task.isCompleted) 100 else null)
if (task.hasDueTime() || task.isCompleted) { if (!allDay || task.isCompleted) {
values.put(Tasks.TZ, TimeZone.getDefault().id) values.put(Tasks.TZ, TimeZone.getDefault().id)
} }
values.put(Tasks.PARENT_ID, null as Long?) values.put(Tasks.PARENT_ID, null as Long?)
@ -315,14 +322,19 @@ class OpenTasksSynchronizer @Inject constructor(
task.notes = it.getString(Tasks.DESCRIPTION) task.notes = it.getString(Tasks.DESCRIPTION)
task.modificationDate = currentTimeMillis() task.modificationDate = currentTimeMillis()
task.creationDate = it.getLong(Tasks.CREATED).toLocal() task.creationDate = it.getLong(Tasks.CREATED).toLocal()
task.setDueDateAdjustingHideUntil(it.getLong(Tasks.DUE).let { due -> val allDay = it.getBoolean(Tasks.IS_ALLDAY)
when { val due = it.getLong(Tasks.DUE)
due == 0L -> 0 task.dueDate = when {
it.getBoolean(Tasks.IS_ALLDAY) -> due == 0L -> 0
Task.createDueDate(URGENCY_SPECIFIC_DAY, due - DateTime(due).offset) allDay -> Task.createDueDate(URGENCY_SPECIFIC_DAY, due - DateTime(due).offset)
else -> Task.createDueDate(URGENCY_SPECIFIC_DAY_TIME, due) else -> Task.createDueDate(URGENCY_SPECIFIC_DAY_TIME, due)
} }
}) val start = it.getLong(Tasks.DTSTART)
task.hideUntil = when {
start == 0L -> 0
allDay -> task.createHideUntil(HIDE_UNTIL_SPECIFIC_DAY, start - DateTime(start).offset)
else -> task.createHideUntil(HIDE_UNTIL_SPECIFIC_DAY_TIME, start)
}
iCalendar.setPlace(task.id, it.getString(Tasks.GEO).toGeo()) iCalendar.setPlace(task.id, it.getString(Tasks.GEO).toGeo())
task.setRecurrence(it.getString(Tasks.RRULE).toRRule()) task.setRecurrence(it.getString(Tasks.RRULE).toRRule())
val tagNames = openTaskDao.getTags(listId, caldavTask) val tagNames = openTaskDao.getTags(listId, caldavTask)

@ -34,6 +34,7 @@ object DateTimeUtils {
"%dh %dm %ds", seconds / 3600L, (seconds % 3600L / 60L).toInt(), (seconds % 60L).toInt()) "%dh %dm %ds", seconds / 3600L, (seconds % 3600L / 60L).toInt(), (seconds % 60L).toInt())
} else millis.toString() } else millis.toString()
@JvmStatic
fun Long.startOfDay(): Long = if (this > 0) toDateTime().startOfDay().millis else 0 fun Long.startOfDay(): Long = if (this > 0) toDateTime().startOfDay().millis else 0
fun Long.millisOfDay(): Int = if (this > 0) toDateTime().millisOfDay else 0 fun Long.millisOfDay(): Int = if (this > 0) toDateTime().millisOfDay else 0

@ -102,6 +102,13 @@ class ThunderbirdTests {
remote.rRule!!.value) remote.rRule!!.value)
} }
@Test
fun startDateTime() {
assertEquals(
DateTime(2021, 1, 12, 11, 0, 1).millis,
vtodo("thunderbird/start_date_time.txt").hideUntil)
}
@Test @Test
@Ignore @Ignore
fun dontCrashOnMultipleTasks() { fun dontCrashOnMultipleTasks() {

@ -0,0 +1,29 @@
BEGIN:VCALENDAR
PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
VERSION:2.0
BEGIN:VTIMEZONE
TZID:America/Chicago
BEGIN:DAYLIGHT
TZOFFSETFROM:-0600
TZOFFSETTO:-0500
TZNAME:CDT
DTSTART:19700308T020000
RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:-0500
TZOFFSETTO:-0600
TZNAME:CST
DTSTART:19701101T020000
RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11
END:STANDARD
END:VTIMEZONE
BEGIN:VTODO
CREATED:20210112T165621Z
LAST-MODIFIED:20210112T165658Z
DTSTAMP:20210112T165658Z
UID:bb7afc40-8800-bb44-be1d-48f3c2909580
SUMMARY:Start datetime test
DTSTART;TZID=America/Chicago:20210112T110000
END:VTODO
END:VCALENDAR
Loading…
Cancel
Save