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
get() = hideUntil > DateUtilities.now()
fun hasStartTime() = hasDueTime(hideUntil)
fun hasStartDate() = hideUntil > 0
/** 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_DAY_BEFORE -> dueDate - DateUtilities.ONE_DAY
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")
}
if (date <= 0) {
@ -224,7 +226,7 @@ class Task : Parcelable {
}
/** 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
get() {
@ -368,6 +370,7 @@ class Task : Parcelable {
false
} else title == original.title
&& priority == original.priority
&& hideUntil == original.hideUntil
&& dueDate == original.dueDate
&& completionDate == original.completionDate
&& 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 org.tasks.Strings.isNullOrEmpty;
import static org.tasks.date.DateTimeUtils.newDateTime;
import static org.tasks.time.DateTimeUtils.startOfDay;
import at.bitfire.ical4android.DateUtils;
import com.todoroo.astrid.data.Task;
@ -26,7 +27,7 @@ import timber.log.Timber;
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) {
Completed completedAt = remote.getCompletedAt();
@ -54,11 +55,10 @@ public class CaldavConverter {
} else {
Date dueDate = due.getDate();
if (dueDate instanceof DateTime) {
local.setDueDateAdjustingHideUntil(
Task.createDueDate(URGENCY_SPECIFIC_DAY_TIME, dueDate.getTime()));
local.setDueDate(Task.createDueDate(URGENCY_SPECIFIC_DAY_TIME, dueDate.getTime()));
} else {
try {
local.setDueDateAdjustingHideUntil(
local.setDueDate(
Task.createDueDate(
URGENCY_SPECIFIC_DAY, DUE_DATE_FORMAT.parse(due.getValue()).getTime()));
} catch (ParseException e) {
@ -66,6 +66,7 @@ public class CaldavConverter {
}
}
}
iCalendar.Companion.apply(remote.getDtStart(), local);
}
public static @Priority int fromRemote(int remotePriority) {
@ -110,19 +111,20 @@ public class CaldavConverter {
remote.setCreatedAt(newDateTime(task.getCreationDate()).toUTC().getMillis());
remote.setSummary(task.getTitle());
remote.setDescription(task.getNotes());
if (task.hasDueTime()) {
net.fortuna.ical4j.model.TimeZone tz =
DateUtils.INSTANCE.ical4jTimeZone(TimeZone.getDefault().getID());
DateTime dateTime = new DateTime(tz != null
? task.getDueDate()
: new org.tasks.time.DateTime(task.getDueDate()).toUTC().getMillis());
dateTime.setTimeZone(tz);
remote.setDue(new Due(dateTime));
} else if (task.hasDueDate()) {
remote.setDue(new Due(new Date(task.getDueDate())));
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 ? new Date(dueDate) : getDateTime(dueDate)));
} else {
remote.setDue(null);
}
if (startDate > 0) {
remote.setDtStart(new DtStart(allDay ? new Date(startDate) : getDateTime(startDate)));
} else {
remote.setDtStart(null);
}
if (task.isCompleted()) {
remote.setCompletedAt(new Completed(new DateTime(task.getCompletionDate())));
remote.setStatus(Status.VTODO_COMPLETED);
@ -158,4 +160,14 @@ public class CaldavConverter {
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.Companion.tasksFromReader
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.service.TaskCreator
import net.fortuna.ical4j.model.DateTime
import net.fortuna.ical4j.model.Parameter
import net.fortuna.ical4j.model.Property
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.RelatedTo
import net.fortuna.ical4j.model.property.XProperty
import org.tasks.Strings.isNullOrEmpty
import org.tasks.caldav.CaldavConverter.DUE_DATE_FORMAT
import org.tasks.caldav.GeoUtils.equalish
import org.tasks.caldav.GeoUtils.toGeo
import org.tasks.caldav.GeoUtils.toLikeString
@ -22,6 +27,7 @@ import org.tasks.preferences.Preferences
import timber.log.Timber
import java.io.ByteArrayOutputStream
import java.io.StringReader
import java.text.ParseException
import javax.inject.Inject
@Suppress("ClassName")
@ -158,6 +164,23 @@ class iCalendar @Inject constructor(
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? {
try {
val tasks = tasksFromReader(StringReader(vtodo))

@ -8,6 +8,9 @@ class CaldavTaskContainer {
@Embedded lateinit var task: Task
@Embedded lateinit var caldavTask: CaldavTask
val id: Long
get() = task.id
val remoteId: String?
get() = caldavTask.remoteId
@ -20,5 +23,8 @@ class CaldavTaskContainer {
val sortOrder: Long
get() = caldavTask.order ?: DateTime(task.creationDate).toAppleEpoch()
val startDate: Long
get() = task.hideUntil
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.astrid.dao.TaskDao
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_TIME
import com.todoroo.astrid.data.Task.Companion.sanitizeRRule
import com.todoroo.astrid.service.TaskCreator
import com.todoroo.astrid.service.TaskDeleter
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.RRule
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.isEteSync
import org.tasks.data.OpenTaskDao.Companion.newAccounts
import org.tasks.date.DateTimeUtils.newDateTime
import org.tasks.time.DateTime
import org.tasks.time.DateTimeUtils.currentTimeMillis
import org.tasks.time.DateTimeUtils.startOfDay
import timber.log.Timber
import java.util.*
import javax.inject.Inject
@ -237,17 +238,23 @@ class OpenTasksSynchronizer @Inject constructor(
}
RRule(rrule.value.sanitizeRRule()).value
} 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 {
task.hasDueTime() -> newDateTime(task.dueDate).toDateTime().time
task.hasDueDate() -> Date(task.dueDate).time
task.hasDueTime() -> task.dueDate
task.hasDueDate() -> task.dueDate.startOfDay()
else -> null
})
values.put(Tasks.DTSTART, when {
task.hasStartTime() -> task.hideUntil
task.hasStartDate() -> task.hideUntil.startOfDay()
else -> null
})
values.put(Tasks.COMPLETED_IS_ALLDAY, 0)
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.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.PARENT_ID, null as Long?)
@ -315,14 +322,19 @@ class OpenTasksSynchronizer @Inject constructor(
task.notes = it.getString(Tasks.DESCRIPTION)
task.modificationDate = currentTimeMillis()
task.creationDate = it.getLong(Tasks.CREATED).toLocal()
task.setDueDateAdjustingHideUntil(it.getLong(Tasks.DUE).let { due ->
when {
due == 0L -> 0
it.getBoolean(Tasks.IS_ALLDAY) ->
Task.createDueDate(URGENCY_SPECIFIC_DAY, due - DateTime(due).offset)
else -> Task.createDueDate(URGENCY_SPECIFIC_DAY_TIME, due)
}
})
val allDay = it.getBoolean(Tasks.IS_ALLDAY)
val due = it.getLong(Tasks.DUE)
task.dueDate = when {
due == 0L -> 0
allDay -> Task.createDueDate(URGENCY_SPECIFIC_DAY, due - DateTime(due).offset)
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())
task.setRecurrence(it.getString(Tasks.RRULE).toRRule())
val tagNames = openTaskDao.getTags(listId, caldavTask)

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

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